Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dns/node.py: 40%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

124 statements  

1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license 

2 

3# Copyright (C) 2001-2017 Nominum, Inc. 

4# 

5# Permission to use, copy, modify, and distribute this software and its 

6# documentation for any purpose with or without fee is hereby granted, 

7# provided that the above copyright notice and this permission notice 

8# appear in all copies. 

9# 

10# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES 

11# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 

12# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR 

13# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 

14# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 

15# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 

16# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 

17 

18"""DNS nodes. A node is a set of rdatasets.""" 

19 

20import dataclasses 

21import enum 

22import io 

23from typing import Any 

24 

25import dns.immutable 

26import dns.name 

27import dns.rdataclass 

28import dns.rdataset 

29import dns.rdatatype 

30import dns.rrset 

31 

32_cname_types = { 

33 dns.rdatatype.CNAME, 

34} 

35 

36# "neutral" types can coexist with a CNAME and thus are not "other data" 

37_neutral_types = { 

38 dns.rdatatype.NSEC, # RFC 4035 section 2.5 

39 dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible! 

40 dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 

41} 

42 

43 

44def _matches_type_or_its_signature(rdtypes, rdtype, covers): 

45 return rdtype in rdtypes or (rdtype == dns.rdatatype.RRSIG and covers in rdtypes) 

46 

47 

48@dataclasses.dataclass(frozen=True) 

49class NodeStyle(dns.rdataset.RdatasetStyle): 

50 """Node text styles. 

51 

52 A ``NodeStyle`` is also a :py:class:`dns.name.NameStyle` and a 

53 :py:class:`dns.rdata.RdataStyle`, and a :py:class:`dns.rdataset.RdatasetStyle`. 

54 See those classes for a description of their options. 

55 

56 There are currently no node-specific style options, but if that changes they 

57 will be documented here. 

58 """ 

59 

60 

61@enum.unique 

62class NodeKind(enum.Enum): 

63 """Rdatasets in nodes""" 

64 

65 #: Has rdatasets not compatible with CNAME 

66 REGULAR = 0 # a.k.a "other data" 

67 #: Has rdatasets compatible with CNAME, but no CNAME 

68 NEUTRAL = 1 

69 #: Has a CNAME or RRSIG(CNAME) 

70 CNAME = 2 

71 

72 @classmethod 

73 def classify( 

74 cls, rdtype: dns.rdatatype.RdataType, covers: dns.rdatatype.RdataType 

75 ) -> "NodeKind": 

76 if _matches_type_or_its_signature(_cname_types, rdtype, covers): 

77 return NodeKind.CNAME 

78 elif _matches_type_or_its_signature(_neutral_types, rdtype, covers): 

79 return NodeKind.NEUTRAL 

80 else: 

81 return NodeKind.REGULAR 

82 

83 @classmethod 

84 def classify_rdataset(cls, rdataset: dns.rdataset.Rdataset) -> "NodeKind": 

85 return cls.classify(rdataset.rdtype, rdataset.covers) 

86 

87 

88class Node: 

89 """A Node is a set of rdatasets. 

90 

91 A node is either a CNAME node or an "other data" node. A CNAME 

92 node contains only CNAME, KEY, NSEC, and NSEC3 rdatasets along with their 

93 covering RRSIG rdatasets. An "other data" node contains any 

94 rdataset other than a CNAME or RRSIG(CNAME) rdataset. When 

95 changes are made to a node, the CNAME or "other data" state is 

96 always consistent with the update, i.e. the most recent change 

97 wins. For example, if you have a node which contains a CNAME 

98 rdataset, and then add an MX rdataset to it, then the CNAME 

99 rdataset will be deleted. Likewise if you have a node containing 

100 an MX rdataset and add a CNAME rdataset, the MX rdataset will be 

101 deleted. 

102 """ 

103 

104 __slots__ = ["rdatasets"] 

105 

106 def __init__(self): 

107 # the set of rdatasets, represented as a list. 

108 self.rdatasets = [] 

109 

110 def to_text( 

111 self, name: dns.name.Name, style: NodeStyle | None = None, **kw: Any 

112 ) -> str: 

113 """Convert a node to text format. 

114 

115 Each rdataset at the node is printed. Any keyword arguments 

116 to this method are passed on to the rdataset's to_text() method. 

117 

118 :param name: The owner name of the rdatasets. 

119 :type name: :py:class:`dns.name.Name` 

120 :param style: If specified, overrides the other parameters except 

121 *name*. 

122 :type style: :py:class:`dns.node.NodeStyle` or ``None`` 

123 :rtype: str 

124 """ 

125 if style is None: 

126 style = NodeStyle.from_keywords(kw) 

127 return self.to_styled_text(style, name) 

128 

129 def to_styled_text(self, style: NodeStyle, name: dns.name.Name) -> str: 

130 """Convert a node to text format. 

131 

132 Each rdataset at the node is printed. 

133 

134 :param name: The owner name of the rdatasets. 

135 :type name: :py:class:`dns.name.Name` 

136 

137 See the documentation for :py:class:`dns.node.NodeStyle` for a description 

138 of the style parameters. 

139 

140 :rtype: str 

141 """ 

142 

143 s = io.StringIO() 

144 for rds in self.rdatasets: 

145 if len(rds) > 0: 

146 s.write(rds.to_styled_text(style, name)) 

147 s.write("\n") 

148 if style.deduplicate_names and not style.first_name_is_duplicate: 

149 style = style.replace(first_name_is_duplicate=True) 

150 return s.getvalue()[:-1] 

151 

152 def __repr__(self): 

153 return "<DNS node " + str(id(self)) + ">" # pragma: no cover 

154 

155 def __eq__(self, other): 

156 # 

157 # This is inefficient. Good thing we don't need to do it much. 

158 # 

159 for rd in self.rdatasets: 

160 if rd not in other.rdatasets: 

161 return False 

162 for rd in other.rdatasets: 

163 if rd not in self.rdatasets: 

164 return False 

165 return True 

166 

167 def __ne__(self, other): 

168 return not self.__eq__(other) # pragma: no cover 

169 

170 def __len__(self): 

171 return len(self.rdatasets) 

172 

173 def __iter__(self): 

174 return iter(self.rdatasets) 

175 

176 def _append_rdataset(self, rdataset): 

177 """Append rdataset to the node with special handling for CNAME and 

178 other data conditions. 

179 

180 Specifically, if the rdataset being appended has ``NodeKind.CNAME``, 

181 then all rdatasets other than KEY, NSEC, NSEC3, and their covering 

182 RRSIGs are deleted. If the rdataset being appended has 

183 ``NodeKind.REGULAR`` then CNAME and RRSIG(CNAME) are deleted. 

184 """ 

185 # Make having just one rdataset at the node fast. 

186 if len(self.rdatasets) > 0: 

187 kind = NodeKind.classify_rdataset(rdataset) 

188 if kind == NodeKind.CNAME: 

189 self.rdatasets = [ 

190 rds 

191 for rds in self.rdatasets 

192 if NodeKind.classify_rdataset(rds) != NodeKind.REGULAR 

193 ] 

194 elif kind == NodeKind.REGULAR: 

195 self.rdatasets = [ 

196 rds 

197 for rds in self.rdatasets 

198 if NodeKind.classify_rdataset(rds) != NodeKind.CNAME 

199 ] 

200 # Otherwise the rdataset is NodeKind.NEUTRAL and we do not need to 

201 # edit self.rdatasets. 

202 self.rdatasets.append(rdataset) 

203 

204 def find_rdataset( 

205 self, 

206 rdclass: dns.rdataclass.RdataClass, 

207 rdtype: dns.rdatatype.RdataType, 

208 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

209 create: bool = False, 

210 ) -> dns.rdataset.Rdataset: 

211 """Find an rdataset matching the specified properties in the 

212 current node. 

213 

214 :param rdclass: The class of the rdataset. 

215 :type rdclass: :py:class:`dns.rdataclass.RdataClass` 

216 :param rdtype: The type of the rdataset. 

217 :type rdtype: :py:class:`dns.rdatatype.RdataType` 

218 :param covers: The covered type. Usually ``dns.rdatatype.NONE``, but 

219 for SIG/RRSIG the type being covered (e.g. A, NS, SOA). 

220 :type covers: :py:class:`dns.rdatatype.RdataType` 

221 :param create: If ``True``, create the rdataset if not found. 

222 :type create: bool 

223 :raises KeyError: If no matching rdataset exists and *create* is not 

224 ``True``. 

225 :rtype: :py:class:`dns.rdataset.Rdataset` 

226 """ 

227 

228 for rds in self.rdatasets: 

229 if rds.match(rdclass, rdtype, covers): 

230 return rds 

231 if not create: 

232 raise KeyError 

233 rds = dns.rdataset.Rdataset(rdclass, rdtype, covers) 

234 self._append_rdataset(rds) 

235 return rds 

236 

237 def get_rdataset( 

238 self, 

239 rdclass: dns.rdataclass.RdataClass, 

240 rdtype: dns.rdatatype.RdataType, 

241 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

242 create: bool = False, 

243 ) -> dns.rdataset.Rdataset | None: 

244 """Get an rdataset matching the specified properties in the 

245 current node. 

246 

247 None is returned if an rdataset of the specified type and 

248 class does not exist and *create* is not ``True``. 

249 

250 :param rdclass: The class of the rdataset. 

251 :type rdclass: :py:class:`dns.rdataclass.RdataClass` 

252 :param rdtype: The type of the rdataset. 

253 :type rdtype: :py:class:`dns.rdatatype.RdataType` 

254 :param covers: The covered type (usually ``dns.rdatatype.NONE``). 

255 :type covers: :py:class:`dns.rdatatype.RdataType` 

256 :param create: If ``True``, create the rdataset if not found. 

257 :type create: bool 

258 :rtype: :py:class:`dns.rdataset.Rdataset` or ``None`` 

259 """ 

260 

261 try: 

262 rds = self.find_rdataset(rdclass, rdtype, covers, create) 

263 except KeyError: 

264 rds = None 

265 return rds 

266 

267 def delete_rdataset( 

268 self, 

269 rdclass: dns.rdataclass.RdataClass, 

270 rdtype: dns.rdatatype.RdataType, 

271 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

272 ) -> None: 

273 """Delete the rdataset matching the specified properties in the 

274 current node. 

275 

276 If a matching rdataset does not exist, it is not an error. 

277 

278 :param rdclass: The class of the rdataset. 

279 :type rdclass: :py:class:`dns.rdataclass.RdataClass` 

280 :param rdtype: The type of the rdataset. 

281 :type rdtype: :py:class:`dns.rdatatype.RdataType` 

282 :param covers: The covered type. 

283 :type covers: :py:class:`dns.rdatatype.RdataType` 

284 """ 

285 

286 rds = self.get_rdataset(rdclass, rdtype, covers) 

287 if rds is not None: 

288 self.rdatasets.remove(rds) 

289 

290 def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: 

291 """Replace an rdataset. 

292 

293 It is not an error if there is no rdataset matching *replacement*. 

294 

295 Ownership of the *replacement* object is transferred to the node; 

296 in other words, this method does not store a copy of *replacement* 

297 at the node, it stores *replacement* itself. 

298 

299 :param replacement: The replacement rdataset. 

300 :type replacement: :py:class:`dns.rdataset.Rdataset` 

301 :raises ValueError: If *replacement* is not a 

302 :py:class:`dns.rdataset.Rdataset`. 

303 """ 

304 

305 if not isinstance(replacement, dns.rdataset.Rdataset): 

306 raise ValueError("replacement is not an rdataset") # pragma: no cover 

307 if isinstance(replacement, dns.rrset.RRset): 

308 # RRsets are not good replacements as the match() method 

309 # is not compatible. 

310 replacement = replacement.to_rdataset() 

311 self.delete_rdataset( 

312 replacement.rdclass, replacement.rdtype, replacement.covers 

313 ) 

314 self._append_rdataset(replacement) 

315 

316 def classify(self) -> NodeKind: 

317 """Classify a node. 

318 

319 A node which contains a CNAME or RRSIG(CNAME) is a 

320 ``NodeKind.CNAME`` node. 

321 

322 A node which contains only "neutral" types, i.e. types allowed to 

323 co-exist with a CNAME, is a ``NodeKind.NEUTRAL`` node. The neutral 

324 types are NSEC, NSEC3, KEY, and their associated RRSIGS. An empty node 

325 is also considered neutral. 

326 

327 A node which contains some rdataset which is not a CNAME, RRSIG(CNAME), 

328 or a neutral type is a a ``NodeKind.REGULAR`` node. Regular nodes are 

329 also commonly referred to as "other data". 

330 """ 

331 for rdataset in self.rdatasets: 

332 kind = NodeKind.classify(rdataset.rdtype, rdataset.covers) 

333 if kind != NodeKind.NEUTRAL: 

334 return kind 

335 return NodeKind.NEUTRAL 

336 

337 def is_immutable(self) -> bool: 

338 return False 

339 

340 

341@dns.immutable.immutable 

342class ImmutableNode(Node): 

343 def __init__(self, node): 

344 super().__init__() 

345 self.rdatasets = tuple( 

346 [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets] 

347 ) 

348 

349 def find_rdataset( 

350 self, 

351 rdclass: dns.rdataclass.RdataClass, 

352 rdtype: dns.rdatatype.RdataType, 

353 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

354 create: bool = False, 

355 ) -> dns.rdataset.Rdataset: 

356 if create: 

357 raise TypeError("immutable") 

358 return super().find_rdataset(rdclass, rdtype, covers, False) 

359 

360 def get_rdataset( 

361 self, 

362 rdclass: dns.rdataclass.RdataClass, 

363 rdtype: dns.rdatatype.RdataType, 

364 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

365 create: bool = False, 

366 ) -> dns.rdataset.Rdataset | None: 

367 if create: 

368 raise TypeError("immutable") 

369 return super().get_rdataset(rdclass, rdtype, covers, False) 

370 

371 def delete_rdataset( 

372 self, 

373 rdclass: dns.rdataclass.RdataClass, 

374 rdtype: dns.rdatatype.RdataType, 

375 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, 

376 ) -> None: 

377 raise TypeError("immutable") 

378 

379 def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: 

380 raise TypeError("immutable") 

381 

382 def is_immutable(self) -> bool: 

383 return True