From: Bob Halley Date: Wed, 18 Feb 2026 23:49:59 +0000 (-0800) Subject: Provide a "styled text" mechanism to comprehensively control (#1258) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6d2aa52d0af95f2705775f53a518253c3d6fbcb8;p=thirdparty%2Fdnspython.git Provide a "styled text" mechanism to comprehensively control (#1258) text generation, and to make it easier to enhance in the future. Styled text includes the ability to output to a (non-standard) zonefile format which is like the normal format except that Punycoded names are converted back to Unicode, and (optionally) TXT-like records may use UTF-8, and if they do the text is rendered as Unicode. --- diff --git a/Makefile b/Makefile index 3bf34c41..f9b42afc 100644 --- a/Makefile +++ b/Makefile @@ -26,8 +26,9 @@ clean: rm -rf dist rm -rf build +.PHONY: doc doc: - cd doc; make html + make -C doc html test: pytest diff --git a/dns/__init__.py b/dns/__init__.py index df8edbda..a3b0fdd7 100644 --- a/dns/__init__.py +++ b/dns/__init__.py @@ -53,6 +53,7 @@ __all__ = [ "rrset", "serial", "set", + "style", "tokenizer", "transaction", "tsig", diff --git a/dns/dnssec.py b/dns/dnssec.py index e4cf609c..6675eaa5 100644 --- a/dns/dnssec.py +++ b/dns/dnssec.py @@ -114,18 +114,7 @@ def key_id(key: DNSKEY | CDNSKEY) -> int: Returns an ``int`` between 0 and 65535 """ - rdata = key.to_wire() - assert rdata is not None # for mypy - if key.algorithm == Algorithm.RSAMD5: - return (rdata[-3] << 8) + rdata[-2] - else: - total = 0 - for i in range(len(rdata) // 2): - total += (rdata[2 * i] << 8) + rdata[2 * i + 1] - if len(rdata) % 2 != 0: - total += rdata[len(rdata) - 1] << 8 - total += (total >> 16) & 0xFFFF - return total & 0xFFFF + return key.key_id() class Policy: @@ -1193,11 +1182,11 @@ def _need_pyca(*args, **kwargs): if dns._features.have("dnssec"): from cryptography.exceptions import InvalidSignature - from cryptography.hazmat.primitives.asymmetric import ec # pylint: disable=W0611 - from cryptography.hazmat.primitives.asymmetric import ed448 # pylint: disable=W0611 - from cryptography.hazmat.primitives.asymmetric import rsa # pylint: disable=W0611 from cryptography.hazmat.primitives.asymmetric import ( # pylint: disable=W0611 + ec, # pylint: disable=W0611 + ed448, # pylint: disable=W0611 ed25519, + rsa, # pylint: disable=W0611 ) from dns.dnssecalgs import ( # pylint: disable=C0412 diff --git a/dns/message.py b/dns/message.py index 2930a23a..4436e114 100644 --- a/dns/message.py +++ b/dns/message.py @@ -18,6 +18,7 @@ """DNS Messages""" import contextlib +import dataclasses import enum import io import time @@ -33,6 +34,7 @@ import dns.opcode import dns.rcode import dns.rdata import dns.rdataclass +import dns.rdataset import dns.rdatatype import dns.rdtypes.ANY.OPT import dns.rdtypes.ANY.SOA @@ -140,6 +142,19 @@ IndexType = dict[IndexKeyType, dns.rrset.RRset] SectionType = int | str | list[dns.rrset.RRset] +@dataclasses.dataclass(frozen=True) +class MessageStyle(dns.rdataset.RdatasetStyle): + """Message text styles. + + A ``MessageStyle`` is also a :py:class:`dns.name.NameStyle` and a + :py:class:`dns.rdata.RdataStyle`, and a :py:class:`dns.rdataset.RdatasetStyle`. + See those classes for a description of their options. + + There are currently no message-specific style options, but if that changes they + will be documented here. + """ + + class Message: """A DNS message.""" @@ -213,12 +228,31 @@ class Message: self, origin: dns.name.Name | None = None, relativize: bool = True, - **kw: dict[str, Any], + style: MessageStyle | None = None, + **kw: Any, ) -> str: """Convert the message to text. The *origin*, *relativize*, and any other keyword - arguments are passed to the RRset ``to_wire()`` method. + arguments are passed to the RRset ``to_text()`` method. + + *style*, a :py:class:`dns.rdataset.RdatasetStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. + + Returns a ``str``. + """ + if style is None: + kw = kw.copy() + kw["origin"] = origin + kw["relativize"] = relativize + style = MessageStyle.from_keywords(kw) + return self.to_styled_text(style) + + def to_styled_text(self, style: MessageStyle) -> str: + """Convert the message to styled text. + + *style*, a :py:class:`dns.rdataset.RdatasetStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. Returns a ``str``. """ @@ -238,10 +272,10 @@ class Message: for name, which in self._section_enum.__members__.items(): s.write(f";{name}\n") for rrset in self.section_from_number(which): - s.write(rrset.to_text(origin, relativize, **kw)) + s.write(rrset.to_styled_text(style)) s.write("\n") if self.tsig is not None: - s.write(self.tsig.to_text(origin, relativize, **kw)) + s.write(self.tsig.to_styled_text(style)) s.write("\n") # # We strip off the final \n so the caller can print the result without @@ -566,7 +600,7 @@ class Message: tsig_ctx: Any | None = None, prepend_length: bool = False, prefer_truncation: bool = False, - **kw: dict[str, Any], + **kw: Any, ) -> bytes: """Return a string containing the message in DNS compressed wire format. diff --git a/dns/name.py b/dns/name.py index 39e78414..4fc64c85 100644 --- a/dns/name.py +++ b/dns/name.py @@ -18,6 +18,7 @@ """DNS Names.""" import copy +import dataclasses import encodings.idna # pyright: ignore import functools import struct @@ -29,6 +30,7 @@ import dns.enum import dns.exception import dns.immutable import dns.wirebase +from dns.style import BaseStyle # Dnspython will never access idna if the import fails, but pyright can't figure # that out, so... @@ -371,6 +373,30 @@ def _maybe_convert_to_binary(label: bytes | str) -> bytes: return label.encode() +@dataclasses.dataclass(frozen=True) +class NameStyle(BaseStyle): + """Name text styles + + *omit_final_dot* is a ``bool``. If True, don't emit the final + dot (denoting the root label) for absolute names. The default + is False. + + *idna_codec* specifies the IDNA decoder to use. The default is ``None`` + which means all text is in the standard DNS zonefile format, i.e. + punycode will not be decoded. + + If *origin* is ``None``, the default, then the name's relativity is not + altered before conversion to text. Otherwise, if *relativize* is ``True`` + the name is relativized, and if *relativize* is ``False`` the name is + derelativized. + """ + + omit_final_dot: bool = False + idna_codec: IDNACodec | None = None + origin: "Name | None" = None + relativize: bool = False + + @dns.immutable.immutable class Name: """A DNS name. @@ -584,54 +610,68 @@ class Name: def __str__(self): return self.to_text(False) - def to_text(self, omit_final_dot: bool = False) -> str: + def to_text( + self, omit_final_dot: bool = False, style: NameStyle | None = None + ) -> str: """Convert name to DNS text format. *omit_final_dot* is a ``bool``. If True, don't emit the final dot (denoting the root label) for absolute names. The default is False. + *style*, a :py:class:`dns.name.NameStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. + Returns a ``str``. """ - - if len(self.labels) == 0: - return "@" - if len(self.labels) == 1 and self.labels[0] == b"": - return "." - if omit_final_dot and self.is_absolute(): - l = self.labels[:-1] - else: - l = self.labels - s = ".".join(map(_escapify, l)) - return s + if style is None: + style = NameStyle(omit_final_dot=omit_final_dot) + return self.to_styled_text(style) def to_unicode( - self, omit_final_dot: bool = False, idna_codec: IDNACodec | None = None + self, + omit_final_dot: bool = False, + idna_codec: IDNACodec | None = None, + style: NameStyle | None = None, ) -> str: - """Convert name to Unicode text format. + """Convert name to DNS text format. - IDN ACE labels are converted to Unicode. + IDN ACE labels are converted to Unicode using the specified codec. *omit_final_dot* is a ``bool``. If True, don't emit the final dot (denoting the root label) for absolute names. The default is False. - *idna_codec* specifies the IDNA encoder/decoder. If None, the - dns.name.IDNA_DEFAULT encoder/decoder is used. + + Returns a ``str``. + """ + if idna_codec is None: + idna_codec = IDNA_DEFAULT + if style is None: + style = NameStyle(omit_final_dot=omit_final_dot, idna_codec=idna_codec) + return self.to_styled_text(style) + + def to_styled_text(self, style: NameStyle) -> str: + """Convert name to text format, applying the style. + + See the documentation for :py:class:`dns.name.NameStyle` for a description + of the style parameters. Returns a ``str``. """ - if len(self.labels) == 0: + name = self.choose_relativity(style.origin, style.relativize) + if len(name.labels) == 0: return "@" - if len(self.labels) == 1 and self.labels[0] == b"": + if len(name.labels) == 1 and name.labels[0] == b"": return "." - if omit_final_dot and self.is_absolute(): - l = self.labels[:-1] + if style.omit_final_dot and name.is_absolute(): + l = name.labels[:-1] else: - l = self.labels - if idna_codec is None: - idna_codec = IDNA_DEFAULT - return ".".join([idna_codec.decode(x) for x in l]) + l = name.labels + if style.idna_codec is None: + return ".".join(map(_escapify, l)) + else: + return ".".join([style.idna_codec.decode(x) for x in l]) def to_digestable(self, origin: "Name | None" = None) -> bytes: """Convert name to a format suitable for digesting in hashes. diff --git a/dns/node.py b/dns/node.py index 84312d88..9f3fbae5 100644 --- a/dns/node.py +++ b/dns/node.py @@ -17,6 +17,7 @@ """DNS nodes. A node is a set of rdatasets.""" +import dataclasses import enum import io from typing import Any @@ -44,6 +45,19 @@ def _matches_type_or_its_signature(rdtypes, rdtype, covers): return rdtype in rdtypes or (rdtype == dns.rdatatype.RRSIG and covers in rdtypes) +@dataclasses.dataclass(frozen=True) +class NodeStyle(dns.rdataset.RdatasetStyle): + """Node text styles. + + A ``NodeStyle`` is also a :py:class:`dns.name.NameStyle` and a + :py:class:`dns.rdata.RdataStyle`, and a :py:class:`dns.rdataset.RdatasetStyle`. + See those classes for a description of their options. + + There are currently no node-specific style options, but if that changes they + will be documented here. + """ + + @enum.unique class NodeKind(enum.Enum): """Rdatasets in nodes""" @@ -90,7 +104,9 @@ class Node: # the set of rdatasets, represented as a list. self.rdatasets = [] - def to_text(self, name: dns.name.Name, **kw: dict[str, Any]) -> str: + def to_text( + self, name: dns.name.Name, style: NodeStyle | None = None, **kw: Any + ) -> str: """Convert a node to text format. Each rdataset at the node is printed. Any keyword arguments @@ -99,15 +115,36 @@ class Node: *name*, a ``dns.name.Name``, the owner name of the rdatasets. + *style*, a :py:class:`dns.node.NodeStyle` or ``None`` (the default). If + specified, the style overrides the other parameters except *name*. + Returns a ``str``. + """ + if style is None: + style = NodeStyle.from_keywords(kw) + return self.to_styled_text(style, name) + def to_styled_text(self, style: NodeStyle, name: dns.name.Name) -> str: + """Convert a node to text format. + + Each rdataset at the node is printed. + + *name*, a ``dns.name.Name``, the owner name of the + rdatasets. + + See the documentation for :py:class:`dns.node.NodeStyle` for a description + of the style parameters. + + Returns a ``str``. """ s = io.StringIO() for rds in self.rdatasets: if len(rds) > 0: - s.write(rds.to_text(name, **kw)) # pyright: ignore[arg-type] + s.write(rds.to_styled_text(style, name)) s.write("\n") + if style.deduplicate_names and not style.first_name_is_duplicate: + style = style.replace(first_name_is_duplicate=True) return s.getvalue()[:-1] def __repr__(self): diff --git a/dns/rdata.py b/dns/rdata.py index 4225164a..9d2bae49 100644 --- a/dns/rdata.py +++ b/dns/rdata.py @@ -16,9 +16,9 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """DNS rdata.""" - import base64 import binascii +import dataclasses import inspect import io import ipaddress @@ -57,11 +57,55 @@ class NoRelativeRdataOrdering(dns.exception.DNSException): """ -def _wordbreak(data, chunksize=_chunksize, separator=b" "): +@dataclasses.dataclass(frozen=True) +class RdataStyle(dns.name.NameStyle): + """Rdata text styles + + An ``RdataStyle`` is also a :py:class:`dns.name.NameStyle`; see that class + for a description of its options. + + If *txt_is_utf8* is ``True``, then TXT-like records will be treated + as UTF-8 if they decode successfully, and the output string may contain any + Unicode codepoint. If ``False``, the default, then TXT-like records are + treated according to RFC 1035 rules. + + *base64_chunk_size*, an ``int`` with default 32, specifies the chunk size for + text representations that break base64 strings into chunks. + + *base64_chunk_separator*, a ``str`` with default ``" "``, specifies the + chunk separator for text representations that break base64 strings into chunks. + + *hex_chunk_size*, an ``int`` with default 128, specifies the chunk size for + text representations that break hex strings into chunks. + + *hex_chunk_separator*, a ``str`` with default ``" "``, specifies the + chunk separator for text representations that break hex strings into chunks. + + *truncate_crypto*, a ``bool``. The default is ``False``. If ``True``, then + output of crypto types (e.g. DNSKEY) is altered to be readable + by humans in a debugging context, but the crypto content will be removed. + A sample use would be a "dig" application where you wanted to see how many + DNSKEYs there were, and what key ids they had, without seeing the actual + public key data. Use of this option will lose information. + """ + + txt_is_utf8: bool = False + base64_chunk_size: int = 32 + base64_chunk_separator: str = " " + hex_chunk_size: int = 128 + hex_chunk_separator: str = " " + truncate_crypto: bool = False + + +def _wordbreak( + data: bytes, chunksize: int = _chunksize, separator: bytes | str = b" " +) -> str: """Break a binary string into chunks of chunksize characters separated by a space. """ + if isinstance(separator, str): + separator = separator.encode() if not chunksize: return data.decode() return separator.join( @@ -72,25 +116,60 @@ def _wordbreak(data, chunksize=_chunksize, separator=b" "): # pylint: disable=unused-argument -def _hexify(data, chunksize=_chunksize, separator=b" ", **kw): +def _hexify(data, chunksize=_chunksize, separator: bytes | str = " ", **kw): """Convert a binary string into its hex encoding, broken up into chunks of chunksize characters separated by a separator. """ + if isinstance(separator, bytes): + separator = separator.decode() + return _styled_hexify( + data, RdataStyle(hex_chunk_separator=separator, hex_chunk_size=chunksize) + ) + + +def _styled_hexify(data, style: RdataStyle, is_crypto: bool = False): + """Convert a binary string into its hex encoding, broken up into chunks + of characters separated by a separator. + """ - return _wordbreak(binascii.hexlify(data), chunksize, separator) + if style.truncate_crypto and is_crypto: + return "[omitted]" + return _wordbreak( + binascii.hexlify(data), + style.hex_chunk_size, + style.hex_chunk_separator, + ) -def _base64ify(data, chunksize=_chunksize, separator=b" ", **kw): +def _base64ify(data, chunksize=_chunksize, separator: bytes | str = " ", **kw): """Convert a binary string into its base64 encoding, broken up into chunks of chunksize characters separated by a separator. """ + if isinstance(separator, bytes): + separator = separator.decode() + return _styled_base64ify( + data, RdataStyle(base64_chunk_separator=separator, base64_chunk_size=chunksize) + ) - return _wordbreak(base64.b64encode(data), chunksize, separator) + +def _styled_base64ify(data, style: RdataStyle, is_crypto: bool = False): + """Convert a binary string into its base64 encoding, broken up into chunks + of characters separated by a separator. + """ + + if style.truncate_crypto and is_crypto: + return "[omitted]" + return _wordbreak( + base64.b64encode(data), + style.base64_chunk_size, + style.base64_chunk_separator, + ) # pylint: enable=unused-argument -__escaped = b'"\\' +_escaped = b'"\\' +_unicode_escaped = '"\\' def _escapify(qstring): @@ -103,7 +182,7 @@ def _escapify(qstring): text = "" for c in qstring: - if c in __escaped: + if c in _escaped: text += "\\" + chr(c) elif c >= 0x20 and c < 0x7F: text += chr(c) @@ -112,6 +191,20 @@ def _escapify(qstring): return text +def _escapify_unicode(qstring): + """Escape the characters in a Unicode quoted string which need it.""" + + text = "" + for c in qstring: + if c in _unicode_escaped: + text += "\\" + c + elif ord(c) >= 0x20: + text += c + else: + text += f"\\{ord(c):03d}" + return text + + def _truncate_bitmap(what): """Determine the index of greatest byte that isn't all zeros, and return the bitmap that contains all the bytes less than that index. @@ -133,6 +226,8 @@ class Rdata: __slots__ = ["rdclass", "rdtype", "rdcomment"] + _crypto_keep_first_n: int | None = None + def __init__( self, rdclass: dns.rdataclass.RdataClass, @@ -204,10 +299,29 @@ class Rdata: self, origin: dns.name.Name | None = None, relativize: bool = True, - **kw: dict[str, Any], + style: RdataStyle | None = None, + **kw: Any, ) -> str: """Convert an rdata to text format. + *style*, a :py:class:`dns.rdata.RdataStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. + + Returns a ``str``. + """ + if style is None: + kw = kw.copy() + kw["origin"] = origin + kw["relativize"] = relativize + style = RdataStyle.from_keywords(kw) + return self.to_styled_text(style) + + def to_styled_text(self, style: RdataStyle) -> str: + """Convert an rdata to styled text format. + + See the documentation for :py:class:`dns.rdata.RdataStyle` for a description + of the style parameters. + Returns a ``str``. """ @@ -629,13 +743,11 @@ class GenericRdata(Rdata): super().__init__(rdclass, rdtype) self.data = data - def to_text( + def to_styled_text( self, - origin: dns.name.Name | None = None, - relativize: bool = True, - **kw: dict[str, Any], + style: RdataStyle, ) -> str: - return rf"\# {len(self.data)} " + _hexify(self.data, **kw) # pyright: ignore + return rf"\# {len(self.data)} " + _styled_hexify(self.data, style) @classmethod def from_text( diff --git a/dns/rdataset.py b/dns/rdataset.py index dda5bb50..46274bdf 100644 --- a/dns/rdataset.py +++ b/dns/rdataset.py @@ -16,7 +16,7 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """DNS rdatasets (an rdataset is a set of rdatas of a given type and class)""" - +import dataclasses import io import random import struct @@ -46,6 +46,84 @@ class IncompatibleTypes(dns.exception.DNSException): """An attempt was made to add DNS RR data of an incompatible type.""" +@dataclasses.dataclass(frozen=True) +class RdatasetStyle(dns.rdata.RdataStyle): + """Rdataset text styles + + An ``RdatasetStyle`` is also a :py:class:`dns.name.NameStyle` and a + :py:class:`dns.rdata.RdataStyle`. See those classes + for a description of their options. + + *override_rdclass*, a ``dns.rdataclass.RdataClass`` or ``None``. + If not ``None``, use this class instead of the Rdataset's class. + + *want_comments*, a ``bool``. If ``True``, emit comments for rdata + which have them. The default is ``False``. + + *omit_rdclass*, a ``bool``. If ``True``, do not print the RdataClass. + The default is ``False``. + + *omit_ttl*, a ``bool``. If ``True``, do not print the TTL. + The default is ``False``. Use of this option may lose information. + + *want_generic*, a ``bool``. If ``True``, print RdataClass, RdataType, + and Rdatas in the generic format, a.k.a. the "unknown rdata format". + The default is ``False``. + + *deduplicate_names*, a ``bool``. If ``True``, print whitespace instead of the + owner name if the owner name of an RR is the same as the prior RR's owner name. + The default is ``False``. + + *first_name_is_duplicate*, a ``bool``. If ``True``, consider the first owner name + of the rdataset as a duplicate too, and emit whitespace for it as well. A sample + use is in emitting a Node of multiple rdatasets and the current rdataset is not + the first to be emitted. The default is ``False``. + + *default_ttl*, an ``int`` or ``None``. If ``None``, the default, there is no + default TTL. If an integer is specified, then any TTL matching that value will + be omitted. When emitting a zonefile, a setting other than ``None`` will cause + a ``$TTL`` directive to be emitted. + + *name_just*, an ``int``. The owner name field justification. Negative values + are left justified, and positive values are right justified. A value of zero, + the default, means that no justification is performed. + + *ttl_just*, an ``int``. The TTL field justification. Negative values + are left justified, and positive values are right justified. A value of zero, + the default, means that no justification is performed. + + *rdclass_just*, an ``int``. The RdataClass name field justification. Negative values + are left justified, and positive values are right justified. A value of zero, + the default, means that no justification is performed. + + *rdtype_just*, an ``int``. The RdataType field justification. Negative values + are left justified, and positive values are right justified. A value of zero, + the default, means that no justification is performed. + """ + + override_rdclass: dns.rdataclass.RdataClass | None = None + want_comments: bool = False + omit_rdclass: bool = False + omit_ttl: bool = False + want_generic: bool = False + deduplicate_names: bool = False + first_name_is_duplicate: bool = False + default_ttl: int | None = None + name_just: int = 0 + ttl_just: int = 0 + rdclass_just: int = 0 + rdtype_just: int = 0 + + +def justify(text: str, amount: int): + if amount == 0: + return text + if amount < 0: + return text.ljust(-1 * amount) + else: + return text.rjust(amount) + + class Rdataset(dns.set.Set): """A DNS rdataset.""" @@ -202,7 +280,8 @@ class Rdataset(dns.set.Set): relativize: bool = True, override_rdclass: dns.rdataclass.RdataClass | None = None, want_comments: bool = False, - **kw: dict[str, Any], + style: RdatasetStyle | None = None, + **kw: Any, ) -> str: """Convert the rdataset into DNS zone file format. @@ -223,47 +302,87 @@ class Rdataset(dns.set.Set): to *origin*. *override_rdclass*, a ``dns.rdataclass.RdataClass`` or ``None``. - If not ``None``, use this class instead of the Rdataset's class. + If not ``None``, when rendering, emit records as if they were of this class. *want_comments*, a ``bool``. If ``True``, emit comments for rdata which have them. The default is ``False``. + + *style*, a :py:class:`dns.rdataset.RdatasetStyle` or ``None`` (the default). If + specified, the style overrides the other parameters except for *name*. """ + if style is None: + kw = kw.copy() + kw["origin"] = origin + kw["relativize"] = relativize + kw["override_rdclass"] = override_rdclass + kw["want_comments"] = want_comments + style = RdatasetStyle.from_keywords(kw) + return self.to_styled_text(style, name) + + def to_styled_text( + self, style: RdatasetStyle, name: dns.name.Name | None = None + ) -> str: + """Convert the rdataset into styled text format. + See the documentation for :py:class:`dns.rdataset.RdatasetStyle` for a description + of the style parameters. + """ if name is not None: - name = name.choose_relativity(origin, relativize) - ntext = str(name) - pad = " " + if style.deduplicate_names and style.first_name_is_duplicate: + ntext = " " + else: + ntext = f"{name.to_styled_text(style)} " + ntext = justify(ntext, style.name_just) else: ntext = "" - pad = "" s = io.StringIO() - if override_rdclass is not None: - rdclass = override_rdclass + if style.override_rdclass is not None: + rdclass = style.override_rdclass else: rdclass = self.rdclass + if style.omit_rdclass: + rdclass_text = "" + elif style.want_generic: + rdclass_text = f"CLASS{rdclass} " + else: + rdclass_text = f"{dns.rdataclass.to_text(rdclass)} " + rdclass_text = justify(rdclass_text, style.rdclass_just) + if style.want_generic: + rdtype_text = f"TYPE{self.rdtype}" + else: + rdtype_text = f"{dns.rdatatype.to_text(self.rdtype)}" + rdtype_text = justify(rdtype_text, style.rdtype_just) if len(self) == 0: # # Empty rdatasets are used for the question section, and in # some dynamic updates, so we don't need to print out the TTL # (which is meaningless anyway). # - s.write( - f"{ntext}{pad}{dns.rdataclass.to_text(rdclass)} " - f"{dns.rdatatype.to_text(self.rdtype)}\n" - ) + s.write(f"{ntext}{rdclass_text}{rdtype_text}\n") else: + if style.omit_ttl or ( + style.default_ttl is not None and self.ttl == style.default_ttl + ): + ttl = "" + else: + ttl = f"{self.ttl} " + ttl = justify(ttl, style.ttl_just) for rd in self: extra = "" - if want_comments: + if style.want_comments: if rd.rdcomment: extra = f" ;{rd.rdcomment}" + if style.want_generic: + rdata_text = rd.to_generic().to_styled_text(style) + else: + rdata_text = rd.to_styled_text(style) s.write( - f"{ntext}{pad}{self.ttl} " - f"{dns.rdataclass.to_text(rdclass)} " - f"{dns.rdatatype.to_text(self.rdtype)} " - f"{rd.to_text(origin=origin, relativize=relativize, **kw)}" - f"{extra}\n" + f"{ntext}{ttl}{rdclass_text}{rdtype_text} {rdata_text}{extra}\n" ) + if style.deduplicate_names: + ntext = " " + ntext = justify(ntext, style.name_just) + # # We strip off the final \n for the caller's convenience in printing # diff --git a/dns/rdtypes/ANY/AFSDB.py b/dns/rdtypes/ANY/AFSDB.py index 06a3b970..778df986 100644 --- a/dns/rdtypes/ANY/AFSDB.py +++ b/dns/rdtypes/ANY/AFSDB.py @@ -16,6 +16,7 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import dns.immutable +import dns.name import dns.rdtypes.mxbase @@ -35,11 +36,11 @@ class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX): # good. @property - def subtype(self): + def subtype(self) -> int: "the AFSDB subtype" return self.preference @property - def hostname(self): + def hostname(self) -> dns.name.Name: "the AFSDB hostname" return self.exchange diff --git a/dns/rdtypes/ANY/AMTRELAY.py b/dns/rdtypes/ANY/AMTRELAY.py index b3096347..e64a60de 100644 --- a/dns/rdtypes/ANY/AMTRELAY.py +++ b/dns/rdtypes/ANY/AMTRELAY.py @@ -19,6 +19,7 @@ import struct import dns.exception import dns.immutable +import dns.name import dns.rdata import dns.rdtypes.util @@ -27,7 +28,7 @@ class Relay(dns.rdtypes.util.Gateway): name = "AMTRELAY relay" @property - def relay(self): + def relay(self) -> str | dns.name.Name | None: return self.gateway @@ -44,13 +45,13 @@ class AMTRELAY(dns.rdata.Rdata): ): super().__init__(rdclass, rdtype) relay = Relay(relay_type, relay) - self.precedence = self._as_uint8(precedence) - self.discovery_optional = self._as_bool(discovery_optional) - self.relay_type = relay.type - self.relay = relay.relay + self.precedence: int = self._as_uint8(precedence) + self.discovery_optional: bool = self._as_bool(discovery_optional) + self.relay_type: int = relay.type + self.relay: str | dns.name.Name | None = relay.relay - def to_text(self, origin=None, relativize=True, **kw): - relay = Relay(self.relay_type, self.relay).to_text(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + relay = Relay(self.relay_type, self.relay).to_styled_text(style) return ( f"{self.precedence} {self.discovery_optional:d} {self.relay_type} {relay}" ) @@ -58,7 +59,7 @@ class AMTRELAY(dns.rdata.Rdata): @classmethod def from_text( cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + ) -> "AMTRELAY": precedence = tok.get_uint8() discovery_optional = tok.get_uint8() if discovery_optional > 1: @@ -79,7 +80,7 @@ class AMTRELAY(dns.rdata.Rdata): Relay(self.relay_type, self.relay).to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None) -> "AMTRELAY": precedence, relay_type = parser.get_struct("!BB") discovery_optional = bool(relay_type >> 7) relay_type &= 0x7F diff --git a/dns/rdtypes/ANY/CAA.py b/dns/rdtypes/ANY/CAA.py index 8c62e626..c44911c1 100644 --- a/dns/rdtypes/ANY/CAA.py +++ b/dns/rdtypes/ANY/CAA.py @@ -33,19 +33,19 @@ class CAA(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, flags, tag, value): super().__init__(rdclass, rdtype) - self.flags = self._as_uint8(flags) - self.tag = self._as_bytes(tag, True, 255) + self.flags: int = self._as_uint8(flags) + self.tag: bytes = self._as_bytes(tag, True, 255) if not tag.isalnum(): raise ValueError("tag is not alphanumeric") - self.value = self._as_bytes(value) + self.value: bytes = self._as_bytes(value) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f'{self.flags} {dns.rdata._escapify(self.tag)} "{dns.rdata._escapify(self.value)}"' @classmethod def from_text( cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + ) -> "CAA": flags = tok.get_uint8() tag = tok.get_string().encode() value = tok.get_string().encode() @@ -60,7 +60,7 @@ class CAA(dns.rdata.Rdata): file.write(self.value) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None) -> "CAA": flags = parser.get_uint8() tag = parser.get_counted_bytes() value = parser.get_remaining() diff --git a/dns/rdtypes/ANY/CERT.py b/dns/rdtypes/ANY/CERT.py index c34febcf..1848af7e 100644 --- a/dns/rdtypes/ANY/CERT.py +++ b/dns/rdtypes/ANY/CERT.py @@ -51,14 +51,14 @@ _ctype_by_name = { } -def _ctype_from_text(what): +def _ctype_from_text(what: str) -> int: v = _ctype_by_name.get(what) if v is not None: return v return int(what) -def _ctype_to_text(what): +def _ctype_to_text(what: int) -> str: v = _ctype_by_value.get(what) if v is not None: return v @@ -77,15 +77,15 @@ class CERT(dns.rdata.Rdata): self, rdclass, rdtype, certificate_type, key_tag, algorithm, certificate ): super().__init__(rdclass, rdtype) - self.certificate_type = self._as_uint16(certificate_type) - self.key_tag = self._as_uint16(key_tag) - self.algorithm = self._as_uint8(algorithm) - self.certificate = self._as_bytes(certificate) + self.certificate_type: int = self._as_uint16(certificate_type) + self.key_tag: int = self._as_uint16(key_tag) + self.algorithm: int = self._as_uint8(algorithm) + self.certificate: bytes = self._as_bytes(certificate) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: certificate_type = _ctype_to_text(self.certificate_type) algorithm = dns.dnssectypes.Algorithm.to_text(self.algorithm) - certificate = dns.rdata._base64ify(self.certificate, **kw) # pyright: ignore + certificate = dns.rdata._styled_base64ify(self.certificate, style) return f"{certificate_type} {self.key_tag} {algorithm} {certificate}" @classmethod diff --git a/dns/rdtypes/ANY/CSYNC.py b/dns/rdtypes/ANY/CSYNC.py index 36d80759..e9132a5e 100644 --- a/dns/rdtypes/ANY/CSYNC.py +++ b/dns/rdtypes/ANY/CSYNC.py @@ -38,13 +38,13 @@ class CSYNC(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, serial, flags, windows): super().__init__(rdclass, rdtype) - self.serial = self._as_uint32(serial) - self.flags = self._as_uint16(flags) + self.serial: int = self._as_uint32(serial) + self.flags: int = self._as_uint16(flags) if not isinstance(windows, Bitmap): windows = Bitmap(windows) self.windows = tuple(windows.windows) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: text = Bitmap(self.windows).to_text() return f"{self.serial} {self.flags}{text}" diff --git a/dns/rdtypes/ANY/DSYNC.py b/dns/rdtypes/ANY/DSYNC.py index 41ce0888..c77c6247 100644 --- a/dns/rdtypes/ANY/DSYNC.py +++ b/dns/rdtypes/ANY/DSYNC.py @@ -5,6 +5,7 @@ import struct import dns.enum import dns.exception import dns.immutable +import dns.name import dns.rdata import dns.rdatatype import dns.rdtypes.util @@ -38,13 +39,13 @@ class DSYNC(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, rrtype, scheme, port, target): super().__init__(rdclass, rdtype) - self.rrtype = self._as_rdatatype(rrtype) - self.scheme = Scheme.make(scheme) - self.port = self._as_uint16(port) - self.target = self._as_name(target) + self.rrtype: dns.rdatatype.RdataType = self._as_rdatatype(rrtype) + self.scheme: Scheme = Scheme.make(scheme) + self.port: int = self._as_uint16(port) + self.target: dns.name.Name = self._as_name(target) - def to_text(self, origin=None, relativize=True, **kw): - target = self.target.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + target = self.target.to_styled_text(style) return ( f"{dns.rdatatype.to_text(self.rrtype)} {Scheme.to_text(self.scheme)} " f"{self.port} {target}" diff --git a/dns/rdtypes/ANY/GPOS.py b/dns/rdtypes/ANY/GPOS.py index f2248ab6..8a6caea3 100644 --- a/dns/rdtypes/ANY/GPOS.py +++ b/dns/rdtypes/ANY/GPOS.py @@ -64,9 +64,9 @@ class GPOS(dns.rdata.Rdata): _validate_float_string(latitude) _validate_float_string(longitude) _validate_float_string(altitude) - self.latitude = latitude - self.longitude = longitude - self.altitude = altitude + self.latitude: bytes = latitude + self.longitude: bytes = longitude + self.altitude: bytes = altitude flat = self.float_latitude if flat < -90.0 or flat > 90.0: raise dns.exception.FormError("bad latitude") @@ -74,7 +74,7 @@ class GPOS(dns.rdata.Rdata): if flong < -180.0 or flong > 180.0: raise dns.exception.FormError("bad longitude") - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return ( f"{self.latitude.decode()} {self.longitude.decode()} " f"{self.altitude.decode()}" diff --git a/dns/rdtypes/ANY/HINFO.py b/dns/rdtypes/ANY/HINFO.py index 06ad3487..ad215480 100644 --- a/dns/rdtypes/ANY/HINFO.py +++ b/dns/rdtypes/ANY/HINFO.py @@ -33,10 +33,10 @@ class HINFO(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, cpu, os): super().__init__(rdclass, rdtype) - self.cpu = self._as_bytes(cpu, True, 255) - self.os = self._as_bytes(os, True, 255) + self.cpu: bytes = self._as_bytes(cpu, True, 255) + self.os: bytes = self._as_bytes(os, True, 255) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f'"{dns.rdata._escapify(self.cpu)}" "{dns.rdata._escapify(self.os)}"' @classmethod diff --git a/dns/rdtypes/ANY/HIP.py b/dns/rdtypes/ANY/HIP.py index d31d6334..d49847ed 100644 --- a/dns/rdtypes/ANY/HIP.py +++ b/dns/rdtypes/ANY/HIP.py @@ -21,6 +21,7 @@ import struct import dns.exception import dns.immutable +import dns.name import dns.rdata import dns.rdatatype @@ -35,20 +36,23 @@ class HIP(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, hit, algorithm, key, servers): super().__init__(rdclass, rdtype) - self.hit = self._as_bytes(hit, True, 255) - self.algorithm = self._as_uint8(algorithm) - self.key = self._as_bytes(key, True) - self.servers = self._as_tuple(servers, self._as_name) + self.hit: bytes = self._as_bytes(hit, True, 255) + self.algorithm: int = self._as_uint8(algorithm) + self.key: bytes = self._as_bytes(key, True) + self.servers: tuple[dns.name.Name] = self._as_tuple(servers, self._as_name) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + # "hit" is not styled. hit = binascii.hexlify(self.hit).decode() - key = base64.b64encode(self.key).replace(b"\n", b"").decode() + # Fixed style + style = style.replace(base64_chunk_size=0) + key = dns.rdata._styled_base64ify(self.key, style, True) text = "" servers = [] for server in self.servers: - servers.append(server.choose_relativity(origin, relativize)) + servers.append(server.to_styled_text(style)) if len(servers) > 0: - text += " " + " ".join(x.to_unicode() for x in servers) + text += " " + " ".join(servers) return f"{self.algorithm} {hit} {key}{text}" @classmethod diff --git a/dns/rdtypes/ANY/ISDN.py b/dns/rdtypes/ANY/ISDN.py index 6428a0a8..bb68d88e 100644 --- a/dns/rdtypes/ANY/ISDN.py +++ b/dns/rdtypes/ANY/ISDN.py @@ -33,10 +33,10 @@ class ISDN(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, address, subaddress): super().__init__(rdclass, rdtype) - self.address = self._as_bytes(address, True, 255) - self.subaddress = self._as_bytes(subaddress, True, 255) + self.address: bytes = self._as_bytes(address, True, 255) + self.subaddress: bytes = self._as_bytes(subaddress, True, 255) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: if self.subaddress: return ( f'"{dns.rdata._escapify(self.address)}" ' diff --git a/dns/rdtypes/ANY/L32.py b/dns/rdtypes/ANY/L32.py index f51e5c79..365b9936 100644 --- a/dns/rdtypes/ANY/L32.py +++ b/dns/rdtypes/ANY/L32.py @@ -17,10 +17,10 @@ class L32(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, preference, locator32): super().__init__(rdclass, rdtype) - self.preference = self._as_uint16(preference) - self.locator32 = self._as_ipv4_address(locator32) + self.preference: int = self._as_uint16(preference) + self.locator32: str = self._as_ipv4_address(locator32) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f"{self.preference} {self.locator32}" @classmethod diff --git a/dns/rdtypes/ANY/L64.py b/dns/rdtypes/ANY/L64.py index a47da19e..6d5fcf24 100644 --- a/dns/rdtypes/ANY/L64.py +++ b/dns/rdtypes/ANY/L64.py @@ -17,16 +17,17 @@ class L64(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, preference, locator64): super().__init__(rdclass, rdtype) - self.preference = self._as_uint16(preference) + self.preference: int = self._as_uint16(preference) if isinstance(locator64, bytes): if len(locator64) != 8: raise ValueError("invalid locator64") - self.locator64 = dns.rdata._hexify(locator64, 4, b":") + # Not styled + self.locator64: str = dns.rdata._hexify(locator64, 4, ":") else: dns.rdtypes.util.parse_formatted_hex(locator64, 4, 4, ":") - self.locator64 = locator64 + self.locator64: str = locator64 - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f"{self.preference} {self.locator64}" @classmethod diff --git a/dns/rdtypes/ANY/LOC.py b/dns/rdtypes/ANY/LOC.py index 227a257f..02b0a2c3 100644 --- a/dns/rdtypes/ANY/LOC.py +++ b/dns/rdtypes/ANY/LOC.py @@ -155,7 +155,7 @@ class LOC(dns.rdata.Rdata): self.horizontal_precision = float(hprec) self.vertical_precision = float(vprec) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle): if self.latitude[4] > 0: lat_hemisphere = "N" else: diff --git a/dns/rdtypes/ANY/LP.py b/dns/rdtypes/ANY/LP.py index 379c8627..ac1d98ed 100644 --- a/dns/rdtypes/ANY/LP.py +++ b/dns/rdtypes/ANY/LP.py @@ -3,6 +3,7 @@ import struct import dns.immutable +import dns.name import dns.rdata @@ -16,12 +17,11 @@ class LP(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, preference, fqdn): super().__init__(rdclass, rdtype) - self.preference = self._as_uint16(preference) - self.fqdn = self._as_name(fqdn) + self.preference: int = self._as_uint16(preference) + self.fqdn: dns.name.Name = self._as_name(fqdn) - def to_text(self, origin=None, relativize=True, **kw): - fqdn = self.fqdn.choose_relativity(origin, relativize) - return f"{self.preference} {fqdn}" + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + return f"{self.preference} {self.fqdn.to_styled_text(style)}" @classmethod def from_text( diff --git a/dns/rdtypes/ANY/NID.py b/dns/rdtypes/ANY/NID.py index fa0dad5c..076d7a09 100644 --- a/dns/rdtypes/ANY/NID.py +++ b/dns/rdtypes/ANY/NID.py @@ -21,12 +21,12 @@ class NID(dns.rdata.Rdata): if isinstance(nodeid, bytes): if len(nodeid) != 8: raise ValueError("invalid nodeid") - self.nodeid = dns.rdata._hexify(nodeid, 4, b":") + self.nodeid = dns.rdata._hexify(nodeid, 4, ":") else: dns.rdtypes.util.parse_formatted_hex(nodeid, 4, 4, ":") self.nodeid = nodeid - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f"{self.preference} {self.nodeid}" @classmethod diff --git a/dns/rdtypes/ANY/NSEC.py b/dns/rdtypes/ANY/NSEC.py index 3c78b722..949c232a 100644 --- a/dns/rdtypes/ANY/NSEC.py +++ b/dns/rdtypes/ANY/NSEC.py @@ -41,10 +41,9 @@ class NSEC(dns.rdata.Rdata): windows = Bitmap(windows) self.windows = tuple(windows.windows) - def to_text(self, origin=None, relativize=True, **kw): - next = self.next.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: text = Bitmap(self.windows).to_text() - return f"{next}{text}" + return f"{self.next.to_styled_text(style)}{text}" @classmethod def from_text( diff --git a/dns/rdtypes/ANY/NSEC3.py b/dns/rdtypes/ANY/NSEC3.py index cd1d9d98..7aa59839 100644 --- a/dns/rdtypes/ANY/NSEC3.py +++ b/dns/rdtypes/ANY/NSEC3.py @@ -69,11 +69,12 @@ class NSEC3(dns.rdata.Rdata): next = next.rstrip("=") return next - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: next = self._next_text() if self.salt == b"": salt = "-" else: + # Not styled salt = binascii.hexlify(self.salt).decode() text = Bitmap(self.windows).to_text() return f"{self.algorithm} {self.flags} {self.iterations} {salt} {next}{text}" diff --git a/dns/rdtypes/ANY/NSEC3PARAM.py b/dns/rdtypes/ANY/NSEC3PARAM.py index 3ab90ee2..1019881d 100644 --- a/dns/rdtypes/ANY/NSEC3PARAM.py +++ b/dns/rdtypes/ANY/NSEC3PARAM.py @@ -36,10 +36,11 @@ class NSEC3PARAM(dns.rdata.Rdata): self.iterations = self._as_uint16(iterations) self.salt = self._as_bytes(salt, True, 255) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: if self.salt == b"": salt = "-" else: + # Not styled salt = binascii.hexlify(self.salt).decode() return f"{self.algorithm} {self.flags} {self.iterations} {salt}" diff --git a/dns/rdtypes/ANY/OPENPGPKEY.py b/dns/rdtypes/ANY/OPENPGPKEY.py index ac1841cc..37514f61 100644 --- a/dns/rdtypes/ANY/OPENPGPKEY.py +++ b/dns/rdtypes/ANY/OPENPGPKEY.py @@ -33,8 +33,10 @@ class OPENPGPKEY(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.key = self._as_bytes(key) - def to_text(self, origin=None, relativize=True, **kw): - return dns.rdata._base64ify(self.key, chunksize=None, **kw) # pyright: ignore + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + # Fixed style + style = style.replace(base64_chunk_size=0) + return dns.rdata._styled_base64ify(self.key, style, True) @classmethod def from_text( diff --git a/dns/rdtypes/ANY/OPT.py b/dns/rdtypes/ANY/OPT.py index 94583ced..57b754ee 100644 --- a/dns/rdtypes/ANY/OPT.py +++ b/dns/rdtypes/ANY/OPT.py @@ -58,7 +58,7 @@ class OPT(dns.rdata.Rdata): file.write(struct.pack("!HH", opt.otype, len(owire))) file.write(owire) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return " ".join(opt.to_text() for opt in self.options) @classmethod diff --git a/dns/rdtypes/ANY/RP.py b/dns/rdtypes/ANY/RP.py index a66cfc50..7cd26daf 100644 --- a/dns/rdtypes/ANY/RP.py +++ b/dns/rdtypes/ANY/RP.py @@ -34,10 +34,10 @@ class RP(dns.rdata.Rdata): self.mbox = self._as_name(mbox) self.txt = self._as_name(txt) - def to_text(self, origin=None, relativize=True, **kw): - mbox = self.mbox.choose_relativity(origin, relativize) - txt = self.txt.choose_relativity(origin, relativize) - return f"{str(mbox)} {str(txt)}" + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + mbox = self.mbox.to_styled_text(style) + txt = self.txt.to_styled_text(style) + return f"{mbox} {txt}" @classmethod def from_text( diff --git a/dns/rdtypes/ANY/SOA.py b/dns/rdtypes/ANY/SOA.py index 3c7cd8c9..4cabea8e 100644 --- a/dns/rdtypes/ANY/SOA.py +++ b/dns/rdtypes/ANY/SOA.py @@ -35,23 +35,23 @@ class SOA(dns.rdata.Rdata): self, rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum ): super().__init__(rdclass, rdtype) - self.mname = self._as_name(mname) - self.rname = self._as_name(rname) - self.serial = self._as_uint32(serial) - self.refresh = self._as_ttl(refresh) - self.retry = self._as_ttl(retry) - self.expire = self._as_ttl(expire) - self.minimum = self._as_ttl(minimum) + self.mname: dns.name.Name = self._as_name(mname) + self.rname: dns.name.Name = self._as_name(rname) + self.serial: int = self._as_uint32(serial) + self.refresh: int = self._as_ttl(refresh) + self.retry: int = self._as_ttl(retry) + self.expire: int = self._as_ttl(expire) + self.minimum: int = self._as_ttl(minimum) - def to_text(self, origin=None, relativize=True, **kw): - mname = self.mname.choose_relativity(origin, relativize) - rname = self.rname.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + mname = self.mname.to_styled_text(style) + rname = self.rname.to_styled_text(style) return f"{mname} {rname} {self.serial} {self.refresh} {self.retry} {self.expire} {self.minimum}" @classmethod def from_text( cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + ) -> "SOA": mname = tok.get_name(origin, relativize, relativize_to) rname = tok.get_name(origin, relativize, relativize_to) serial = tok.get_uint32() @@ -72,7 +72,7 @@ class SOA(dns.rdata.Rdata): file.write(five_ints) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None) -> "SOA": mname = parser.get_name(origin) rname = parser.get_name(origin) return cls(rdclass, rdtype, mname, rname, *parser.get_struct("!IIIII")) diff --git a/dns/rdtypes/ANY/SSHFP.py b/dns/rdtypes/ANY/SSHFP.py index 3f08f3a5..627122bc 100644 --- a/dns/rdtypes/ANY/SSHFP.py +++ b/dns/rdtypes/ANY/SSHFP.py @@ -37,12 +37,8 @@ class SSHFP(dns.rdata.Rdata): self.fp_type = self._as_uint8(fp_type) self.fingerprint = self._as_bytes(fingerprint, True) - def to_text(self, origin=None, relativize=True, **kw): - kw = kw.copy() - chunksize = kw.pop("chunksize", 128) - fingerprint = dns.rdata._hexify( - self.fingerprint, chunksize=chunksize, **kw # pyright: ignore - ) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + fingerprint = dns.rdata._styled_hexify(self.fingerprint, style) return f"{self.algorithm} {self.fp_type} {fingerprint}" @classmethod diff --git a/dns/rdtypes/ANY/TKEY.py b/dns/rdtypes/ANY/TKEY.py index f9189b16..dd476a71 100644 --- a/dns/rdtypes/ANY/TKEY.py +++ b/dns/rdtypes/ANY/TKEY.py @@ -58,13 +58,15 @@ class TKEY(dns.rdata.Rdata): self.key = self._as_bytes(key) self.other = self._as_bytes(other) - def to_text(self, origin=None, relativize=True, **kw): - _algorithm = self.algorithm.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + algorithm = self.algorithm.to_styled_text(style) + # Not styled key = dns.rdata._base64ify(self.key, 0) other = "" if len(self.other) > 0: + # Not styled other = " " + dns.rdata._base64ify(self.other, 0) - return f"{_algorithm} {self.inception} {self.expiration} {self.mode} {self.error} {key}{other}" + return f"{algorithm} {self.inception} {self.expiration} {self.mode} {self.error} {key}{other}" @classmethod def from_text( diff --git a/dns/rdtypes/ANY/TSIG.py b/dns/rdtypes/ANY/TSIG.py index c375e0a7..a848e19d 100644 --- a/dns/rdtypes/ANY/TSIG.py +++ b/dns/rdtypes/ANY/TSIG.py @@ -80,8 +80,8 @@ class TSIG(dns.rdata.Rdata): self.error = dns.rcode.Rcode.make(error) self.other = self._as_bytes(other) - def to_text(self, origin=None, relativize=True, **kw): - algorithm = self.algorithm.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + algorithm = self.algorithm.to_styled_text(style) error = dns.rcode.to_text(self.error, True) text = ( f"{algorithm} {self.time_signed} {self.fudge} " diff --git a/dns/rdtypes/ANY/URI.py b/dns/rdtypes/ANY/URI.py index 1943e58d..24d5d08a 100644 --- a/dns/rdtypes/ANY/URI.py +++ b/dns/rdtypes/ANY/URI.py @@ -41,7 +41,7 @@ class URI(dns.rdata.Rdata): if len(self.target) == 0: raise dns.exception.SyntaxError("URI target cannot be empty") - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f'{self.priority} {self.weight} "{self.target.decode()}"' @classmethod diff --git a/dns/rdtypes/ANY/X25.py b/dns/rdtypes/ANY/X25.py index 2436ddb6..bd7e431d 100644 --- a/dns/rdtypes/ANY/X25.py +++ b/dns/rdtypes/ANY/X25.py @@ -35,7 +35,7 @@ class X25(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.address = self._as_bytes(address, True, 255) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return f'"{dns.rdata._escapify(self.address)}"' @classmethod diff --git a/dns/rdtypes/ANY/ZONEMD.py b/dns/rdtypes/ANY/ZONEMD.py index acef4f27..ffc034d6 100644 --- a/dns/rdtypes/ANY/ZONEMD.py +++ b/dns/rdtypes/ANY/ZONEMD.py @@ -33,12 +33,8 @@ class ZONEMD(dns.rdata.Rdata): if hasher and hasher().digest_size != len(self.digest): raise ValueError("digest length inconsistent with hash algorithm") - def to_text(self, origin=None, relativize=True, **kw): - kw = kw.copy() - chunksize = kw.pop("chunksize", 128) - digest = dns.rdata._hexify( - self.digest, chunksize=chunksize, **kw # pyright: ignore - ) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + digest = dns.rdata._styled_hexify(self.digest, style, True) return f"{self.serial} {self.scheme} {self.hash_algorithm} {digest}" @classmethod diff --git a/dns/rdtypes/CH/A.py b/dns/rdtypes/CH/A.py index e3e07521..11207980 100644 --- a/dns/rdtypes/CH/A.py +++ b/dns/rdtypes/CH/A.py @@ -36,8 +36,8 @@ class A(dns.rdata.Rdata): self.domain = self._as_name(domain) self.address = self._as_uint16(address) - def to_text(self, origin=None, relativize=True, **kw): - domain = self.domain.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + domain = self.domain.to_styled_text(style) return f"{domain} {self.address:o}" @classmethod diff --git a/dns/rdtypes/IN/A.py b/dns/rdtypes/IN/A.py index e09d6110..b0c5308a 100644 --- a/dns/rdtypes/IN/A.py +++ b/dns/rdtypes/IN/A.py @@ -32,7 +32,7 @@ class A(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.address = self._as_ipv4_address(address) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return self.address @classmethod diff --git a/dns/rdtypes/IN/AAAA.py b/dns/rdtypes/IN/AAAA.py index 0cd139e7..7fe63d94 100644 --- a/dns/rdtypes/IN/AAAA.py +++ b/dns/rdtypes/IN/AAAA.py @@ -32,7 +32,7 @@ class AAAA(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.address = self._as_ipv6_address(address) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return self.address @classmethod diff --git a/dns/rdtypes/IN/APL.py b/dns/rdtypes/IN/APL.py index daa1e5f9..fa93ab0d 100644 --- a/dns/rdtypes/IN/APL.py +++ b/dns/rdtypes/IN/APL.py @@ -92,7 +92,7 @@ class APL(dns.rdata.Rdata): raise ValueError("item not an APLItem") self.items = tuple(items) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: return " ".join(map(str, self.items)) @classmethod diff --git a/dns/rdtypes/IN/DHCID.py b/dns/rdtypes/IN/DHCID.py index 8de8cdf1..923b6091 100644 --- a/dns/rdtypes/IN/DHCID.py +++ b/dns/rdtypes/IN/DHCID.py @@ -34,8 +34,8 @@ class DHCID(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.data = self._as_bytes(data) - def to_text(self, origin=None, relativize=True, **kw): - return dns.rdata._base64ify(self.data, **kw) # pyright: ignore + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + return dns.rdata._styled_base64ify(self.data, style) @classmethod def from_text( diff --git a/dns/rdtypes/IN/IPSECKEY.py b/dns/rdtypes/IN/IPSECKEY.py index aef93ae1..a2c90932 100644 --- a/dns/rdtypes/IN/IPSECKEY.py +++ b/dns/rdtypes/IN/IPSECKEY.py @@ -47,9 +47,9 @@ class IPSECKEY(dns.rdata.Rdata): self.gateway = gateway.gateway self.key = self._as_bytes(key) - def to_text(self, origin=None, relativize=True, **kw): - gateway = Gateway(self.gateway_type, self.gateway).to_text(origin, relativize) - key = dns.rdata._base64ify(self.key, **kw) # pyright: ignore + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + gateway = Gateway(self.gateway_type, self.gateway).to_styled_text(style) + key = dns.rdata._styled_base64ify(self.key, style, True) return f"{self.precedence} {self.gateway_type} {self.algorithm} {gateway} {key}" @classmethod diff --git a/dns/rdtypes/IN/NAPTR.py b/dns/rdtypes/IN/NAPTR.py index 866a016e..cd7d3e9a 100644 --- a/dns/rdtypes/IN/NAPTR.py +++ b/dns/rdtypes/IN/NAPTR.py @@ -50,8 +50,8 @@ class NAPTR(dns.rdata.Rdata): self.preference = self._as_uint16(preference) self.replacement = self._as_name(replacement) - def to_text(self, origin=None, relativize=True, **kw): - replacement = self.replacement.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + replacement = self.replacement.to_styled_text(style) return ( f"{self.order} {self.preference} " f'"{dns.rdata._escapify(self.flags)}" ' diff --git a/dns/rdtypes/IN/NSAP.py b/dns/rdtypes/IN/NSAP.py index d55edb73..18013f09 100644 --- a/dns/rdtypes/IN/NSAP.py +++ b/dns/rdtypes/IN/NSAP.py @@ -35,7 +35,8 @@ class NSAP(dns.rdata.Rdata): super().__init__(rdclass, rdtype) self.address = self._as_bytes(address) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + # Not styled return f"0x{binascii.hexlify(self.address).decode()}" @classmethod diff --git a/dns/rdtypes/IN/PX.py b/dns/rdtypes/IN/PX.py index 20143bf6..7fe755f4 100644 --- a/dns/rdtypes/IN/PX.py +++ b/dns/rdtypes/IN/PX.py @@ -38,9 +38,9 @@ class PX(dns.rdata.Rdata): self.map822 = self._as_name(map822) self.mapx400 = self._as_name(mapx400) - def to_text(self, origin=None, relativize=True, **kw): - map822 = self.map822.choose_relativity(origin, relativize) - mapx400 = self.mapx400.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + map822 = self.map822.to_styled_text(style) + mapx400 = self.mapx400.to_styled_text(style) return f"{self.preference} {map822} {mapx400}" @classmethod diff --git a/dns/rdtypes/IN/SRV.py b/dns/rdtypes/IN/SRV.py index 50f69765..743a6753 100644 --- a/dns/rdtypes/IN/SRV.py +++ b/dns/rdtypes/IN/SRV.py @@ -39,8 +39,8 @@ class SRV(dns.rdata.Rdata): self.port = self._as_uint16(port) self.target = self._as_name(target) - def to_text(self, origin=None, relativize=True, **kw): - target = self.target.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + target = self.target.to_styled_text(style) return f"{self.priority} {self.weight} {self.port} {target}" @classmethod diff --git a/dns/rdtypes/IN/WKS.py b/dns/rdtypes/IN/WKS.py index cc6c3733..634386fc 100644 --- a/dns/rdtypes/IN/WKS.py +++ b/dns/rdtypes/IN/WKS.py @@ -45,7 +45,7 @@ class WKS(dns.rdata.Rdata): self.protocol = self._as_uint8(protocol) self.bitmap = self._as_bytes(bitmap) - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: bits = [] for i, byte in enumerate(self.bitmap): for j in range(0, 8): diff --git a/dns/rdtypes/dnskeybase.py b/dns/rdtypes/dnskeybase.py index fb49f922..243415b0 100644 --- a/dns/rdtypes/dnskeybase.py +++ b/dns/rdtypes/dnskeybase.py @@ -18,6 +18,7 @@ import base64 import enum import struct +from typing import TypeVar import dns.dnssectypes import dns.exception @@ -34,6 +35,9 @@ class Flag(enum.IntFlag): ZONE = 0x0100 +T = TypeVar("T", bound="DNSKEYBase") + + @dns.immutable.immutable class DNSKEYBase(dns.rdata.Rdata): """Base class for rdata that is like a DNSKEY record""" @@ -42,19 +46,30 @@ class DNSKEYBase(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key): super().__init__(rdclass, rdtype) - self.flags = Flag(self._as_uint16(flags)) - self.protocol = self._as_uint8(protocol) - self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) - self.key = self._as_bytes(key) - - def to_text(self, origin=None, relativize=True, **kw): - key = dns.rdata._base64ify(self.key, **kw) # pyright: ignore + self.flags: int = Flag(self._as_uint16(flags)) + self.protocol: int = self._as_uint8(protocol) + self.algorithm: dns.dnssectypes.Algorithm = dns.dnssectypes.Algorithm.make( + algorithm + ) + self.key: bytes = self._as_bytes(key) + + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + if style.truncate_crypto: + key = f"[key id = {self.key_id()}]" + else: + key = dns.rdata._styled_base64ify(self.key, style) return f"{self.flags} {self.protocol} {self.algorithm} {key}" @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: flags = tok.get_uint16() protocol = tok.get_uint8() algorithm = tok.get_string() @@ -68,11 +83,33 @@ class DNSKEYBase(dns.rdata.Rdata): file.write(self.key) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: header = parser.get_struct("!HBB") key = parser.get_remaining() return cls(rdclass, rdtype, header[0], header[1], header[2], key) + def key_id(self) -> int: + """Return the key id (a 16-bit number) for the specified key. + + *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` + + Returns an ``int`` between 0 and 65535 + """ + + wire = self.to_wire() + assert wire is not None # for mypy + if self.algorithm == dns.dnssectypes.Algorithm.RSAMD5: + return (wire[-3] << 8) + wire[-2] + else: + total = 0 + for i in range(len(wire) // 2): + total += (wire[2 * i] << 8) + wire[2 * i + 1] + if len(wire) % 2 != 0: + total += wire[len(wire) - 1] << 8 + total += (total >> 16) & 0xFFFF + return total & 0xFFFF + return total & 0xFFFF + ### BEGIN generated Flag constants diff --git a/dns/rdtypes/dsbase.py b/dns/rdtypes/dsbase.py index 8e05c2a7..8fab0b6c 100644 --- a/dns/rdtypes/dsbase.py +++ b/dns/rdtypes/dsbase.py @@ -17,12 +17,15 @@ import binascii import struct +from typing import TypeVar import dns.dnssectypes import dns.immutable import dns.rdata import dns.rdatatype +T = TypeVar("T", bound="DSBase") + @dns.immutable.immutable class DSBase(dns.rdata.Rdata): @@ -52,18 +55,20 @@ class DSBase(dns.rdata.Rdata): if self.digest_type == 0: # reserved, RFC 3658 Sec. 2.4 raise ValueError("digest type 0 is reserved") - def to_text(self, origin=None, relativize=True, **kw): - kw = kw.copy() - chunksize = kw.pop("chunksize", 128) - digest = dns.rdata._hexify( - self.digest, chunksize=chunksize, **kw # pyright: ignore - ) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + digest = dns.rdata._styled_hexify(self.digest, style, True) return f"{self.key_tag} {self.algorithm} {self.digest_type} {digest}" @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: key_tag = tok.get_uint16() algorithm = tok.get_string() digest_type = tok.get_uint8() @@ -77,7 +82,7 @@ class DSBase(dns.rdata.Rdata): file.write(self.digest) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: header = parser.get_struct("!HBB") digest = parser.get_remaining() return cls(rdclass, rdtype, header[0], header[1], header[2], digest) diff --git a/dns/rdtypes/euibase.py b/dns/rdtypes/euibase.py index 4eb82eb5..e7f9a92e 100644 --- a/dns/rdtypes/euibase.py +++ b/dns/rdtypes/euibase.py @@ -15,11 +15,14 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import binascii +from typing import TypeVar import dns.exception import dns.immutable import dns.rdata +T = TypeVar("T", bound="EUIBase") + @dns.immutable.immutable class EUIBase(dns.rdata.Rdata): @@ -42,13 +45,20 @@ class EUIBase(dns.rdata.Rdata): f"EUI{self.byte_len * 8} rdata has to have {self.byte_len} bytes" ) - def to_text(self, origin=None, relativize=True, **kw): - return dns.rdata._hexify(self.eui, chunksize=2, separator=b"-", **kw) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + # Not using style as the style of EUIs is fixed + return dns.rdata._hexify(self.eui, chunksize=2, separator="-") @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: text = tok.get_string() if len(text) != cls.text_len: raise dns.exception.SyntaxError( @@ -68,6 +78,6 @@ class EUIBase(dns.rdata.Rdata): file.write(self.eui) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: eui = parser.get_bytes(cls.byte_len) return cls(rdclass, rdtype, eui) diff --git a/dns/rdtypes/mxbase.py b/dns/rdtypes/mxbase.py index 5d33e61f..79f76310 100644 --- a/dns/rdtypes/mxbase.py +++ b/dns/rdtypes/mxbase.py @@ -18,6 +18,7 @@ """MX-like base classes.""" import struct +from typing import TypeVar import dns.exception import dns.immutable @@ -25,6 +26,8 @@ import dns.name import dns.rdata import dns.rdtypes.util +T = TypeVar("T", bound="MXBase") + @dns.immutable.immutable class MXBase(dns.rdata.Rdata): @@ -34,17 +37,22 @@ class MXBase(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, preference, exchange): super().__init__(rdclass, rdtype) - self.preference = self._as_uint16(preference) - self.exchange = self._as_name(exchange) + self.preference: int = self._as_uint16(preference) + self.exchange: dns.name.Name = self._as_name(exchange) - def to_text(self, origin=None, relativize=True, **kw): - exchange = self.exchange.choose_relativity(origin, relativize) - return f"{self.preference} {exchange}" + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + return f"{self.preference} {self.exchange.to_styled_text(style)}" @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: preference = tok.get_uint16() exchange = tok.get_name(origin, relativize, relativize_to) return cls(rdclass, rdtype, preference, exchange) @@ -55,7 +63,7 @@ class MXBase(dns.rdata.Rdata): self.exchange.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: preference = parser.get_uint16() exchange = parser.get_name(origin) return cls(rdclass, rdtype, preference, exchange) diff --git a/dns/rdtypes/nsbase.py b/dns/rdtypes/nsbase.py index 904224f0..6cabb4d2 100644 --- a/dns/rdtypes/nsbase.py +++ b/dns/rdtypes/nsbase.py @@ -17,11 +17,15 @@ """NS-like base classes.""" +from typing import TypeVar + import dns.exception import dns.immutable import dns.name import dns.rdata +T = TypeVar("T", bound="NSBase") + @dns.immutable.immutable class NSBase(dns.rdata.Rdata): @@ -31,16 +35,21 @@ class NSBase(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, target): super().__init__(rdclass, rdtype) - self.target = self._as_name(target) + self.target: dns.name.Name = self._as_name(target) - def to_text(self, origin=None, relativize=True, **kw): - target = self.target.choose_relativity(origin, relativize) - return str(target) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + return self.target.to_styled_text(style) @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: target = tok.get_name(origin, relativize, relativize_to) return cls(rdclass, rdtype, target) @@ -48,7 +57,7 @@ class NSBase(dns.rdata.Rdata): self.target.to_wire(file, compress, origin, canonicalize) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: target = parser.get_name(origin) return cls(rdclass, rdtype, target) diff --git a/dns/rdtypes/rrsigbase.py b/dns/rdtypes/rrsigbase.py index 3961a5d8..2b7208fd 100644 --- a/dns/rdtypes/rrsigbase.py +++ b/dns/rdtypes/rrsigbase.py @@ -19,10 +19,12 @@ import base64 import calendar import struct import time +from typing import TypeVar import dns.dnssectypes import dns.exception import dns.immutable +import dns.name import dns.rdata import dns.rdatatype @@ -49,6 +51,9 @@ def posixtime_to_sigtime(what): return time.strftime("%Y%m%d%H%M%S", time.gmtime(what)) +T = TypeVar("T", bound="RRSIGBase") + + @dns.immutable.immutable class RRSIGBase(dns.rdata.Rdata): """Base class for rdata that is like a RRSIG record""" @@ -80,25 +85,27 @@ class RRSIGBase(dns.rdata.Rdata): signature, ): super().__init__(rdclass, rdtype) - self.type_covered = self._as_rdatatype(type_covered) - self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) - self.labels = self._as_uint8(labels) - self.original_ttl = self._as_ttl(original_ttl) - self.expiration = self._as_uint32(expiration) - self.inception = self._as_uint32(inception) - self.key_tag = self._as_uint16(key_tag) - self.signer = self._as_name(signer) - self.signature = self._as_bytes(signature) + self.type_covered: dns.rdatatype.RdataType = self._as_rdatatype(type_covered) + self.algorithm: dns.dnssectypes.Algorithm = dns.dnssectypes.Algorithm.make( + algorithm + ) + self.labels: int = self._as_uint8(labels) + self.original_ttl: int = self._as_ttl(original_ttl) + self.expiration: int = self._as_uint32(expiration) + self.inception: int = self._as_uint32(inception) + self.key_tag: int = self._as_uint16(key_tag) + self.signer: dns.name.Name = self._as_name(signer) + self.signature: bytes = self._as_bytes(signature) def covers(self): return self.type_covered - def to_text(self, origin=None, relativize=True, **kw): + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: ctext = dns.rdatatype.to_text(self.type_covered) expiration = posixtime_to_sigtime(self.expiration) inception = posixtime_to_sigtime(self.inception) - signer = self.signer.choose_relativity(origin, relativize) - sig = dns.rdata._base64ify(self.signature, **kw) # pyright: ignore + signer = self.signer.to_styled_text(style) + sig = dns.rdata._styled_base64ify(self.signature, style, True) return ( f"{ctext} {self.algorithm} {self.labels} {self.original_ttl} " + f"{expiration} {inception} {self.key_tag} {signer} {sig}" @@ -106,8 +113,14 @@ class RRSIGBase(dns.rdata.Rdata): @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: type_covered = dns.rdatatype.from_text(tok.get_string()) algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string()) labels = tok.get_int() @@ -148,7 +161,7 @@ class RRSIGBase(dns.rdata.Rdata): file.write(self.signature) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: header = parser.get_struct("!HBBIIIH") signer = parser.get_name(origin) signature = parser.get_remaining() diff --git a/dns/rdtypes/svcbbase.py b/dns/rdtypes/svcbbase.py index f06d31ad..148c263c 100644 --- a/dns/rdtypes/svcbbase.py +++ b/dns/rdtypes/svcbbase.py @@ -3,13 +3,14 @@ import base64 import enum import struct -from typing import Any +from typing import Any, TypeVar import dns.enum import dns.exception import dns.immutable import dns.ipv4 import dns.ipv6 +import dns.name import dns.rdata import dns.rdtypes.util import dns.renderer @@ -457,6 +458,9 @@ def _validate_and_define(params, key, value): params[key] = value +T = TypeVar("T", bound="SVCBBase") + + @dns.immutable.immutable class SVCBBase(dns.rdata.Rdata): """Base class for SVCB-like records""" @@ -467,13 +471,13 @@ class SVCBBase(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, priority, target, params): super().__init__(rdclass, rdtype) - self.priority = self._as_uint16(priority) - self.target = self._as_name(target) + self.priority: int = self._as_uint16(priority) + self.target: dns.name.Name = self._as_name(target) for k, v in params.items(): k = ParamKey.make(k) if not isinstance(v, Param) and v is not None: raise ValueError(f"{k:d} not a Param") - self.params = dns.immutable.Dict(params) + self.params: dns.immutable.Dict = dns.immutable.Dict(params) # Make sure any parameter listed as mandatory is present in the # record. mandatory = params.get(ParamKey.MANDATORY) @@ -488,8 +492,8 @@ class SVCBBase(dns.rdata.Rdata): if ParamKey.ALPN not in params: raise ValueError("no-default-alpn present, but alpn missing") - def to_text(self, origin=None, relativize=True, **kw): - target = self.target.choose_relativity(origin, relativize) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + target = self.target.to_styled_text(style) params = [] for key in sorted(self.params.keys()): value = self.params[key] @@ -506,8 +510,14 @@ class SVCBBase(dns.rdata.Rdata): @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: priority = tok.get_uint16() target = tok.get_name(origin, relativize, relativize_to) if priority == 0: @@ -558,7 +568,7 @@ class SVCBBase(dns.rdata.Rdata): value.to_wire(file, origin) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: priority = parser.get_uint16() target = parser.get_name(origin) if priority == 0 and parser.remaining() != 0: diff --git a/dns/rdtypes/tlsabase.py b/dns/rdtypes/tlsabase.py index ddc196f1..363a34aa 100644 --- a/dns/rdtypes/tlsabase.py +++ b/dns/rdtypes/tlsabase.py @@ -17,11 +17,14 @@ import binascii import struct +from typing import TypeVar import dns.immutable import dns.rdata import dns.rdatatype +T = TypeVar("T", bound="TLSABase") + @dns.immutable.immutable class TLSABase(dns.rdata.Rdata): @@ -33,23 +36,25 @@ class TLSABase(dns.rdata.Rdata): def __init__(self, rdclass, rdtype, usage, selector, mtype, cert): super().__init__(rdclass, rdtype) - self.usage = self._as_uint8(usage) - self.selector = self._as_uint8(selector) - self.mtype = self._as_uint8(mtype) - self.cert = self._as_bytes(cert) + self.usage: int = self._as_uint8(usage) + self.selector: int = self._as_uint8(selector) + self.mtype: int = self._as_uint8(mtype) + self.cert: bytes = self._as_bytes(cert) - def to_text(self, origin=None, relativize=True, **kw): - kw = kw.copy() - chunksize = kw.pop("chunksize", 128) - cert = dns.rdata._hexify( - self.cert, chunksize=chunksize, **kw # pyright: ignore - ) + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: + cert = dns.rdata._styled_hexify(self.cert, style, True) return f"{self.usage} {self.selector} {self.mtype} {cert}" @classmethod def from_text( - cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None - ): + cls: type[T], + rdclass, + rdtype, + tok, + origin=None, + relativize=True, + relativize_to=None, + ) -> T: usage = tok.get_uint8() selector = tok.get_uint8() mtype = tok.get_uint8() @@ -63,7 +68,7 @@ class TLSABase(dns.rdata.Rdata): file.write(self.cert) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: header = parser.get_struct("BBB") cert = parser.get_remaining() return cls(rdclass, rdtype, header[0], header[1], header[2], cert) diff --git a/dns/rdtypes/txtbase.py b/dns/rdtypes/txtbase.py index b10da13b..8722c34c 100644 --- a/dns/rdtypes/txtbase.py +++ b/dns/rdtypes/txtbase.py @@ -18,7 +18,7 @@ """TXT-like base class.""" from collections.abc import Iterable -from typing import Any +from typing import TypeVar import dns.exception import dns.immutable @@ -29,6 +29,8 @@ import dns.rdatatype import dns.renderer import dns.tokenizer +T = TypeVar("T", bound="TXTBase") + @dns.immutable.immutable class TXTBase(dns.rdata.Rdata): @@ -57,29 +59,32 @@ class TXTBase(dns.rdata.Rdata): if len(self.strings) == 0: raise ValueError("the list of strings must not be empty") - def to_text( - self, - origin: dns.name.Name | None = None, - relativize: bool = True, - **kw: dict[str, Any], - ) -> str: + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: txt = "" prefix = "" for s in self.strings: - txt += f'{prefix}"{dns.rdata._escapify(s)}"' + if style is not None and style.txt_is_utf8: + try: + us = s.decode() + element = dns.rdata._escapify_unicode(us) + except Exception: + element = dns.rdata._escapify(s) + else: + element = dns.rdata._escapify(s) + txt += f'{prefix}"{element}"' prefix = " " return txt @classmethod def from_text( - cls, + cls: type[T], rdclass: dns.rdataclass.RdataClass, rdtype: dns.rdatatype.RdataType, tok: dns.tokenizer.Tokenizer, origin: dns.name.Name | None = None, relativize: bool = True, relativize_to: dns.name.Name | None = None, - ) -> dns.rdata.Rdata: + ) -> T: strings = [] for token in tok.get_remaining(): token = token.unescape_to_bytes() @@ -102,7 +107,7 @@ class TXTBase(dns.rdata.Rdata): file.write(s) @classmethod - def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + def from_wire_parser(cls: type[T], rdclass, rdtype, parser, origin=None) -> T: strings = [] while parser.remaining() > 0: s = parser.get_counted_bytes() diff --git a/dns/rdtypes/util.py b/dns/rdtypes/util.py index f0840d45..fc6fefe1 100644 --- a/dns/rdtypes/util.py +++ b/dns/rdtypes/util.py @@ -37,8 +37,8 @@ class Gateway: name = "" def __init__(self, type: Any, gateway: str | dns.name.Name | None = None): - self.type = dns.rdata.Rdata._as_uint8(type) - self.gateway = gateway + self.type: int = dns.rdata.Rdata._as_uint8(type) + self.gateway: str | dns.name.Name | None = gateway self._check() @classmethod @@ -64,14 +64,18 @@ class Gateway: else: raise SyntaxError(self._invalid_type(self.type)) - def to_text(self, origin=None, relativize=True): + def to_text(self, origin=None, relativize=True) -> str: + return self.to_styled_text(dns.rdata.RdataStyle(origin=origin, relativize=True)) + + def to_styled_text(self, style: dns.rdata.RdataStyle) -> str: if self.type == 0: return "." elif self.type in (1, 2): + assert isinstance(self.gateway, str) return self.gateway elif self.type == 3: assert isinstance(self.gateway, dns.name.Name) - return str(self.gateway.choose_relativity(origin, relativize)) + return self.gateway.to_styled_text(style) else: raise ValueError(self._invalid_type(self.type)) # pragma: no cover diff --git a/dns/rrset.py b/dns/rrset.py index 40982b5f..19501c68 100644 --- a/dns/rrset.py +++ b/dns/rrset.py @@ -133,7 +133,9 @@ class RRset(dns.rdataset.Rdataset): self, origin: dns.name.Name | None = None, relativize: bool = True, - **kw: dict[str, Any], + want_comments: bool = False, + style: dns.rdataset.RdatasetStyle | None = None, + **kw: Any, ) -> str: """Convert the RRset into DNS zone file format. @@ -149,18 +151,42 @@ class RRset(dns.rdataset.Rdataset): *relativize*, a ``bool``. If ``True``, names will be relativized to *origin*. + + *want_comments*, a ``bool``. If ``True``, emit comments for rdata + which have them. The default is ``False``. + + *style*, a :py:class:`dns.rdataset.RdatasetStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. """ + if style is None: + kw = kw.copy() + kw["origin"] = origin + kw["relativize"] = relativize + kw["want_comments"] = want_comments + style = dns.rdataset.RdatasetStyle.from_keywords(kw) + return self.to_styled_text(style) - return super().to_text( - self.name, origin, relativize, self.deleting, **kw # type: ignore - ) + def to_styled_text(self, style: dns.rdataset.RdatasetStyle) -> str: # type: ignore + """Convert the RRset to styled text. + + A new style is made from the specified style setting the ``override_rdclass`` + attribute appropriately for the deleting status of the RRset. + + *style*, a :py:class:`dns.rdataset.RdatasetStyle` or ``None`` (the default). If + specified, the style overrides the other parameters. + + returns a ``str``. + """ + if self.deleting is not None: + style = style.replace(override_rdclass=self.deleting) + return super().to_styled_text(style, self.name) def to_wire( # type: ignore self, file: Any, compress: dns.name.CompressType | None = None, origin: dns.name.Name | None = None, - **kw: dict[str, Any], + **kw: Any, ) -> int: """Convert the RRset to wire format. @@ -170,9 +196,7 @@ class RRset(dns.rdataset.Rdataset): Returns an ``int``, the number of records emitted. """ - return super().to_wire( - self.name, file, compress, origin, self.deleting, **kw # type: ignore - ) + return super().to_wire(self.name, file, compress, origin, self.deleting, **kw) # pylint: enable=arguments-differ diff --git a/dns/style.py b/dns/style.py new file mode 100644 index 00000000..bf04235e --- /dev/null +++ b/dns/style.py @@ -0,0 +1,28 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dataclasses +from typing import Any, TypeVar + +T = TypeVar("T", bound="BaseStyle") + + +@dataclasses.dataclass(frozen=True) +class BaseStyle: + """All text styles""" + + def replace(self: T, /, **changes) -> T: + return dataclasses.replace(self, **changes) + + @classmethod + def from_keywords(cls: type[T], kw: dict[str, Any]) -> T: + ok_kw: dict[str, Any] = {} + for k, v in kw.items(): + if k == "chunksize": + ok_kw["hex_chunk_size"] = v + ok_kw["base64_chunk_size"] = v + elif k == "separator": + ok_kw["hex_separator"] = v + ok_kw["base64_separator"] = v + elif hasattr(cls, k): + ok_kw[k] = v + return cls(**ok_kw) diff --git a/dns/transaction.py b/dns/transaction.py index 9b0c254a..8ce14eb4 100644 --- a/dns/transaction.py +++ b/dns/transaction.py @@ -115,6 +115,7 @@ class Transaction: self.manager = manager self.replacement = replacement self.read_only = read_only + self.unicode: set[str] = set() self._ended = False self._check_put_rdataset: list[CheckPutRdatasetType] = [] self._check_delete_rdataset: list[CheckDeleteRdatasetType] = [] @@ -281,6 +282,9 @@ class Transaction: new_rdataset = dns.rdataset.from_rdata(rdataset.ttl, rdata) self.replace(name, new_rdataset) + def add_unicode(self, attribute: str): + self.unicode.add(attribute.upper()) + def __iter__(self): self._check_ended() return self._iterate_rdatasets() diff --git a/dns/zone.py b/dns/zone.py index 06e65520..62df90a3 100644 --- a/dns/zone.py +++ b/dns/zone.py @@ -16,8 +16,8 @@ # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """DNS Zones.""" - import contextlib +import dataclasses import io import os import struct @@ -105,6 +105,41 @@ def _validate_name( return name +@dataclasses.dataclass(frozen=True) +class ZoneStyle(dns.node.NodeStyle): + """Zone text styles + + A ``ZoneStyle`` is also a :py:class:`dns.name.NameStyle` and a + :py:class:`dns.rdata.RdataStyle`, a :py:class:`dns.rdataset.RdatasetStyle`, + and a :py:class:`dns.node.NodeStyle`. + See those classes for a description of their options. + + *sorted*, a ``bool``. If True, the default, then the file + will be written with the names sorted in DNSSEC order from + least to greatest. Otherwise the names will be written in + whatever order they happen to have in the zone's dictionary. + + *nl*, a ``str`` or ``None`` (the default). The end of line string, + or if ``None``, the output will use the platform's native + end-of-line marker (i.e. LF on POSIX, CRLF on Windows). + + *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at + the start of the output. If ``False``, the default, do not emit + one. + + *want_unicode_directive*, a ``bool``. If ``True`` and the zone + has a non-empty ``unicode`` attribute, then emit a ``$UNICODE`` + line in the output. This directive is not standard, but allows + dnspython and other aware software to read and write Unicode zonefiles + without changing the rendering of names and TXT-like records. + """ + + sorted: bool = True + nl: str | None = None + want_origin: bool = False + want_unicode_directive: bool = True + + class Zone(dns.transaction.TransactionManager): """A DNS zone. @@ -154,6 +189,7 @@ class Zone(dns.transaction.TransactionManager): self.rdclass = rdclass self.nodes: MutableMapping[dns.name.Name, dns.node.Node] = self.map_factory() self.relativize = relativize + self.unicode: set[str] = set() def __eq__(self, other): """Two zones are equal if they have the same origin, class, and @@ -620,6 +656,7 @@ class Zone(dns.transaction.TransactionManager): nl: str | bytes | None = None, want_comments: bool = False, want_origin: bool = False, + style: ZoneStyle | None = None, ) -> None: """Write a zone to a file. @@ -647,7 +684,52 @@ class Zone(dns.transaction.TransactionManager): the start of the file. If ``False``, the default, do not emit one. """ + if style is None: + kw = {} + kw["sorted"] = sorted + kw["relativize"] = relativize + if relativize: + assert self.origin is not None + kw["origin"] = self.origin + kw["nl"] = nl + kw["want_comments"] = want_comments + kw["want_origin"] = want_origin + style = ZoneStyle.from_keywords(kw) + return self.to_styled_file(style, f) + + def _write_line(self, output, l, l_b, nl, nl_b): + try: + bout = cast(BinaryIO, output) + bout.write(l_b) + bout.write(nl_b) + except TypeError: # textual mode + tout = cast(TextIO, output) + tout.write(l) + tout.write(nl) + + def to_styled_file( + self, + style: ZoneStyle, + f: Any, + ) -> None: + """Write a zone to a styled file. + + *f*, a file or `str`. If *f* is a string, it is treated + as the name of a file to open. + """ + # Apply style items we learned from $UNICODE when we loaded the zone (if any). + if style.want_unicode_directive: + idna_codec: dns.name.IDNACodec | None = style.idna_codec + if "2008" in self.unicode: + idna_codec = dns.name.IDNA_2008_Practical + elif "2003" in self.unicode: + idna_codec = dns.name.IDNA_2003_Practical + if "TXT" in self.unicode: + txt_is_utf8 = True + else: + txt_is_utf8 = style.txt_is_utf8 + style = style.replace(idna_codec=idna_codec, txt_is_utf8=txt_is_utf8) if isinstance(f, str): cm: contextlib.AbstractContextManager = open(f, "wb") else: @@ -659,52 +741,44 @@ class Zone(dns.transaction.TransactionManager): if file_enc is None: file_enc = "utf-8" - if nl is None: + if style.nl is None: # binary mode, '\n' is not enough nl_b = os.linesep.encode(file_enc) nl = "\n" - elif isinstance(nl, str): - nl_b = nl.encode(file_enc) + elif isinstance(style.nl, str): + nl_b = style.nl.encode(file_enc) + nl = style.nl else: - nl_b = nl - nl = nl.decode() + nl_b = style.nl + nl = style.nl.decode() assert nl is not None assert nl_b is not None - if want_origin: + if style.want_unicode_directive and len(self.unicode) > 0: + l = "$UNICODE " + " ".join(sorted(self.unicode)) + l_b = l.encode(file_enc) + self._write_line(output, l, l_b, nl, nl_b) + if style.want_origin: assert self.origin is not None - l = "$ORIGIN " + self.origin.to_text() + # Ensure we don't relativize the origin to the origin in $ORIGIN! + origin_style = style.replace(origin=None) + l = "$ORIGIN " + self.origin.to_styled_text(origin_style) l_b = l.encode(file_enc) - try: - bout = cast(BinaryIO, output) - bout.write(l_b) - bout.write(nl_b) - except TypeError: # textual mode - tout = cast(TextIO, output) - tout.write(l) - tout.write(nl) - - if sorted: + self._write_line(output, l, l_b, nl, nl_b) + if style.default_ttl is not None: + l = f"$TTL {style.default_ttl}" + l_b = l.encode(file_enc) + self._write_line(output, l, l_b, nl, nl_b) + + if style.sorted: names = list(self.keys()) names.sort() else: names = self.keys() for n in names: - l = self[n].to_text( - n, - origin=self.origin, # pyright: ignore - relativize=relativize, # pyright: ignore - want_comments=want_comments, # pyright: ignore - ) + l = self[n].to_styled_text(style, n) l_b = l.encode(file_enc) - try: - bout = cast(BinaryIO, output) - bout.write(l_b) - bout.write(nl_b) - except TypeError: # textual mode - tout = cast(TextIO, output) - tout.write(l) - tout.write(nl) + self._write_line(output, l, l_b, nl, nl_b) def to_text( self, @@ -713,6 +787,7 @@ class Zone(dns.transaction.TransactionManager): nl: str | None = None, want_comments: bool = False, want_origin: bool = False, + style: ZoneStyle | None = None, ) -> str: """Return a zone's text as though it were written to a file. @@ -737,6 +812,9 @@ class Zone(dns.transaction.TransactionManager): the start of the output. If ``False``, the default, do not emit one. + *style*, a :py:class:`dns.zone.ZoneStyle` or ``None`` (the default). If specified, + the style overrides the other parameters. + Returns a ``str``. """ temp_buffer = io.StringIO() @@ -745,6 +823,19 @@ class Zone(dns.transaction.TransactionManager): temp_buffer.close() return return_value + def to_styled_text(self, style: ZoneStyle): + """Return a zone's styled text as though it were written to a file. + + See the documentation for :py:class:`dns.zone.ZoneStyle` for a description + of the style parameters. + """ + + temp_buffer = io.StringIO() + self.to_styled_file(style, temp_buffer) + return_value = temp_buffer.getvalue() + temp_buffer.close() + return return_value + def check_origin(self) -> None: """Do some simple checking of the zone's origin. @@ -1246,6 +1337,7 @@ def _from_text( ) try: reader.read() + zone.unicode = txn.unicode except dns.zonefile.UnknownOrigin: # for backwards compatibility raise UnknownOrigin diff --git a/dns/zonefile.py b/dns/zonefile.py index 590e79a1..ce7ec765 100644 --- a/dns/zonefile.py +++ b/dns/zonefile.py @@ -122,7 +122,7 @@ class Reader: self.current_file: Any | None = None self.allowed_directives: set[str] if allow_directives is True: - self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL"} + self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL", "$UNICODE"} if allow_include: self.allowed_directives.add("$INCLUDE") elif allow_directives is False: @@ -541,6 +541,18 @@ class Reader: self.current_origin = new_origin elif c == "$GENERATE": self._generate_line() + elif c == "$UNICODE": + while True: + token = self.tok.get() + if token.is_eol_or_eof(): + break + if not token.is_identifier(): + raise dns.exception.SyntaxError("bad $UNICODE") + self.txn.add_unicode(token.value) + if token.value == "2008": + self.tok.idna_codec = dns.name.IDNA_2008_Practical + elif token.value == "2003": + self.tok.idna_codec = dns.name.IDNA_2003_Practical else: raise dns.exception.SyntaxError( f"Unknown zone file directive '{c}'" diff --git a/doc/message-class.rst b/doc/message-class.rst index 5a833691..8f2731df 100644 --- a/doc/message-class.rst +++ b/doc/message-class.rst @@ -133,6 +133,10 @@ DNS opcodes that do not have a more specific class. message with ``dns.message.from_wire()`` or the output most recently generated by ``to_wire()``. +.. autoclass:: dns.message.MessageStyle + :members: + :inherited-members: + The following constants may be used to specify sections in the ``find_rrset()`` and ``get_rrset()`` methods: diff --git a/doc/name-class.rst b/doc/name-class.rst index eb1611eb..acae087e 100644 --- a/doc/name-class.rst +++ b/doc/name-class.rst @@ -27,3 +27,6 @@ The dns.name.Name Class and Predefined Names .. autoclass:: dns.name.NameRelation :members: + +.. autoclass:: dns.name.NameStyle + :members: diff --git a/doc/rdata-class.rst b/doc/rdata-class.rst index cdebaa80..82d61cfe 100644 --- a/doc/rdata-class.rst +++ b/doc/rdata-class.rst @@ -40,3 +40,7 @@ future behavior to be tested with existing code. .. autoclass:: dns.rdata.Rdata :members: :inherited-members: + +.. autoclass:: dns.rdata.RdataStyle + :members: + :inherited-members: diff --git a/doc/rdata-set-classes.rst b/doc/rdata-set-classes.rst index ed350085..d2963cc9 100644 --- a/doc/rdata-set-classes.rst +++ b/doc/rdata-set-classes.rst @@ -19,8 +19,19 @@ tree. Nodes are primarily used in ``Zone`` objects. .. autoclass:: dns.rdataset.Rdataset :members: +.. autoclass:: dns.rdataset.RdatasetStyle + :members: + :inherited-members: + .. autoclass:: dns.rrset.RRset :members: .. autoclass:: dns.node.Node :members: + +.. autoclass:: dns.node.NodeKind + :members: + +.. autoclass:: dns.node.NodeStyle + :members: + :inherited-members: diff --git a/doc/zone-class.rst b/doc/zone-class.rst index 554922d8..ed8a9fde 100644 --- a/doc/zone-class.rst +++ b/doc/zone-class.rst @@ -10,7 +10,7 @@ use the :py:class:`dns.versioned.Zone` class (described below). .. autoclass:: dns.zone.Zone :members: - + .. attribute:: rdclass The zone's rdata class, an ``int``; the default is class IN. @@ -20,10 +20,10 @@ use the :py:class:`dns.versioned.Zone` class (described below). The origin of the zone, a ``dns.name.Name``. .. attribute:: nodes - + A dictionary mapping the names of nodes in the zone to the nodes themselves. - + .. attribute:: relativize A ``bool``, which is ``True`` if names in the zone should be relativized. @@ -34,6 +34,9 @@ subclassed if a different node factory is desired. The node factory is a class or callable that returns a subclass of ``dns.node.Node``. +.. autoclass:: dns.zone.ZoneStyle + :members: + :inherited-members: The dns.versioned.Zone Class ---------------------------- @@ -68,11 +71,11 @@ Transactions are context managers, and are created with ``reader()`` or txn.set_serial() See below for more information on the ``Transaction`` API. - + .. autoclass:: dns.versioned.Zone :exclude-members: delete_node, delete_rdataset, replace_rdataset :members: - + .. attribute:: rdclass The zone's rdata class, an ``int``; the default is class IN. @@ -82,10 +85,10 @@ See below for more information on the ``Transaction`` API. The origin of the zone, a ``dns.name.Name``. .. attribute:: nodes - + A dictionary mapping the names of nodes in the zone to the nodes themselves. - + .. attribute:: relativize A ``bool``, which is ``True`` if names in the zone should be relativized. @@ -105,4 +108,4 @@ The Transaction Class .. autoclass:: dns.transaction.Transaction :members: - + diff --git a/tests/test_zone.py b/tests/test_zone.py index 6b354cd4..452c2c9e 100644 --- a/tests/test_zone.py +++ b/tests/test_zone.py @@ -287,6 +287,68 @@ web a 10.0.0.4 rrsig NSEC 1 3 3600 20200101000000 20030101000000 2143 foo MxFcby9k/yvedMfQgKzhH5er0Mu/vILz 45IkskceFGgiWCn/GxHhai6VAuHAoNUz 4YoU1tVfSCSqQYn6//11U6Nld80jEeC8 aTrO+KKmCaY= """ +example_unicode = """$UNICODE 2008 TXT +$ORIGIN example. +$TTL 300 +@ SOA ns1.example. hostmaster.example. 1 2 3 4 5 + NS ns1.example. + NS ns2.example. + MX 10 boîte-aux-lettres +ns1 60 A 10.53.0.1 + 60 A 10.53.1.1 +ns2 60 A 10.53.0.2 +élèves TXT "Je peux manger du verre, ça me fait pas mal." + TXT "Puedo comer vidrio, no me hace daño." + TXT "ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει." + TXT "Я могу есть стекло, оно мне не вредит." + TXT "मैं काँच खा सकता हूँ और मुझे उससे कोई चोट नहीं पहुंचती." + TXT "أنا قادر على أكل الزجاج و هذا لا يؤلمني." + TXT "我能吞下玻璃而不伤身体。" + TXT "私はガラスを食べられます。それは私を傷つけません。" + TXT "나는 유리를 먹을 수 있어요. 그래도 아프지 않아요" +""" + +example_unicode_expected = """$UNICODE 2008 TXT +$ORIGIN example. +@ 300 SOA ns1 hostmaster 1 2 3 4 5 + 300 NS ns1 + 300 NS ns2 + 300 MX 10 boîte-aux-lettres +ns1 60 A 10.53.0.1 + 60 A 10.53.1.1 +ns2 60 A 10.53.0.2 +élèves 300 TXT "Je peux manger du verre, ça me fait pas mal." + 300 TXT "Puedo comer vidrio, no me hace daño." + 300 TXT "ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει." + 300 TXT "Я могу есть стекло, оно мне не вредит." + 300 TXT "मैं काँच खा सकता हूँ और मुझे उससे कोई चोट नहीं पहुंचती." + 300 TXT "أنا قادر على أكل الزجاج و هذا لا يؤلمني." + 300 TXT "我能吞下玻璃而不伤身体。" + 300 TXT "私はガラスを食べられます。それは私を傷つけません。" + 300 TXT "나는 유리를 먹을 수 있어요. 그래도 아프지 않아요" +""" + +example_unicode_justified = """$UNICODE 2008 TXT +$ORIGIN example. +$TTL 300 +@ SOA ns1 hostmaster 1 2 3 4 5 + NS ns1 + NS ns2 + MX 10 boîte-aux-lettres +ns1 60 A 10.53.0.1 + 60 A 10.53.1.1 +ns2 60 A 10.53.0.2 +élèves TXT "Je peux manger du verre, ça me fait pas mal." + TXT "Puedo comer vidrio, no me hace daño." + TXT "ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει." + TXT "Я могу есть стекло, оно мне не вредит." + TXT "मैं काँच खा सकता हूँ और मुझे उससे कोई चोट नहीं पहुंचती." + TXT "أنا قادر على أكل الزجاج و هذا لا يؤلمني." + TXT "我能吞下玻璃而不伤身体。" + TXT "私はガラスを食べられます。それは私を傷つけません。" + TXT "나는 유리를 먹을 수 있어요. 그래도 아프지 않아요" +""" + _keep_output = True @@ -1188,6 +1250,38 @@ class ZoneTestCase(unittest.TestCase): with self.assertRaises(TypeError): node.replace_rdataset(None) + def testUnicodeThereAndBack(self): + z1 = dns.zone.from_text(example_unicode, "example") + s = dns.zone.ZoneStyle( + deduplicate_names=True, omit_rdclass=True, want_origin=True + ) + t1 = z1.to_styled_text(s) + self.assertEqual(t1, example_unicode_expected) + z2 = dns.zone.from_text(t1, "example") + self.assertEqual(z1, z2) + z1.unicode = set() + t2 = z1.to_text() + z3 = dns.zone.from_text(t2, "example.") + self.assertEqual(z1, z3) + + def testJustificationAndDefaultTTL(self): + z1 = dns.zone.from_text(example_unicode, "example") + s = dns.zone.ZoneStyle( + deduplicate_names=True, + omit_rdclass=True, + want_origin=True, + default_ttl=300, + name_just=-10, + ttl_just=4, + rdclass_just=-4, + rdtype_just=-8, + ) + t1 = z1.to_styled_text(s) + print(t1) + print("-------") + print(example_unicode_justified) + self.assertEqual(t1, example_unicode_justified) + class VersionedZoneTestCase(unittest.TestCase): zone_factory = dns.versioned.Zone