From: Miod Vallat Date: Mon, 7 Apr 2025 09:55:21 +0000 (+0200) Subject: Add optional variants to ZoneName. X-Git-Tag: auth-5.0.0-alpha1~1^2~23 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=6c13c42202c70ff54d5fda64779cf406add70970;p=thirdparty%2Fpdns.git Add optional variants to ZoneName. --- diff --git a/pdns/dnsname.cc b/pdns/dnsname.cc index 4f735d8f08..147aa4cbd6 100644 --- a/pdns/dnsname.cc +++ b/pdns/dnsname.cc @@ -757,4 +757,133 @@ void DNSName::makeUsRelative(const ZoneName& zone) { makeUsRelative(zone.operator const DNSName&()); } + +std::string_view::size_type ZoneName::findVariantSeparator(std::string_view name) +{ + std::string_view::size_type pos{0}; + + // Try to be as fast as possible in the non-variant case and exit + // quickly if we don't find two dots in a row. + while ((pos = name.find('.', pos)) != std::string_view::npos) { + ++pos; + if (pos >= name.length()) { // trailing single dot + return std::string_view::npos; + } + if (name.at(pos) == '.') { + // We have found two dots in a row, but the first dot might have been + // escaped. So we now need to count how many \ characters we can find a + // row before it; if their number is odd, the first dot is escaped and + // we need to keep searching. + size_t slashes{0}; + while (pos >= 2 + slashes && name.at(pos - 2 - slashes) == '\\') { + ++slashes; + } + if ((slashes % 2) == 0) { + break; + } + } + } + return pos; +} + +ZoneName::ZoneName(std::string_view name) +{ + if (auto sep = findVariantSeparator(name); sep != std::string_view::npos) { + setVariant(name.substr(sep + 1)); // ignore leading dot in variant name + name = name.substr(0, sep); // keep trailing dot in zone name + } + d_name = DNSName(name); +} + +ZoneName::ZoneName(std::string_view name, std::string_view::size_type sep) +{ + if (sep != std::string_view::npos) { + setVariant(name.substr(sep + 1)); // ignore leading dot in variant name + name = name.substr(0, sep); // keep trailing dot in zone name + } + d_name = DNSName(name); +} + +void ZoneName::setVariant(std::string_view variant) +{ + if (variant.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789_-") != std::string_view::npos) { + throw std::out_of_range("invalid character in variant name '" + std::string{variant} + "'"); + } + d_variant = variant; +} + +std::string ZoneName::toLogString() const +{ + std::string ret = d_name.toLogString(); + if (!d_variant.empty()) { + // Because toLogString() above uses toStringRootDot(), we do not want to + // output one too many dots if this is a root-with-variant. + ret.push_back('.'); + if (!d_name.isRoot()) { + ret.push_back('.'); + } + ret += d_variant; + } + return ret; +} + +size_t ZoneName::hash(size_t init) const +{ + if (!d_variant.empty()) { + init = burtleCI(reinterpret_cast(d_variant.data()), d_variant.length(), init); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast): can't static_cast because of sign difference + } + + return d_name.hash(init); +} + +bool ZoneName::operator<(const ZoneName& rhs) const +{ + // Order by DNSName first, by variant second. + // Unfortunately we can't use std::lexicographical_compare_three_way() yet + // as this would require C++20. + const auto *iter1 = d_name.getStorage().cbegin(); + const auto *last1 = d_name.getStorage().cend(); + const auto *iter2 = rhs.d_name.getStorage().cbegin(); + const auto *last2 = rhs.d_name.getStorage().cend(); + while (iter1 != last1 && iter2 != last2) { + auto char1 = dns_tolower(*iter1); + auto char2 = dns_tolower(*iter2); + if (char1 < char2) { + return true; + } + if (char1 > char2) { + return false; + } + ++iter1; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + ++iter2; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + } + if (iter1 == last1) { + if (iter2 != last2) { + return true; // our DNSName is shorter (subset) than the other + } + } + else { + return false; // our DNSName is longer (superset) than the other + } + // At this point, both DNSName compare equal, we have to compare + // variants (which are case-sensitive). + return d_variant < rhs.d_variant; +} + +bool ZoneName::canonCompare(const ZoneName& rhs) const +{ + // Similarly to operator< above, this compares DNSName first, variant + // second. Unfortunately because DNSName::canonCompare() is complicated, + // it can't pragmatically be duplicated here, hence the two calls. + // TODO: change DNSName::canonCompare() to return a three-state value + // (lt, eq, ge) in order to be able to call it only once. + if (!d_name.canonCompare(rhs.d_name)) { + return false; + } + if (!rhs.d_name.canonCompare(d_name)) { + return true; + } + // Both DNSName compare equal. + return d_variant < rhs.d_variant; +} #endif // ] diff --git a/pdns/dnsname.hh b/pdns/dnsname.hh index 1c2e591bd2..9339b9d847 100644 --- a/pdns/dnsname.hh +++ b/pdns/dnsname.hh @@ -34,6 +34,8 @@ #include #include +using namespace std::string_view_literals; + #include #include @@ -317,10 +319,14 @@ inline DNSName operator+(const DNSName& lhs, const DNSName& rhs) extern const DNSName g_rootdnsname, g_wildcarddnsname; -// ZoneName: this is equivalent to DNSName, but intended to only store zone -// names. #if defined(PDNS_AUTH) // [ -// Conversions between DNSName and ZoneName are allowed, but must be explicit. +// ZoneName: this is equivalent to DNSName, but intended to only store zone +// names. In addition to the name, an optional variant is allowed. The +// variant is never part of a DNS packet; it can only be used by backends to +// perform specific extra processing. +// Variant names are limited to [a-z0-9_-]. +// Conversions between DNSName and ZoneName are allowed, but must be explicit; +// conversions to DNSName lose the variant part. class ZoneName { public: @@ -330,6 +336,7 @@ public: { if (this != &rhs) { d_name = rhs.d_name; + d_variant = rhs.d_variant; } return *this; } @@ -337,23 +344,27 @@ public: { if (this != &rhs) { d_name = std::move(rhs.d_name); + d_variant = std::move(rhs.d_variant); } return *this; } ZoneName(const ZoneName& a) = default; ZoneName(ZoneName&& a) = default; - explicit ZoneName(std::string_view name) : d_name(name) {} - explicit ZoneName(const DNSName& name) : d_name(name) {} + explicit ZoneName(std::string_view name); + explicit ZoneName(std::string_view name, std::string_view variant) : d_name(name), d_variant(variant) {} + explicit ZoneName(const DNSName& name, std::string_view variant = ""sv) : d_name(name), d_variant(variant) {} + explicit ZoneName(std::string_view name, std::string_view::size_type sep); bool isPartOf(const ZoneName& rhs) const { return d_name.isPartOf(rhs.d_name); } bool isPartOf(const DNSName& rhs) const { return d_name.isPartOf(rhs); } - bool operator==(const ZoneName& rhs) const { return d_name == rhs.d_name; } - bool operator!=(const ZoneName& rhs) const { return d_name != rhs.d_name; } + bool operator==(const ZoneName& rhs) const { return d_name == rhs.d_name && d_variant == rhs.d_variant; } + bool operator!=(const ZoneName& rhs) const { return !operator==(rhs); } + // IMPORTANT! None of the "toString" routines will output the variant, but toLogString(). std::string toString(const std::string& separator=".", const bool trailing=true) const { return d_name.toString(separator, trailing); } void toString(std::string& output, const std::string& separator=".", const bool trailing=true) const { d_name.toString(output, separator, trailing); } - std::string toLogString() const { return d_name.toLogString(); } + std::string toLogString() const; std::string toStringNoDot() const { return d_name.toStringNoDot(); } std::string toStringRootDot() const { return d_name.toStringRootDot(); } @@ -366,19 +377,31 @@ public: } void makeUsLowerCase() { d_name.makeUsLowerCase(); } bool empty() const { return d_name.empty(); } - void clear() { d_name.clear(); } + void clear() { d_name.clear(); d_variant.clear(); } void trimToLabels(unsigned int trim) { d_name.trimToLabels(trim); } - size_t hash(size_t init=0) const { return d_name.hash(init); } + size_t hash(size_t init=0) const; - bool operator<(const ZoneName& rhs) const { return d_name.operator<(rhs.d_name); } + bool operator<(const ZoneName& rhs) const; - bool canonCompare(const ZoneName& rhs) const { return d_name.canonCompare(rhs.d_name); } + bool canonCompare(const ZoneName& rhs) const; // Conversion from ZoneName to DNSName explicit operator const DNSName&() const { return d_name; } explicit operator DNSName&() { return d_name; } + + bool hasVariant() const { return !d_variant.empty(); } + std::string getVariant() const { return d_variant; } + void setVariant(std::string_view); + + // Search for a variant separator: mandatory (when variants are used) trailing + // dot followed by another dot and the variant name, and return the length of + // the zone name without its variant part, or npos if there is no variant + // present. + static std::string_view::size_type findVariantSeparator(std::string_view name); + private: DNSName d_name; + std::string d_variant{}; }; size_t hash_value(ZoneName const& zone); diff --git a/pdns/test-dnsname_cc.cc b/pdns/test-dnsname_cc.cc index c6af666b8c..a478e59bad 100644 --- a/pdns/test-dnsname_cc.cc +++ b/pdns/test-dnsname_cc.cc @@ -1032,4 +1032,34 @@ BOOST_AUTO_TEST_CASE(test_getcommonlabels) { BOOST_CHECK_EQUAL(name5.getCommonLabels(name1), DNSName()); } +#if defined(PDNS_AUTH) +BOOST_AUTO_TEST_CASE(test_variantnames) { + ZoneName zone1("..variant"); + ZoneName zone2("bug.less..variant"); + ZoneName zone3(R"(actually\..not.a.variant)"); + ZoneName zone4(R"(still\\\..not.a.variant)"); + ZoneName zone5(R"(anti-\\..variant)"); + ZoneName zone6(R"(sl\\\\\..a\\\..sh\...overflow)"); + + BOOST_CHECK(zone1.hasVariant()); + BOOST_CHECK(zone1.operator const DNSName&().isRoot()); + + BOOST_CHECK(zone2.hasVariant()); + BOOST_CHECK_EQUAL(zone2.operator const DNSName&().toString(), "bug.less."); + BOOST_CHECK_EQUAL(zone2.getVariant(), "variant"); + + BOOST_CHECK(!zone3.hasVariant()); + BOOST_CHECK(!zone4.hasVariant()); + + BOOST_CHECK(zone5.hasVariant()); + BOOST_CHECK_EQUAL(zone5.operator const DNSName&().toString(), R"(anti-\\.)"); + BOOST_CHECK_EQUAL(zone5.getVariant(), "variant"); + + BOOST_CHECK(zone6.hasVariant()); + BOOST_CHECK_EQUAL(zone6.getVariant(), "overflow"); + + BOOST_CHECK_THROW(ZoneName zone("variants.r.us..dot..dot...dot....dot.....dots"),std::out_of_range); +} +#endif + BOOST_AUTO_TEST_SUITE_END()