]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add optional variants to ZoneName.
authorMiod Vallat <miod.vallat@powerdns.com>
Mon, 7 Apr 2025 09:55:21 +0000 (11:55 +0200)
committerMiod Vallat <miod.vallat@powerdns.com>
Mon, 26 May 2025 09:23:23 +0000 (11:23 +0200)
pdns/dnsname.cc
pdns/dnsname.hh
pdns/test-dnsname_cc.cc

index 4f735d8f08039006d19f65b20fa31218a6959ea3..147aa4cbd6af398e98bb46e187e89346d8977548 100644 (file)
@@ -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<const unsigned char *>(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 // ]
index 1c2e591bd21eb816a6a9b63866d94541e83485de..9339b9d8473765c5819d9da26b6f9ad358af48a6 100644 (file)
@@ -34,6 +34,8 @@
 #include <unordered_set>
 #include <string_view>
 
+using namespace std::string_view_literals;
+
 #include <boost/version.hpp>
 #include <boost/container/string.hpp>
 
@@ -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);
index c6af666b8c037d23c5115e0e8baa55b0d27d5bd2..a478e59bade78a03fad2cdaade6d17841a3771d1 100644 (file)
@@ -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()