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.
rm -rf dist
rm -rf build
+.PHONY: doc
doc:
- cd doc; make html
+ make -C doc html
test:
pytest
"rrset",
"serial",
"set",
+ "style",
"tokenizer",
"transaction",
"tsig",
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:
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
"""DNS Messages"""
import contextlib
+import dataclasses
import enum
import io
import time
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
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."""
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``.
"""
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
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.
"""DNS Names."""
import copy
+import dataclasses
import encodings.idna # pyright: ignore
import functools
import struct
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...
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.
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.
"""DNS nodes. A node is a set of rdatasets."""
+import dataclasses
import enum
import io
from typing import Any
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"""
# 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
*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):
# 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
"""
-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(
# 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):
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)
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.
__slots__ = ["rdclass", "rdtype", "rdcomment"]
+ _crypto_keep_first_n: int | None = None
+
def __init__(
self,
rdclass: dns.rdataclass.RdataClass,
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``.
"""
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(
# 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
"""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."""
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.
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
#
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
import dns.immutable
+import dns.name
import dns.rdtypes.mxbase
# 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
import dns.exception
import dns.immutable
+import dns.name
import dns.rdata
import dns.rdtypes.util
name = "AMTRELAY relay"
@property
- def relay(self):
+ def relay(self) -> str | dns.name.Name | None:
return self.gateway
):
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}"
)
@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:
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
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()
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()
}
-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
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
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}"
import dns.enum
import dns.exception
import dns.immutable
+import dns.name
import dns.rdata
import dns.rdatatype
import dns.rdtypes.util
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}"
_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")
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()}"
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
import dns.exception
import dns.immutable
+import dns.name
import dns.rdata
import dns.rdatatype
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
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)}" '
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
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
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:
import struct
import dns.immutable
+import dns.name
import dns.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(
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
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(
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}"
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}"
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(
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
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(
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()
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"))
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
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(
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} "
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
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
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
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
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
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
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
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(
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
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)}" '
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
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
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
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):
import base64
import enum
import struct
+from typing import TypeVar
import dns.dnssectypes
import dns.exception
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"""
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()
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
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):
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()
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)
# 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):
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(
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)
"""MX-like base classes."""
import struct
+from typing import TypeVar
import dns.exception
import dns.immutable
import dns.rdata
import dns.rdtypes.util
+T = TypeVar("T", bound="MXBase")
+
@dns.immutable.immutable
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)
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)
"""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):
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)
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)
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
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"""
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}"
@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()
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()
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
params[key] = value
+T = TypeVar("T", bound="SVCBBase")
+
+
@dns.immutable.immutable
class SVCBBase(dns.rdata.Rdata):
"""Base class for SVCB-like records"""
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)
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]
@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:
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:
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):
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()
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)
"""TXT-like base class."""
from collections.abc import Iterable
-from typing import Any
+from typing import TypeVar
import dns.exception
import dns.immutable
import dns.renderer
import dns.tokenizer
+T = TypeVar("T", bound="TXTBase")
+
@dns.immutable.immutable
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()
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()
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
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
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.
*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.
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
--- /dev/null
+# 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)
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] = []
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()
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""DNS Zones."""
-
import contextlib
+import dataclasses
import io
import os
import struct
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.
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
nl: str | bytes | None = None,
want_comments: bool = False,
want_origin: bool = False,
+ style: ZoneStyle | None = None,
) -> None:
"""Write a zone to a file.
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:
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,
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.
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()
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.
)
try:
reader.read()
+ zone.unicode = txn.unicode
except dns.zonefile.UnknownOrigin:
# for backwards compatibility
raise UnknownOrigin
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:
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}'"
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:
.. autoclass:: dns.name.NameRelation
:members:
+
+.. autoclass:: dns.name.NameStyle
+ :members:
.. autoclass:: dns.rdata.Rdata
:members:
:inherited-members:
+
+.. autoclass:: dns.rdata.RdataStyle
+ :members:
+ :inherited-members:
.. 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:
.. autoclass:: dns.zone.Zone
:members:
-
+
.. attribute:: rdclass
The zone's rdata class, an ``int``; the default is class IN.
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.
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
----------------------------
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.
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.
.. autoclass:: dns.transaction.Transaction
:members:
-
+
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
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