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
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
1# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license
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.
18"""DNS nodes. A node is a set of rdatasets."""
20import dataclasses
21import enum
22import io
23from typing import Any
25import dns.immutable
26import dns.name
27import dns.rdataclass
28import dns.rdataset
29import dns.rdatatype
30import dns.rrset
32_cname_types = {
33 dns.rdatatype.CNAME,
34}
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}
44def _matches_type_or_its_signature(rdtypes, rdtype, covers):
45 return rdtype in rdtypes or (rdtype == dns.rdatatype.RRSIG and covers in rdtypes)
48@dataclasses.dataclass(frozen=True)
49class NodeStyle(dns.rdataset.RdatasetStyle):
50 """Node text styles.
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.
56 There are currently no node-specific style options, but if that changes they
57 will be documented here.
58 """
61@enum.unique
62class NodeKind(enum.Enum):
63 """Rdatasets in nodes"""
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
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
83 @classmethod
84 def classify_rdataset(cls, rdataset: dns.rdataset.Rdataset) -> "NodeKind":
85 return cls.classify(rdataset.rdtype, rdataset.covers)
88class Node:
89 """A Node is a set of rdatasets.
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 """
104 __slots__ = ["rdatasets"]
106 def __init__(self):
107 # the set of rdatasets, represented as a list.
108 self.rdatasets = []
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.
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.
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)
129 def to_styled_text(self, style: NodeStyle, name: dns.name.Name) -> str:
130 """Convert a node to text format.
132 Each rdataset at the node is printed.
134 :param name: The owner name of the rdatasets.
135 :type name: :py:class:`dns.name.Name`
137 See the documentation for :py:class:`dns.node.NodeStyle` for a description
138 of the style parameters.
140 :rtype: str
141 """
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]
152 def __repr__(self):
153 return "<DNS node " + str(id(self)) + ">" # pragma: no cover
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
167 def __ne__(self, other):
168 return not self.__eq__(other) # pragma: no cover
170 def __len__(self):
171 return len(self.rdatasets)
173 def __iter__(self):
174 return iter(self.rdatasets)
176 def _append_rdataset(self, rdataset):
177 """Append rdataset to the node with special handling for CNAME and
178 other data conditions.
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)
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.
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 """
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
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.
247 None is returned if an rdataset of the specified type and
248 class does not exist and *create* is not ``True``.
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 """
261 try:
262 rds = self.find_rdataset(rdclass, rdtype, covers, create)
263 except KeyError:
264 rds = None
265 return rds
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.
276 If a matching rdataset does not exist, it is not an error.
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 """
286 rds = self.get_rdataset(rdclass, rdtype, covers)
287 if rds is not None:
288 self.rdatasets.remove(rds)
290 def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
291 """Replace an rdataset.
293 It is not an error if there is no rdataset matching *replacement*.
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.
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 """
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)
316 def classify(self) -> NodeKind:
317 """Classify a node.
319 A node which contains a CNAME or RRSIG(CNAME) is a
320 ``NodeKind.CNAME`` node.
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.
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
337 def is_immutable(self) -> bool:
338 return False
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 )
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)
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)
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")
379 def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None:
380 raise TypeError("immutable")
382 def is_immutable(self) -> bool:
383 return True