Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/dns/rdataset.py: 32%
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 rdatasets (an rdataset is a set of rdatas of a given type and class)"""
20import dataclasses
21import io
22import random
23import struct
24from collections.abc import Collection
25from typing import Any, cast
27import dns.exception
28import dns.immutable
29import dns.name
30import dns.rdata
31import dns.rdataclass
32import dns.rdatatype
33import dns.set
34import dns.ttl
35from dns._render_util import prefixed_length
37# define SimpleSet here for backwards compatibility
38SimpleSet = dns.set.Set
41class DifferingCovers(dns.exception.DNSException):
42 """An attempt was made to add a DNS SIG/RRSIG whose covered type
43 is not the same as that of the other rdatas in the rdataset."""
46class IncompatibleTypes(dns.exception.DNSException):
47 """An attempt was made to add DNS RR data of an incompatible type."""
50@dataclasses.dataclass(frozen=True)
51class RdatasetStyle(dns.rdata.RdataStyle):
52 """Rdataset text styles
54 An ``RdatasetStyle`` is also a :py:class:`dns.name.NameStyle` and a
55 :py:class:`dns.rdata.RdataStyle`. See those classes
56 for a description of their options.
58 .. attribute:: override_rdclass
60 A :py:class:`dns.rdataclass.RdataClass` or ``None``. If not ``None``,
61 use this class instead of the Rdataset's class.
63 .. attribute:: want_comments
65 A ``bool``. If ``True``, emit comments for rdata which have them.
66 The default is ``False``.
68 .. attribute:: omit_rdclass
70 A ``bool``. If ``True``, do not print the RdataClass. The default is
71 ``False``.
73 .. attribute:: omit_ttl
75 A ``bool``. If ``True``, do not print the TTL. The default is
76 ``False``. Use of this option may lose information.
78 .. attribute:: want_generic
80 A ``bool``. If ``True``, print RdataClass, RdataType, and Rdatas in
81 the generic format (the "unknown rdata format"). The default is
82 ``False``.
84 .. attribute:: deduplicate_names
86 A ``bool``. If ``True``, print whitespace instead of the owner name
87 when the owner name of an RR is the same as the prior RR's owner name.
88 The default is ``False``.
90 .. attribute:: first_name_is_duplicate
92 A ``bool``. If ``True``, consider the first owner name of the rdataset
93 as a duplicate too, emitting whitespace for it as well. Useful when
94 emitting a Node of multiple rdatasets and the current rdataset is not
95 the first. The default is ``False``.
97 .. attribute:: default_ttl
99 An ``int`` or ``None``. If ``None`` (the default), there is no default
100 TTL. If an integer is specified, any TTL matching that value will be
101 omitted. When emitting a zonefile, a value other than ``None`` will
102 cause a ``$TTL`` directive to be emitted.
104 .. attribute:: name_just
106 An ``int``. The owner name field justification. Negative values are
107 left-justified, positive values are right-justified. Zero (the default)
108 means no justification.
110 .. attribute:: ttl_just
112 An ``int``. The TTL field justification. Negative values are
113 left-justified, positive values are right-justified. Zero (the default)
114 means no justification.
116 .. attribute:: rdclass_just
118 An ``int``. The RdataClass field justification. Negative values are
119 left-justified, positive values are right-justified. Zero (the default)
120 means no justification.
122 .. attribute:: rdtype_just
124 An ``int``. The RdataType field justification. Negative values are
125 left-justified, positive values are right-justified. Zero (the default)
126 means no justification.
127 """
129 override_rdclass: dns.rdataclass.RdataClass | None = None
130 want_comments: bool = False
131 omit_rdclass: bool = False
132 omit_ttl: bool = False
133 want_generic: bool = False
134 deduplicate_names: bool = False
135 first_name_is_duplicate: bool = False
136 default_ttl: int | None = None
137 name_just: int = 0
138 ttl_just: int = 0
139 rdclass_just: int = 0
140 rdtype_just: int = 0
143def justify(text: str, amount: int):
144 if amount == 0:
145 return text
146 if amount < 0:
147 return text.ljust(-1 * amount)
148 else:
149 return text.rjust(amount)
152class Rdataset(dns.set.Set):
153 """A DNS rdataset."""
155 __slots__ = ["rdclass", "rdtype", "covers", "ttl"]
157 def __init__(
158 self,
159 rdclass: dns.rdataclass.RdataClass,
160 rdtype: dns.rdatatype.RdataType,
161 covers: dns.rdatatype.RdataType = dns.rdatatype.NONE,
162 ttl: int = 0,
163 ):
164 """Create a new rdataset of the specified class and type.
166 :param rdclass: The rdataclass.
167 :type rdclass: :py:class:`dns.rdataclass.RdataClass`
168 :param rdtype: The rdatatype.
169 :type rdtype: :py:class:`dns.rdatatype.RdataType`
170 :param covers: The covered rdatatype.
171 :type covers: :py:class:`dns.rdatatype.RdataType`
172 :param ttl: The TTL.
173 :type ttl: int
174 """
176 super().__init__()
177 self.rdclass = rdclass
178 self.rdtype: dns.rdatatype.RdataType = rdtype
179 self.covers: dns.rdatatype.RdataType = covers
180 self.ttl = ttl
182 def _clone(self):
183 obj = cast(Rdataset, super()._clone())
184 obj.rdclass = self.rdclass
185 obj.rdtype = self.rdtype
186 obj.covers = self.covers
187 obj.ttl = self.ttl
188 return obj
190 def update_ttl(self, ttl: int) -> None:
191 """Perform TTL minimization.
193 Set the TTL of the rdataset to be the lesser of the set's current
194 TTL or the specified TTL. If the set contains no rdatas, set the TTL
195 to the specified TTL.
197 :param ttl: The candidate TTL value.
198 :type ttl: int or str
199 """
200 ttl = dns.ttl.make(ttl)
201 if len(self) == 0:
202 self.ttl = ttl
203 elif ttl < self.ttl:
204 self.ttl = ttl
206 # pylint: disable=arguments-differ,arguments-renamed
207 def add(self, rd: dns.rdata.Rdata, ttl: int | None = None) -> None: # type: ignore
208 """Add the specified rdata to the rdataset.
210 If the optional *ttl* parameter is supplied, then
211 ``self.update_ttl(ttl)`` will be called prior to adding the rdata.
213 :param rd: The rdata to add.
214 :type rd: :py:class:`dns.rdata.Rdata`
215 :param ttl: The TTL.
216 :type ttl: int or ``None``
217 :raises dns.rdataset.IncompatibleTypes: If the type and class do not
218 match the type and class of the rdataset.
219 :raises dns.rdataset.DifferingCovers: If the type is a signature type
220 and the covered type does not match that of the rdataset.
221 """
223 #
224 # If we're adding a signature, do some special handling to
225 # check that the signature covers the same type as the
226 # other rdatas in this rdataset. If this is the first rdata
227 # in the set, initialize the covers field.
228 #
229 if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype:
230 raise IncompatibleTypes
231 if ttl is not None:
232 self.update_ttl(ttl)
233 if self.rdtype == dns.rdatatype.RRSIG or self.rdtype == dns.rdatatype.SIG:
234 covers = rd.covers()
235 if len(self) == 0 and self.covers == dns.rdatatype.NONE:
236 self.covers = covers
237 elif self.covers != covers:
238 raise DifferingCovers
239 if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0:
240 self.clear()
241 super().add(rd)
243 def union_update(self, other):
244 self.update_ttl(other.ttl)
245 super().union_update(other)
247 def intersection_update(self, other):
248 self.update_ttl(other.ttl)
249 super().intersection_update(other)
251 def update(self, other):
252 """Add all rdatas in other to self.
254 :param other: The rdataset from which to update.
255 :type other: :py:class:`dns.rdataset.Rdataset`
256 """
258 self.update_ttl(other.ttl)
259 super().update(other)
261 def _rdata_repr(self):
262 def maybe_truncate(s):
263 if len(s) > 100:
264 return s[:100] + "..."
265 return s
267 return "[" + ", ".join(f"<{maybe_truncate(str(rr))}>" for rr in self) + "]"
269 def __repr__(self):
270 if self.covers == 0:
271 ctext = ""
272 else:
273 ctext = "(" + dns.rdatatype.to_text(self.covers) + ")"
274 return (
275 "<DNS "
276 + dns.rdataclass.to_text(self.rdclass)
277 + " "
278 + dns.rdatatype.to_text(self.rdtype)
279 + ctext
280 + " rdataset: "
281 + self._rdata_repr()
282 + ">"
283 )
285 def __str__(self):
286 return self.to_text()
288 def __eq__(self, other):
289 if not isinstance(other, Rdataset):
290 return False
291 if (
292 self.rdclass != other.rdclass
293 or self.rdtype != other.rdtype
294 or self.covers != other.covers
295 ):
296 return False
297 return super().__eq__(other)
299 def __ne__(self, other):
300 return not self.__eq__(other)
302 def to_text(
303 self,
304 name: dns.name.Name | None = None,
305 origin: dns.name.Name | None = None,
306 relativize: bool = True,
307 override_rdclass: dns.rdataclass.RdataClass | None = None,
308 want_comments: bool = False,
309 style: RdatasetStyle | None = None,
310 **kw: Any,
311 ) -> str:
312 """Convert the rdataset into DNS zone file format.
314 See ``dns.name.Name.choose_relativity`` for more information
315 on how *origin* and *relativize* determine the way names
316 are emitted.
318 Any additional keyword arguments are passed on to the rdata
319 ``to_text()`` method.
321 :param name: If not ``None``, emit RRs with this as the owner name.
322 :type name: :py:class:`dns.name.Name` or ``None``
323 :param origin: The origin for relative names.
324 :type origin: :py:class:`dns.name.Name` or ``None``
325 :param relativize: If ``True``, names will be relativized to *origin*.
326 :type relativize: bool
327 :param override_rdclass: If not ``None``, emit records as if they were
328 of this class.
329 :type override_rdclass: :py:class:`dns.rdataclass.RdataClass` or ``None``
330 :param want_comments: If ``True``, emit comments for rdata which have
331 them. The default is ``False``.
332 :type want_comments: bool
333 :param style: If specified, overrides the other parameters except
334 *name*.
335 :type style: :py:class:`dns.rdataset.RdatasetStyle` or ``None``
336 """
337 if style is None:
338 kw = kw.copy()
339 kw["origin"] = origin
340 kw["relativize"] = relativize
341 kw["override_rdclass"] = override_rdclass
342 kw["want_comments"] = want_comments
343 style = RdatasetStyle.from_keywords(kw)
344 return self.to_styled_text(style, name)
346 def to_styled_text(
347 self, style: RdatasetStyle, name: dns.name.Name | None = None
348 ) -> str:
349 """Convert the rdataset into styled text format.
351 See the documentation for :py:class:`dns.rdataset.RdatasetStyle` for a description
352 of the style parameters.
353 """
354 if name is not None:
355 if style.deduplicate_names and style.first_name_is_duplicate:
356 ntext = " "
357 else:
358 ntext = f"{name.to_styled_text(style)} "
359 ntext = justify(ntext, style.name_just)
360 else:
361 ntext = ""
362 s = io.StringIO()
363 if style.override_rdclass is not None:
364 rdclass = style.override_rdclass
365 else:
366 rdclass = self.rdclass
367 if style.omit_rdclass:
368 rdclass_text = ""
369 elif style.want_generic:
370 rdclass_text = f"CLASS{rdclass} "
371 else:
372 rdclass_text = f"{dns.rdataclass.to_text(rdclass)} "
373 rdclass_text = justify(rdclass_text, style.rdclass_just)
374 if style.want_generic:
375 rdtype_text = f"TYPE{self.rdtype}"
376 else:
377 rdtype_text = f"{dns.rdatatype.to_text(self.rdtype)}"
378 rdtype_text = justify(rdtype_text, style.rdtype_just)
379 if len(self) == 0:
380 #
381 # Empty rdatasets are used for the question section, and in
382 # some dynamic updates, so we don't need to print out the TTL
383 # (which is meaningless anyway).
384 #
385 s.write(f"{ntext}{rdclass_text}{rdtype_text}\n")
386 else:
387 if style.omit_ttl or (
388 style.default_ttl is not None and self.ttl == style.default_ttl
389 ):
390 ttl = ""
391 else:
392 ttl = f"{self.ttl} "
393 ttl = justify(ttl, style.ttl_just)
394 for rd in self:
395 extra = ""
396 if style.want_comments:
397 if rd.rdcomment:
398 extra = f" ;{rd.rdcomment}"
399 if style.want_generic:
400 rdata_text = rd.to_generic().to_styled_text(style)
401 else:
402 rdata_text = rd.to_styled_text(style)
403 s.write(
404 f"{ntext}{ttl}{rdclass_text}{rdtype_text} {rdata_text}{extra}\n"
405 )
406 if style.deduplicate_names:
407 ntext = " "
408 ntext = justify(ntext, style.name_just)
410 #
411 # We strip off the final \n for the caller's convenience in printing
412 #
413 return s.getvalue()[:-1]
415 def to_wire(
416 self,
417 name: dns.name.Name,
418 file: Any,
419 compress: dns.name.CompressType | None = None,
420 origin: dns.name.Name | None = None,
421 override_rdclass: dns.rdataclass.RdataClass | None = None,
422 want_shuffle: bool = True,
423 ) -> int:
424 """Convert the rdataset to wire format.
426 :param name: The owner name to use.
427 :type name: :py:class:`dns.name.Name`
428 :param file: The file where the name is emitted (typically a
429 ``BytesIO`` file).
430 :param compress: The compression table to use. If ``None`` (the
431 default), names will not be compressed.
432 :type compress: dict or ``None``
433 :param origin: If the name is relative and *origin* is not ``None``,
434 *origin* will be appended to it.
435 :type origin: :py:class:`dns.name.Name` or ``None``
436 :param override_rdclass: If not ``None``, used as the class instead of
437 the class of the rdataset. Useful for dynamic update rendering.
438 :type override_rdclass: int or ``None``
439 :param want_shuffle: If ``True``, the order of Rdatas within the
440 Rdataset will be shuffled before rendering.
441 :type want_shuffle: bool
443 :returns: The number of records emitted.
444 :rtype: int
445 """
447 if override_rdclass is not None:
448 rdclass = override_rdclass
449 want_shuffle = False
450 else:
451 rdclass = self.rdclass
452 if len(self) == 0:
453 name.to_wire(file, compress, origin)
454 file.write(struct.pack("!HHIH", self.rdtype, rdclass, 0, 0))
455 return 1
456 else:
457 l: Rdataset | list[dns.rdata.Rdata]
458 if want_shuffle:
459 l = list(self)
460 random.shuffle(l)
461 else:
462 l = self
463 for rd in l:
464 name.to_wire(file, compress, origin)
465 file.write(struct.pack("!HHI", self.rdtype, rdclass, self.ttl))
466 with prefixed_length(file, 2):
467 rd.to_wire(file, compress, origin)
468 return len(self)
470 def match(
471 self,
472 rdclass: dns.rdataclass.RdataClass,
473 rdtype: dns.rdatatype.RdataType,
474 covers: dns.rdatatype.RdataType,
475 ) -> bool:
476 """Returns ``True`` if this rdataset matches the specified class,
477 type, and covers.
478 """
479 if self.rdclass == rdclass and self.rdtype == rdtype and self.covers == covers:
480 return True
481 return False
483 def processing_order(self) -> list[dns.rdata.Rdata]:
484 """Return rdatas in a valid processing order according to the type's
485 specification. For example, MX records are in preference order from
486 lowest to highest preferences, with items of the same preference
487 shuffled.
489 For types that do not define a processing order, the rdatas are
490 simply shuffled.
491 """
492 if len(self) == 0:
493 return []
494 else:
495 return self[0]._processing_order(iter(self)) # pyright: ignore
498@dns.immutable.immutable
499class ImmutableRdataset(Rdataset): # lgtm[py/missing-equals]
500 """An immutable DNS rdataset."""
502 _clone_class = Rdataset
504 def __init__(self, rdataset: Rdataset):
505 """Create an immutable rdataset from the specified rdataset."""
507 super().__init__(
508 rdataset.rdclass, rdataset.rdtype, rdataset.covers, rdataset.ttl
509 )
510 self.items = dns.immutable.Dict(rdataset.items)
512 def update_ttl(self, ttl):
513 raise TypeError("immutable")
515 def add(self, rd, ttl=None): # pyright: ignore
516 raise TypeError("immutable")
518 def union_update(self, other):
519 raise TypeError("immutable")
521 def intersection_update(self, other):
522 raise TypeError("immutable")
524 def update(self, other):
525 raise TypeError("immutable")
527 def __delitem__(self, i):
528 raise TypeError("immutable")
530 # lgtm complains about these not raising ArithmeticError, but there is
531 # precedent for overrides of these methods in other classes to raise
532 # TypeError, and it seems like the better exception.
534 def __ior__(self, other): # lgtm[py/unexpected-raise-in-special-method]
535 raise TypeError("immutable")
537 def __iand__(self, other): # lgtm[py/unexpected-raise-in-special-method]
538 raise TypeError("immutable")
540 def __iadd__(self, other): # lgtm[py/unexpected-raise-in-special-method]
541 raise TypeError("immutable")
543 def __isub__(self, other): # lgtm[py/unexpected-raise-in-special-method]
544 raise TypeError("immutable")
546 def clear(self):
547 raise TypeError("immutable")
549 def __copy__(self):
550 return ImmutableRdataset(super().copy()) # pyright: ignore
552 def copy(self):
553 return ImmutableRdataset(super().copy()) # pyright: ignore
555 def union(self, other):
556 return ImmutableRdataset(super().union(other)) # pyright: ignore
558 def intersection(self, other):
559 return ImmutableRdataset(super().intersection(other)) # pyright: ignore
561 def difference(self, other):
562 return ImmutableRdataset(super().difference(other)) # pyright: ignore
564 def symmetric_difference(self, other):
565 return ImmutableRdataset(super().symmetric_difference(other)) # pyright: ignore
568def from_text_list(
569 rdclass: dns.rdataclass.RdataClass | str,
570 rdtype: dns.rdatatype.RdataType | str,
571 ttl: int,
572 text_rdatas: Collection[str],
573 idna_codec: dns.name.IDNACodec | None = None,
574 origin: dns.name.Name | None = None,
575 relativize: bool = True,
576 relativize_to: dns.name.Name | None = None,
577) -> Rdataset:
578 """Create an rdataset with the specified class, type, and TTL, and with
579 the specified list of rdatas in text format.
581 :param idna_codec: Specifies the IDNA encoder/decoder. If ``None``, the
582 default IDNA 2003 encoder/decoder is used.
583 :type idna_codec: :py:class:`dns.name.IDNACodec` or ``None``
584 :param origin: The origin to use for relative names.
585 :type origin: :py:class:`dns.name.Name` or ``None``
586 :param relativize: If ``True``, names will be relativized.
587 :type relativize: bool
588 :param relativize_to: The origin to use when relativizing names. If not
589 set, *origin* is used.
590 :type relativize_to: :py:class:`dns.name.Name` or ``None``
592 :rtype: :py:class:`dns.rdataset.Rdataset`
593 """
595 rdclass = dns.rdataclass.RdataClass.make(rdclass)
596 rdtype = dns.rdatatype.RdataType.make(rdtype)
597 r = Rdataset(rdclass, rdtype)
598 r.update_ttl(ttl)
599 for t in text_rdatas:
600 rd = dns.rdata.from_text(
601 r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec
602 )
603 r.add(rd)
604 return r
607def from_text(
608 rdclass: dns.rdataclass.RdataClass | str,
609 rdtype: dns.rdatatype.RdataType | str,
610 ttl: int,
611 *text_rdatas: Any,
612) -> Rdataset:
613 """Create an rdataset with the specified class, type, and TTL, and with
614 the specified rdatas in text format.
616 :rtype: :py:class:`dns.rdataset.Rdataset`
617 """
619 return from_text_list(rdclass, rdtype, ttl, cast(Collection[str], text_rdatas))
622def from_rdata_list(ttl: int, rdatas: Collection[dns.rdata.Rdata]) -> Rdataset:
623 """Create an rdataset with the specified TTL, and with
624 the specified list of rdata objects.
626 :rtype: :py:class:`dns.rdataset.Rdataset`
627 """
629 if len(rdatas) == 0:
630 raise ValueError("rdata list must not be empty")
631 r = None
632 for rd in rdatas:
633 if r is None:
634 r = Rdataset(rd.rdclass, rd.rdtype)
635 r.update_ttl(ttl)
636 r.add(rd)
637 assert r is not None
638 return r
641def from_rdata(ttl: int, *rdatas: Any) -> Rdataset:
642 """Create an rdataset with the specified TTL, and with
643 the specified rdata objects.
645 :rtype: :py:class:`dns.rdataset.Rdataset`
646 """
648 return from_rdata_list(ttl, cast(Collection[dns.rdata.Rdata], rdatas))