"""
The Name module provides a class for DNS names.
The Name class
==============
A name is initialized from a tuple of bytes::
from dike import Name
python_sitename = Name((b'www', b'python', b'org'))
We can also create a name from a string using
:py:meth:`Name.fromstr()`, which makes a label for each dot-separated
piece. Each label is converted to
Punycode <https://tools.ietf.org/html/rfc3492>`_::
linux_sitename = Name.fromstr('www.kernel.org')
idn_city = Name.fromstr('Gießen.de')
idn_country = Name.fromstr('België.be')
A bytes value can also be used via
:py:meth:`Name.frombytes()`, which make a label for each dot-separated
piece. It does no further conversion::
bytes_name = Name.frombytes(b'non_unicode.name\\0x80')
If you are uncertain of which type you will be using to initialize, or
just want to make life easy, the :py:func:`make_name()` function will
create a name from any supported type::
from dike import make_name
example_name = make_name('a.good.example')
.. note::
All names are treated as `Fully-Qualified Domain Names (FQDN)
<https://tools.ietf.org/html/rfc8499#page-8>`_, also called
*absolute names*. Names are parsed the same if they end with a
dot, ``"."``, whether they do or not. For example,
``"foo.example."`` is the same as ``"foo.example"`` when used to
initialize a :py:class:`Name` instance.
:py:class:`Name` instances are immutable - once initialized the value
never changes. This has the advantage of allowing names to be used as
keys in :py:class:`dict` and various other places, but does mean that
updating a name is not possible.
Name comparisons use the canonical ordering defined in `RFC 4034
<https://tools.ietf.org/html/rfc4034#section-6.1>`_. This sorts the
names by the right-most label::
assert Name.fromstr('example') < Name.fromstr('Z.a.example')
assert Name.fromstr('Z.a.example') > Name.fromstr('a.example')
assert Name.fromstr('Z.a.eXaMPLe') == Name.fromstr('z.A.ExAmplE')
In general, names can be treated as sequences::
k_root = Name.fromstr('k.root-servers.net')
for label_val in k_root:
print(label_val)
assert 'net.root-servers.k' == str(Name(tuple(reversed(k_root))))
assert 'f.root-servers.net' == 'f' + k_root[1:]
You can use the `in` operator to check for the presence of a given
label in a name. Note that this searches for *labels*, not
*substrings*::
foo = Name.fromstr('foo.example')
assert "example" in "foo.example" # we end with "example"
assert "exam" not in "foo.example" # "exam" is not a label
assert "exam" in str("foo.example") # "exam" is a substring
Similarly the built-in `len()` function will return the number of
labels in a name, rather than the length of the name as a string. You
can get the length of a name either as a `str` or `bytes` value by
explicitly converting first::
assert len(foo) == 2
assert len(str(foo)) == 11
Converting to `bytes` works as expected, except in the case where
there is a dot `.` character in one of the labels. In that case, a
:py:class:`LabelHasDot` exception will be raised. Converting to `str`
also works as expected, except when there is a dot `.` character in
one of the labels (when a :py:class:`LabelHasDot` exception will be
raised) or a non-Unicode byte sequence (when a
:py:class:`UnicodeError` will be raised). To convert to a printable
format in a safe way, use the :py:meth:`Name.to_presentation()`
method::
bar = Name((b'www',b'K\\x80T',b'd.t'))
assert bar.to_presentation() == 'www.K\\\\128T.d\\\\.t.''
To check for parent/child relationships you can use the
:py:meth:`Name.startswith()` or :py:meth:`Name.endswith()` methods, or
slicing::
child = Name.fromstr('child.parent.zone')
parent = child[1:]
assert str(parent) == 'parent.zone'
assert child.endswith(parent)
assert child.endswith('zone')
These methods work very similar to the built-in
:py:meth:`str.startswith()` or :py:meth:`str.endswith()` methods,
including the ability to specify start & end values, as well as
comparing with multiple targets by passing a tuple::
zone_to_check = Name.fromstr('lots-o.info')
assert zone_to_check.endswith(('com', 'net', 'org', 'info'))
A couple of utility functions are also available to check for common
types of names::
host_name = Name.fromstr('valid.hostname)
not_host_name = Name.fromstr('in_valid.hostname')
assert hostname.ishost() == True
assert not_host_name.ishost() == False
assert Name().isroot() == True
assert Name('any-other-name').isroot() == False
"""
from typing import Iterator, Sequence, Tuple, Union
import encodings.idna
from dike.errors import EmptyLabel, LabelTooLong, LabelHasDot, NameTooLong
[docs]class Name:
"""
The :py:class:`Name` constructor requires a single argument, which
is a tuple of :py:class:`bytes` objects.
Optionally `canonicalize` can be set to `True`, in which case the
name will be set to the canonical (that is, lowercase) version.
If any of the labels is length 0, an :py:class:`EmptyLabel`
exception will be raised.
If any of the labels is longer than 63 bytes long, a
:py:class:`LabelTooLong` exception will be raised.
If the name is more than 253 characters long, a
:py:class:`NameTooLong` exception will be raised.
:param val: Value to use when creating the name.
:type val: tuple of Label
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`NameTooLong`
"""
__slots__ = (
'_reversed_labels',
'_canonical_reversed_labels',
'_hash_val',
'_ishost_flag',
)
# "I put my thing down, flip it and reverse it." - Missy Elliot
#
# If we need to, store our labels in reverse order. This is
# because names sort based on the rightmost labels first. Having
# the labels in reverse order makes DNS name comparisons easy in
# Python.
_reversed_labels: Tuple[bytes, ...]
_canonical_reversed_labels: Union[None, Tuple[bytes, ...]]
_hash_val: Union[None, int]
_ishost_flag: bool
def __init__(self, labels: Tuple[bytes, ...] = (), *,
canonicalize: bool = False) -> None:
# The _ishost_flag attribute is set when the ishost() method is
# first invoked.
# Special-case initialization of an empty name object.
if not labels:
self._reversed_labels = ()
self._canonical_reversed_labels = None
self._hash_val = None
return
# Go through the labels checking each for length, and each
# (possibly canonicalized) to a list of them reversed.
reversed_labels = []
len_check = 0
for label in reversed(labels):
label_len = len(label)
if label_len == 0:
raise EmptyLabel()
if label_len > 63:
raise LabelTooLong(label)
len_check += (1 + label_len)
if canonicalize:
reversed_labels.append(label.lower())
else:
reversed_labels.append(label)
# Make sure our length is not too long.
#
# Note that we are adding up the length byte plus the bytes
# of each label. That can be up to 254 bytes, since we need
# one more 0-byte to terminate the name when converted to wire
# format and fit in our 255-byte name length limit.
#
# We do could do this in the loop above, but we expect this
# condition to be rare, so prefer not to check many times for
# no reason.
if len_check > 254:
raise NameTooLong(labels)
# Save our reversed labels. If we canonicalized them, we can
# save them in our canonicalized labels, otherwise not.
self._reversed_labels = tuple(reversed_labels)
if canonicalize:
self._canonical_reversed_labels = self._reversed_labels
else:
self._canonical_reversed_labels = None
self._hash_val = None
@staticmethod
def _str_label_to_bytes(label: str) -> bytes:
try:
return bytes(encodings.idna.ToASCII(label))
except UnicodeError as err:
# encodings.idna.ToASCII raises a single exception on an
# empty label or too long label, so differentiate here.
if label == "":
raise EmptyLabel() from err
if str(err) == "label empty or too long":
raise LabelTooLong(label) from err
raise err
[docs] @classmethod
def fromstr(cls, val: str, *, canonicalize: bool = False) -> 'Name':
"""
Convert the specified value to a Name object, by splitting it
into labels first. It is converted to Punycode as necessary.
Optionally `canonicalize` can be set to `True`, in which case
the name will be set to the canonical (that is, lowercase)
version.
A number of issues will cause an exception to be raised. Note
that whitespace is not treated specially in any way, so if you
want to trim it do so before passing to this method.
:param val: Value to use when creating the name.
:type val: str
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`UnicodeError`
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
"""
# Special-case the root zone.
if val == '.':
return Name()
labels = val.split('.')
# Handle names written as FQDN (with the final dot).
if labels[-1] == '':
labels.pop()
# Handle empty name.
if not labels:
raise EmptyLabel()
initialized_labels = []
for label in labels:
initialized_labels.append(Name._str_label_to_bytes(label))
return Name(tuple(initialized_labels), canonicalize=canonicalize)
[docs] @classmethod
def frombytes(cls, val: bytes, *, canonicalize: bool = False) -> 'Name':
"""
Convert the specified value to a Name object, by splitting it
into labels first.
Optionally `canonicalize` can be set to `True`, in which case
the name will be set to the canonical (that is, lowercase)
version.
A number of issues will cause an exception to be raised. Note
that whitespace is not treated specially in any way, so if you
want to trim it do so before passing to this method.
:param val: Value to use when creating the name.
:type val: bytes
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
"""
# Special-case the root zone.
if val == b'.':
return Name()
labels = val.split(b'.')
# Handle names written as FQDN (with the final dot).
if labels[-1] == b'':
labels.pop()
# Handle empty name.
if not labels:
raise EmptyLabel()
return Name(tuple(labels), canonicalize=canonicalize)
def _invariant(self) -> None:
assert isinstance(self._reversed_labels, tuple)
name_len = 0
for label in self._reversed_labels:
assert isinstance(label, bytes)
assert 1 <= len(label) <= 63
name_len += len(bytes(label)) + 1
assert 0 <= name_len <= 254
if self._canonical_reversed_labels:
assert isinstance(self._canonical_reversed_labels, tuple)
assert (len(self._canonical_reversed_labels) ==
len(self._reversed_labels))
for pair in zip(self._reversed_labels,
self._canonical_reversed_labels):
assert pair[0].lower() == pair[1]
if self._hash_val is not None:
assert isinstance(self._hash_val, int)
assert self._hash_val == hash(self._canonical())
if getattr(self, '_ishost_flag', None) is not None:
assert isinstance(self._ishost_flag, bool)
def _canonical(self) -> Tuple[bytes, ...]:
if self._canonical_reversed_labels is None:
self._canonical_reversed_labels = tuple(label.lower() for label in
self._reversed_labels)
return self._canonical_reversed_labels
@staticmethod
def _label_ishost(label: bytes) -> bool:
if not chr(label[0]).isalnum():
return False
if len(label) > 1:
for octet in label[1:-1]:
if (not chr(octet).isalnum()) and (octet != ord('-')):
return False
if not chr(label[-1]).isalnum():
return False
return True
[docs] def ishost(self) -> bool:
"""
Test if the name is valid as a host name. The rules for host
names are defined in:
* `RFC 1034 Section 3.5
<https://tools.ietf.org/html/rfc1034#section-3.5>`_
* `RFC 1123 Section 2
<https://tools.ietf.org/html/rfc1123#section-2>`_
:rtype: bool
"""
if getattr(self, "_ishost_flag", None) is None:
if not self._reversed_labels:
self._ishost_flag = False
else:
ishost_flag = True
for label in self._reversed_labels:
if not Name._label_ishost(label):
ishost_flag = False
break
self._ishost_flag = ishost_flag
return self._ishost_flag
[docs] def isroot(self) -> bool:
"""
Test if the name is the root (that is, has no labels).
:rtype: bool
"""
return not bool(self._reversed_labels)
def _stringify(self) -> str:
labels = []
for name_label in self._reversed_labels:
label_str = encodings.idna.ToUnicode(name_label)
if '.' in label_str:
raise LabelHasDot(name_label)
labels.append(label_str)
labels.reverse()
return '.'.join(labels)
def __str__(self) -> str:
if not self._reversed_labels:
return '.'
return self._stringify()
def __bytes__(self) -> bytes:
if not self._reversed_labels:
return b'.'
labels = []
for name_label in self._reversed_labels:
label_bytes = bytes(name_label)
if b'.' in label_bytes:
raise LabelHasDot(name_label)
labels.append(label_bytes)
labels.reverse()
return b'.'.join(labels)
# This is a table used to translate bytes in a label to the master
# zone file presentation format, as documented in RFC 1035.
#
# Non-printable characters get translated into the decimal-escaped
# version, so chr(4) becomes '\004'.
#
# We also escape a few other characters:
#
# * <space> becomes '\032', to avoid treating it as whitespace.
# Note that this is not strictly necessary, and could possibly
# be presented as '\ '. This may be visually confusing, so we
# opt for the decimal-encoded version.
#
# * The double-quote, ", becomes '\"', to avoid starting or ending
# a quoted string.
#
# * The dollar sign, $, becomes '\$', to avoid anyone confusing
# with a control entry like $INCLUDE or $ORIGIN. (You _can_ have
# a label named '$ORIGIN', after all.) Note that this is not
# strictly necessary at all times; it could be used only for
# dollar signs that appear as the first character as a label.
# However using it in all cases is not an error.
#
# * Open and close parenthesis become '\(' and '\)', respectively.
# These are used for grouping otherwise.
#
# * The dot, ., becomes '\.', to avoid being used as the label
# separator.
#
# * The semicolon, ;, becomes '\;', to avoid being used as the
# start of a comment.
#
# * The at sign, @, becomes '\@', to avoid being substituted for
# the origin.
#
# * The backslash, \, becomes '\092', since it otherwise indicates
# a escaped character.
#
_presentation_translation = [
'\\000', '\\001', '\\002', '\\003', '\\004', '\\005', '\\006', '\\007',
'\\008', '\\009', '\\010', '\\011', '\\012', '\\013', '\\014', '\\015',
'\\016', '\\017', '\\018', '\\019', '\\020', '\\021', '\\022', '\\023',
'\\024', '\\025', '\\026', '\\027', '\\028', '\\029', '\\030', '\\031',
'\\032', '!', '\\"', '#', '\\$', '%', '&', "'",
'\\(', '\\)', '*', '+', ',', '-', '\\.', '/',
'0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', ':', '\\;', '<', '=', '>', '?',
'\\@', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
'X', 'Y', 'Z', '[', '\\092', ']', '^', '_',
'`', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
'x', 'y', 'z', '{', '|', '}', '~', '\\127',
'\\128', '\\129', '\\130', '\\131', '\\132', '\\133', '\\134', '\\135',
'\\136', '\\137', '\\138', '\\139', '\\140', '\\141', '\\142', '\\143',
'\\144', '\\145', '\\146', '\\147', '\\148', '\\149', '\\150', '\\151',
'\\152', '\\153', '\\154', '\\155', '\\156', '\\157', '\\158', '\\159',
'\\160', '\\161', '\\162', '\\163', '\\164', '\\165', '\\166', '\\167',
'\\168', '\\169', '\\170', '\\171', '\\172', '\\173', '\\174', '\\175',
'\\176', '\\177', '\\178', '\\179', '\\180', '\\181', '\\182', '\\183',
'\\184', '\\185', '\\186', '\\187', '\\188', '\\189', '\\190', '\\191',
'\\192', '\\193', '\\194', '\\195', '\\196', '\\197', '\\198', '\\199',
'\\200', '\\201', '\\202', '\\203', '\\204', '\\205', '\\206', '\\207',
'\\208', '\\209', '\\210', '\\211', '\\212', '\\213', '\\214', '\\215',
'\\216', '\\217', '\\218', '\\219', '\\220', '\\221', '\\222', '\\223',
'\\224', '\\225', '\\226', '\\227', '\\228', '\\229', '\\230', '\\231',
'\\232', '\\233', '\\234', '\\235', '\\236', '\\237', '\\238', '\\239',
'\\240', '\\241', '\\242', '\\243', '\\244', '\\245', '\\246', '\\247',
'\\248', '\\249', '\\250', '\\251', '\\252', '\\253', '\\254', '\\255',
]
[docs] def to_presentation(self) -> str:
"""
Return a string of the name converted to the master zone file
presentation format described in
`RFC 1035 <https://tools.ietf.org/html/rfc1035#section-5.1>`_.
:return: presentation format of the name
:rtype: str
"""
if not self._reversed_labels:
return "."
presentation = []
for label in self._reversed_labels:
presentation_octets = [self._presentation_translation[octet]
for octet in label]
presentation.append(''.join(presentation_octets))
presentation.reverse()
return ".".join(presentation) + "."
[docs] def to_wire(self) -> bytes:
"""
Return a DNS wire-format version of the name, without any name
compression.
:return: wire format of the name
:rtype: bytes
"""
# Apparently using a bytearray is the fastest way to
# concatenate bytes:
# https://www.guyrutenberg.com/2020/04/04/fast-bytes-concatenation-in-python/
wire_labels = bytearray()
for label in reversed(self._reversed_labels):
wire_labels += chr(len(bytes(label))).encode()
wire_labels += bytes(label)
wire_labels += b'\0'
return bytes(wire_labels)
def __repr__(self) -> str:
cls_name = self.__class__.__name__
if not self._reversed_labels:
return cls_name + "()"
try:
try:
return cls_name + ".fromstr(" + repr(self._stringify()) + ")"
except UnicodeError:
return cls_name + ".frombytes(" + repr(bytes(self)) + ")"
except LabelHasDot:
labels = ", ".join(repr(label)
for label in reversed(self._reversed_labels))
return cls_name + "([" + labels + "])"
def __len__(self) -> int:
return len(self._reversed_labels)
@staticmethod
def _prepare_labels(name_val: Union['Name',
str, bytes]) -> Tuple[bytes, ...]:
# pylint: disable=protected-access
if isinstance(name_val, Name):
return name_val._canonical()
if isinstance(name_val, str):
return Name.fromstr(name_val)._canonical()
if isinstance(name_val, bytes):
return Name.frombytes(name_val)._canonical()
raise TypeError(f"Cannot be converted to a Name instance: {name_val}")
def __eq__(self, other: object) -> bool:
if not isinstance(other, (Name, str, bytes)):
return NotImplemented
return self._canonical() == Name._prepare_labels(other)
def __ge__(self, other: Union['Name', str, bytes]) -> bool:
return self._canonical() >= Name._prepare_labels(other)
def __gt__(self, other: Union['Name', str, bytes]) -> bool:
return self._canonical() > Name._prepare_labels(other)
def __le__(self, other: Union['Name', str, bytes]) -> bool:
return self._canonical() <= Name._prepare_labels(other)
def __lt__(self, other: Union['Name', str, bytes]) -> bool:
return self._canonical() < Name._prepare_labels(other)
def __ne__(self, other: object) -> bool:
if not isinstance(other, (Name, str, bytes)):
return NotImplemented
return self._canonical() != Name._prepare_labels(other)
def __hash__(self) -> int:
if self._hash_val is None:
self._hash_val = hash(self._canonical())
return self._hash_val
def __contains__(self, other: Union['Name', str, bytes]) -> bool:
# Looking to see if a given name contains another name is
# equivalent to string-searching:
#
# https://en.wikipedia.org/wiki/String-searching_algorithm
#
# We choose a simple set of heuristics and then a naive search
# rather than a more sophisticated algorithm because we expect
# that most DNS names will be relatively short. This is not
# necessarily so, and in the case of IPv6 reverse names this
# is definitely not true, where each name is 34 labels.
other_labels = Name._prepare_labels(other)
my_labels = self._canonical()
if len(other_labels) == 1:
return other_labels[0] in my_labels
if len(other_labels) > len(my_labels):
return False
for ofs in range(len(my_labels) - len(other_labels) + 1):
if my_labels[ofs:ofs+len(other_labels)] == other_labels:
return True
return False
def __add__(self, other: Union['Name', str, bytes]) -> 'Name':
new_name = Name()
new_name._reversed_labels = (Name._prepare_labels(other) +
self._reversed_labels)
return new_name
def __radd__(self, other: Union['Name', str, bytes]) -> 'Name':
new_name = Name()
new_name._reversed_labels = (self._reversed_labels +
Name._prepare_labels(other))
return new_name
def __iter__(self) -> Iterator[bytes]:
return iter(reversed(self._reversed_labels))
def __getitem__(self,
slice_obj: Union[int, slice]) -> Union['Name', bytes]:
# If we have a integer, just grab the value at the correct offset.
if isinstance(slice_obj, int):
return self._reversed_labels[-slice_obj - 1]
# Calculate our new start position.
if slice_obj.start is None:
start = len(self._reversed_labels) - 1
else:
start = -slice_obj.start - 1
# Calculate the new end position.
if slice_obj.stop is None:
stop = None
else:
stop = -slice_obj.stop - 1
# Calculate the step - reverse of the old step.
if slice_obj.step is None:
step = -1
else:
step = -slice_obj.step
# Return a new Name object with the labels found in the slice.
return Name(self._reversed_labels[start:stop:step])
[docs] def endswith(self,
suffix: Union['Name', str, bytes,
Tuple[Union['Name', str, bytes], ...]],
start: int = 0, end: Union[int, None] = None) -> bool:
"""
Return True if the name ends with the specified suffix.
With optional start, test the name beginning at that label.
With optional end, stop comparing the name at that label.
The suffix can be a :py:class:`Name`, :py:class:`str`, or
:py:class:`bytes`.
The suffix can also be a :py:class:`tuple` of :py:class:`Name`
or things that can be converted to :py:class:`Name`
(:py:class:`str` or :py:class:`bytes`).
:param suffix: Suffix to check the end of the name for.
:type suffix: str, bytes, :py:class:`Name`, or tuple
:param start: label to start checking at.
:type start: int
:param end: label to stop checking at.
:type end: int
:return: Whether the name ends with the specified prefix.
:rtype: bool
:raises: :py:class:`TypeError` when suffix cannot be compared
"""
if start > len(self._reversed_labels):
return False
if isinstance(suffix, tuple):
suffix_list = suffix
else:
suffix_list = (suffix,)
rev_start = -start - 1
rev_end = None
if end is not None:
rev_end = -1 - end
label_slice = list(self._reversed_labels[rev_start:rev_end:-1])
label_slice.reverse()
for test_suffix in suffix_list:
try:
suffix_labels = Name._prepare_labels(test_suffix)
except EmptyLabel:
if isinstance(test_suffix, str) and (test_suffix == ''):
return True
if isinstance(test_suffix, bytes) and (test_suffix == b''):
return True
raise
if len(suffix_labels) > len(label_slice):
continue
if len(suffix_labels) == 0:
return True
end_slice = label_slice[:len(suffix_labels)]
if tuple(end_slice) == suffix_labels:
return True
return False
[docs] def startswith(self,
prefix: Union['Name', str, bytes,
Tuple[Union['Name', str, bytes], ...]],
start: int = 0, end: Union[int, None] = None) -> bool:
"""
Return True if the name starts with the specified prefix.
With optional start, test the name beginning at that label.
With optional end, stop comparing the name at that label.
The prefix can be a :py:class:`Name`, :py:class:`str`, or
:py:class:`bytes`.
The prefix can also be a :py:class:`tuple` of :py:class:`Name`
or things that can be converted to :py:class:`Name`
(:py:class:`str` or :py:class:`bytes`).
:param prefix: Prefix to check the end of the name for.
:type prefix: str, bytes, :py:class:`Name` or tuple
:param start: Label to start checking at.
:type start: int
:param end: Label to stop checking at.
:type end: int
:return: Whether the name ends with the specified prefix.
:rtype: bool
:raises: :py:class:`TypeError` when prefix cannot be compared
"""
if start > len(self._reversed_labels):
return False
if isinstance(prefix, tuple):
prefix_list = prefix
else:
prefix_list = (prefix,)
rev_start = -start - 1
rev_end = None
if end is not None:
rev_end = -1 - end
label_slice = list(self._reversed_labels[rev_start:rev_end:-1])
label_slice.reverse()
for test_prefix in prefix_list:
try:
prefix_labels = Name._prepare_labels(test_prefix)
except EmptyLabel:
if isinstance(test_prefix, str) and (test_prefix == ''):
return True
if isinstance(test_prefix, bytes) and (test_prefix == b''):
return True
raise
if len(prefix_labels) > len(label_slice):
continue
if len(prefix_labels) == 0:
return True
start_slice = label_slice[-len(prefix_labels):]
if tuple(start_slice) == prefix_labels:
return True
return False
[docs]def make_name(name_val: Union[Name, str, bytes,
Sequence[Union[str, bytes]]], *,
canonicalize: bool = False) -> Name:
"""
Initialize a name from a string, bytes, or a sequence containing
these (such as a list or tuple). It can also be initialized from
another name.
Optionally `canonicalize` can be set to `True`, in which case the
name will be set to the canonical (that is, lowercase) version.
:param name_val: Value to create a new name from.
:type prefix: str, bytes, :py:class:`Name`
or a sequence of str or bytes
:param canonicalize: Whether to canonicalize on creation.
:type canonicalize: bool
:return: an initialized name
:rtype: Name
:raises: :py:class:`UnicodeError`
:raises: :py:class:`EmptyLabel`
:raises: :py:class:`LabelTooLong`
:raises: :py:class:`NameTooLong`
:raises: :py:class:`TypeError`
"""
# pylint: disable=protected-access
if isinstance(name_val, Name):
if canonicalize:
labels = list(name_val._reversed_labels)
labels.reverse()
result = Name(tuple(labels), canonicalize=True)
else:
result = name_val
elif isinstance(name_val, str):
result = Name.fromstr(name_val, canonicalize=canonicalize)
elif isinstance(name_val, bytes):
result = Name.frombytes(name_val, canonicalize=canonicalize)
elif isinstance(name_val, Sequence):
labels = []
for val in name_val:
if isinstance(val, str):
labels.append(Name._str_label_to_bytes(val))
elif isinstance(val, bytes):
labels.append(val)
else:
disallowed = (type(name_val).__module__ + '.' +
type(name_val).__name__)
raise TypeError("make_name() argument in tuple must be "
f"a string or bytes, not '{disallowed}'")
result = Name(tuple(labels), canonicalize=canonicalize)
else:
allowed = "a string, bytes, Name, or a tuple of those"
disallowed = type(name_val).__module__ + '.' + type(name_val).__name__
raise TypeError(f"make_name() argument must be {allowed}, "
f"not '{disallowed}'")
return result