--- /dev/null
+#!/usr/bin/python3
+
+# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+#
+# SPDX-License-Identifier: MPL-2.0
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at https://mozilla.org/MPL/2.0/.
+#
+# See the COPYRIGHT file distributed with this work for additional
+# information regarding copyright ownership.
+
+from typing import List
+from warnings import warn
+
+from hypothesis.strategies import (
+ binary,
+ builds,
+ composite,
+ integers,
+ just,
+ nothing,
+ permutations,
+)
+
+import dns.name
+import dns.message
+import dns.rdataclass
+import dns.rdatatype
+
+# LATER: Move this file so it can be easily reused.
+
+
+@composite
+def dns_names(
+ draw,
+ *,
+ prefix: dns.name.Name = dns.name.empty,
+ suffix: dns.name.Name = dns.name.root,
+ min_labels: int = 1,
+ max_labels: int = 128,
+) -> dns.name.Name:
+ """
+ This is a hypothesis strategy to be used for generating DNS names with given `prefix`, `suffix`
+ and with total number of labels specified by `min_labels` and `max labels`.
+
+ For example, calling
+ ```
+ dns_names(
+ prefix=dns.name.from_text("test"),
+ suffix=dns.name.from_text("isc.org"),
+ max_labels=6
+ ).example()
+ ```
+ will result in names like `test.abc.isc.org.` or `test.abc.def.isc.org`.
+
+ There is no attempt to make the distribution of the generated names uniform in any way.
+ The strategy however minimizes towards shorter names with shorter labels.
+
+ It can be used with to build compound strategies, like this one which generates random DNS queries.
+
+ ```
+ dns_queries = builds(
+ dns.message.make_query,
+ qname=dns_names(),
+ rdtype=dns_rdatatypes,
+ rdclass=dns_rdataclasses,
+ )
+ ```
+ """
+
+ prefix = prefix.relativize(dns.name.root)
+ suffix = suffix.derelativize(dns.name.root)
+
+ try:
+ outer_name = prefix + suffix
+ remaining_bytes = 255 - len(outer_name.to_wire())
+ assert remaining_bytes >= 0
+ except dns.name.NameTooLong:
+ warn(
+ "Maximal length name of name execeeded by prefix and suffix. Strategy won't generate any names.",
+ RuntimeWarning,
+ )
+ return draw(nothing())
+
+ minimum_number_of_labels_to_generate = max(0, min_labels - len(outer_name.labels))
+ maximum_number_of_labels_to_generate = max_labels - len(outer_name.labels)
+ if maximum_number_of_labels_to_generate < 0:
+ warn(
+ "Maximal number of labels execeeded by prefix and suffix. Strategy won't generate any names.",
+ RuntimeWarning,
+ )
+ return draw(nothing())
+
+ maximum_number_of_labels_to_generate = min(
+ maximum_number_of_labels_to_generate, remaining_bytes // 2
+ )
+ if maximum_number_of_labels_to_generate < minimum_number_of_labels_to_generate:
+ warn(
+ f"Minimal number set to {minimum_number_of_labels_to_generate}, but in {remaining_bytes} bytes there is only space for maximum of {maximum_number_of_labels_to_generate} labels.",
+ RuntimeWarning,
+ )
+ return draw(nothing())
+
+ if remaining_bytes == 0 or maximum_number_of_labels_to_generate == 0:
+ warn(
+ f"Strategy will return only one name ({outer_name}) as it exactly matches byte or label length limit.",
+ RuntimeWarning,
+ )
+ return draw(just(outer_name))
+
+ chosen_number_of_labels_to_generate = draw(
+ integers(
+ minimum_number_of_labels_to_generate, maximum_number_of_labels_to_generate
+ )
+ )
+ chosen_number_of_bytes_to_partion = draw(
+ integers(2 * chosen_number_of_labels_to_generate, remaining_bytes)
+ )
+ chosen_lengths_of_labels = draw(
+ _partition_bytes_to_labels(
+ chosen_number_of_bytes_to_partion, chosen_number_of_labels_to_generate
+ )
+ )
+ generated_labels = tuple(
+ draw(binary(min_size=l - 1, max_size=l - 1)) for l in chosen_lengths_of_labels
+ )
+
+ return dns.name.Name(prefix.labels + generated_labels + suffix.labels)
+
+
+RDATACLASS_MAX = RDATATYPE_MAX = 65535
+dns_rdataclasses = builds(dns.rdataclass.RdataClass, integers(0, RDATACLASS_MAX))
+dns_rdataclasses_without_meta = dns_rdataclasses.filter(dns.rdataclass.is_metaclass)
+dns_rdatatypes = builds(dns.rdatatype.RdataType, integers(0, RDATATYPE_MAX))
+
+# NOTE: This should really be `dns_rdatatypes_without_meta = dns_rdatatypes_without_meta.filter(dns.rdatatype.is_metatype()`,
+# but hypothesis then complains about the filter being too strict, so it is done in a “constructive” way.
+dns_rdatatypes_without_meta = integers(0, dns.rdatatype.OPT - 1) | integers(dns.rdatatype.OPT + 1, 127) | integers(256, RDATATYPE_MAX) # type: ignore
+
+
+@composite
+def _partition_bytes_to_labels(
+ draw, remaining_bytes: int, number_of_labels: int
+) -> List[int]:
+ two_bytes_reserved_for_label = 2
+
+ # Reserve two bytes for each label
+ partition = [two_bytes_reserved_for_label] * number_of_labels
+ remaining_bytes -= two_bytes_reserved_for_label * number_of_labels
+
+ assert remaining_bytes >= 0
+
+ # Add a random number between 0 and the remainder to each partition
+ for i in range(number_of_labels):
+ added = draw(
+ integers(0, min(remaining_bytes, 64 - two_bytes_reserved_for_label))
+ )
+ partition[i] += added
+ remaining_bytes -= added
+
+ # NOTE: Some of the remaining bytes will usually not be assigned to any label, but we don't care.
+
+ return draw(permutations(partition))