]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Support for finding the longest parent domain in a qp-trie
authorTony Finch <fanf@isc.org>
Fri, 10 Feb 2023 16:53:31 +0000 (16:53 +0000)
committerTony Finch <fanf@isc.org>
Wed, 5 Apr 2023 11:35:04 +0000 (12:35 +0100)
This is the first of the "fancy" searches that know how the DNS
namespace maps on to the structure of a qp-trie. For example, it will
find the closest enclosing zone in the zone tree.

lib/dns/include/dns/qp.h
lib/dns/qp.c
tests/dns/qp_test.c

index 5befed68ca1aea3a47f26c497e4475f0efd54f3b..a860c5a901b494bc77f92d1acce740514e21ad66 100644 (file)
  *
  * Keys are `dns_qpkey_t`, which is a string-like thing, usually created
  * from a DNS name. You can use both relative and absolute DNS names as
- * keys.
+ * keys, even in the same trie, except for one caveat: if a trie contains
+ * names relative to the zone apex, the natural way to represent the apex
+ * itself (spelled `@` in zone files) is a zero-length name; but a
+ * zero-length name has the same qpkey representation as the root zone
+ * (apart from its length), so they collide.
  *
  * Leaf values are a pair of a `void *` pointer and a `uint32_t`
  * (because that is what fits inside an internal qp-trie leaf node).
@@ -259,6 +263,13 @@ typedef enum dns_qpgc {
        DNS_QPGC_ALL,
 } dns_qpgc_t;
 
+/*%
+ * Options for fancy searches such as `dns_qp_findname_parent()`
+ */
+typedef enum dns_qpfind {
+       DNS_QPFIND_NOEXACT = 1 << 0,
+} dns_qpfind_t;
+
 /***********************************************************************
  *
  *  functions - create, destory, enquire
@@ -395,8 +406,8 @@ dns_qpmulti_memusage(dns_qpmulti_t *multi);
 /*
  * XXXFANF todo, based on what we discover BIND needs
  *
- * fancy searches: longest match, lexicographic predecessor
- * (for NSEC), successor (for modification-safe iteration), etc.
+ * more fancy searches: lexicographic predecessor (for NSEC),
+ * successor (for modification-safe iteration), etc.
  *
  * do we need specific lookup functions to find out if the
  * returned value is readonly or mutable?
@@ -457,6 +468,30 @@ dns_qp_getname(dns_qpreadable_t qpr, const dns_name_t *name, void **pval_r,
  * \li  ISC_R_SUCCESS if the leaf was found
  */
 
+isc_result_t
+dns_qp_findname_parent(dns_qpreadable_t qpr, const dns_name_t *name,
+                      dns_qpfind_t options, void **pval_r, uint32_t *ival_r);
+/*%<
+ * Find a leaf in a qp-trie that is a parent domain of or equal to the
+ * given DNS name.
+ *
+ * If the DNS_QPFIND_NOEXACT option is set, find a strict parent
+ * domain not equal to the search name.
+ *
+ * The leaf values are assigned to `*pval_r` and `*ival_r`
+ *
+ * Requires:
+ * \li  `qpr` is a pointer to a readable qp-trie
+ * \li  `name` is a pointer to a valid `dns_name_t`
+ * \li  `pval_r != NULL`
+ * \li  `ival_r != NULL`
+ *
+ * Returns:
+ * \li  ISC_R_SUCCESS if an exact match was found
+ * \li  ISC_R_PARTIALMATCH if a parent domain was found
+ * \li  ISC_R_NOTFOUND if no match was found
+ */
+
 isc_result_t
 dns_qp_insert(dns_qp_t *qp, void *pval, uint32_t ival);
 /*%<
index 935dba21cf7817794ade9f9fc796afeb9bff6de2..c6bfa7495322dd0be41c29972232c8013957dd7f 100644 (file)
@@ -278,6 +278,25 @@ qpkey_compare(const dns_qpkey_t key_a, const size_t keylen_a,
        return (QPKEY_EQUAL);
 }
 
+/*
+ * Given a key constructed by dns_qpkey_fromname(), trim it down to the last
+ * label boundary before the `max` length.
+ *
+ * This is used when searching a trie for the best match for a name.
+ */
+static size_t
+qpkey_trim_label(dns_qpkey_t key, size_t len, size_t max) {
+       size_t stop = 0;
+       for (size_t offset = 0; offset < max; offset++) {
+               if (qpkey_bit(key, len, offset) == SHIFT_NOBYTE &&
+                   qpkey_bit(key, len, offset + 1) != SHIFT_NOBYTE)
+               {
+                       stop = offset + 1;
+               }
+       }
+       return (stop);
+}
+
 /***********************************************************************
  *
  *  allocator wrappers
@@ -1800,4 +1819,101 @@ dns_qp_getname(dns_qpreadable_t qpr, const dns_name_t *name, void **pval_r,
        return (dns_qp_getkey(qpr, key, keylen, pval_r, ival_r));
 }
 
+isc_result_t
+dns_qp_findname_parent(dns_qpreadable_t qpr, const dns_name_t *name,
+                      dns_qpfind_t options, void **pval_r, uint32_t *ival_r) {
+       dns_qpreader_t *qp = dns_qpreader(qpr);
+       dns_qpkey_t search, found;
+       size_t searchlen, foundlen;
+       size_t offset;
+       qp_shift_t bit;
+       qp_node_t *n, *twigs;
+       isc_result_t result;
+       unsigned int labels = 0;
+       struct offref {
+               uint32_t off;
+               qp_ref_t ref;
+       } label[DNS_NAME_MAXLABELS];
+
+       REQUIRE(QP_VALID(qp));
+       REQUIRE(pval_r != NULL);
+       REQUIRE(ival_r != NULL);
+
+       searchlen = dns_qpkey_fromname(search, name);
+       if ((options & DNS_QPFIND_NOEXACT) != 0) {
+               searchlen = qpkey_trim_label(search, searchlen, searchlen);
+               result = DNS_R_PARTIALMATCH;
+       } else {
+               result = ISC_R_SUCCESS;
+       }
+
+       n = get_root(qp);
+       if (n == NULL) {
+               return (ISC_R_NOTFOUND);
+       }
+
+       /*
+        * Like `dns_qp_insert()`, we must find a leaf. However, we don't make a
+        * second pass: instead, we keep track of any leaves with shorter keys
+        * that we discover along the way. (In general, qp-trie searches can be
+        * one-pass, by recording their traversal, or two-pass, for less stack
+        * memory usage.)
+        *
+        * A shorter key that can be a parent domain always has a leaf node at
+        * SHIFT_NOBYTE (indicating end of its key) where our search key has a
+        * normal character immediately after a label separator. Note 1: It is
+        * OK if `offset - 1` underflows: it will become SIZE_MAX, which is
+        * greater than `searchlen`, so `qpkey_bit()` will return SHIFT_NOBYTE,
+        * which is what we want when `offset == 0`. Note 2: Any SHIFT_NOBYTE
+        * twig is always `twigs[0]`.
+        */
+       while (is_branch(n)) {
+               prefetch_twigs(qp, n);
+               twigs = branch_twigs_vector(qp, n);
+               offset = branch_key_offset(n);
+               bit = qpkey_bit(search, searchlen, offset);
+               if (bit != SHIFT_NOBYTE && branch_has_twig(n, SHIFT_NOBYTE) &&
+                   qpkey_bit(search, searchlen, offset - 1) == SHIFT_NOBYTE &&
+                   !is_branch(&twigs[0]))
+               {
+                       label[labels].off = offset;
+                       label[labels].ref = branch_twigs_ref(n);
+                       labels++;
+                       INSIST(labels <= DNS_NAME_MAXLABELS);
+               }
+               if (branch_has_twig(n, bit)) {
+                       n = branch_twig_ptr(qp, n, bit);
+               } else if (labels == 0) {
+                       /* any twig will do */
+                       n = &twigs[0];
+               } else {
+                       n = ref_ptr(qp, label[labels - 1].ref);
+                       break;
+               }
+       }
+
+       /* do the keys differ, and if so, where? */
+       foundlen = leaf_qpkey(qp, n, found);
+       offset = qpkey_compare(search, searchlen, found, foundlen);
+
+       if (offset == QPKEY_EQUAL || offset == foundlen) {
+               *pval_r = leaf_pval(n);
+               *ival_r = leaf_ival(n);
+               if (offset == QPKEY_EQUAL) {
+                       return (result);
+               } else {
+                       return (DNS_R_PARTIALMATCH);
+               }
+       }
+       while (labels-- > 0) {
+               if (offset > label[labels].off) {
+                       n = ref_ptr(qp, label[labels].ref);
+                       *pval_r = leaf_pval(n);
+                       *ival_r = leaf_ival(n);
+                       return (DNS_R_PARTIALMATCH);
+               }
+       }
+       return (ISC_R_NOTFOUND);
+}
+
 /**********************************************************************/
index 3b096d28dbcc82a800e9010b0e400645241326c0..9391804b1589ffdf69671afbcca2627329dd9e68 100644 (file)
@@ -216,10 +216,150 @@ ISC_RUN_TEST_IMPL(qpiter) {
        dns_qp_destroy(&qp);
 }
 
+static uint32_t
+no_op(void *uctx, void *pval, uint32_t ival) {
+       UNUSED(uctx);
+       UNUSED(pval);
+       UNUSED(ival);
+       return (1);
+}
+
+static size_t
+qpkey_fromstring(dns_qpkey_t key, void *uctx, void *pval, uint32_t ival) {
+       dns_fixedname_t fixed;
+
+       UNUSED(uctx);
+       UNUSED(ival);
+       if (*(char *)pval == '\0') {
+               return (0);
+       }
+       dns_test_namefromstring(pval, &fixed);
+       return (dns_qpkey_fromname(key, dns_fixedname_name(&fixed)));
+}
+
+const struct dns_qpmethods string_methods = {
+       no_op,
+       no_op,
+       qpkey_fromstring,
+       getname,
+};
+
+struct check_partialmatch {
+       const char *query;
+       dns_qpfind_t options;
+       isc_result_t result;
+       const char *found;
+};
+
+static void
+check_partialmatch(dns_qp_t *qp, struct check_partialmatch check[]) {
+       for (int i = 0; check[i].query != NULL; i++) {
+               isc_result_t result;
+               dns_fixedname_t fixed;
+               dns_name_t *name = dns_fixedname_name(&fixed);
+               void *pval = NULL;
+               uint32_t ival;
+
+#if 0
+               fprintf(stderr, "%s %u %s %s\n", check[i].query,
+                       check[i].options, isc_result_totext(check[i].result),
+                       check[i].found);
+#endif
+               dns_test_namefromstring(check[i].query, &fixed);
+               result = dns_qp_findname_parent(qp, name, check[i].options,
+                                               &pval, &ival);
+               assert_int_equal(result, check[i].result);
+               if (check[i].found == NULL) {
+                       assert_null(pval);
+               } else {
+                       assert_string_equal(pval, check[i].found);
+               }
+       }
+}
+
+static void
+insert_str(dns_qp_t *qp, const char *str) {
+       isc_result_t result;
+       uintptr_t pval = (uintptr_t)str;
+       INSIST((pval & 3) == 0);
+       result = dns_qp_insert(qp, (void *)pval, 0);
+       assert_int_equal(result, ISC_R_SUCCESS);
+}
+
+ISC_RUN_TEST_IMPL(partialmatch) {
+       isc_result_t result;
+       dns_qp_t *qp = NULL;
+
+       dns_qp_create(mctx, &string_methods, NULL, &qp);
+
+       /*
+        * Fixed size strings [16] should ensure leaf-compatible alignment.
+        */
+       const char insert[][16] = {
+               "a.b.",      "b.",           "fo.bar.", "foo.bar.",
+               "fooo.bar.", "web.foo.bar.", ".",       "",
+       };
+
+       int i = 0;
+       while (insert[i][0] != '.') {
+               insert_str(qp, insert[i++]);
+       }
+
+       static struct check_partialmatch check1[] = {
+               { "a.b.", 0, ISC_R_SUCCESS, "a.b." },
+               { "a.b.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH, "b." },
+               { "b.c.", DNS_QPFIND_NOEXACT, ISC_R_NOTFOUND, NULL },
+               { "bar.", 0, ISC_R_NOTFOUND, NULL },
+               { "f.bar.", 0, ISC_R_NOTFOUND, NULL },
+               { "foo.bar.", 0, ISC_R_SUCCESS, "foo.bar." },
+               { "foo.bar.", DNS_QPFIND_NOEXACT, ISC_R_NOTFOUND, NULL },
+               { "foooo.bar.", 0, ISC_R_NOTFOUND, NULL },
+               { "w.foo.bar.", 0, DNS_R_PARTIALMATCH, "foo.bar." },
+               { "www.foo.bar.", 0, DNS_R_PARTIALMATCH, "foo.bar." },
+               { "web.foo.bar.", 0, ISC_R_SUCCESS, "web.foo.bar." },
+               { "webby.foo.bar.", 0, DNS_R_PARTIALMATCH, "foo.bar." },
+               { "my.web.foo.bar.", 0, DNS_R_PARTIALMATCH, "web.foo.bar." },
+               { "web.foo.bar.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH,
+                 "foo.bar." },
+               { "my.web.foo.bar.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH,
+                 "web.foo.bar." },
+               { NULL, 0, 0, NULL },
+       };
+       check_partialmatch(qp, check1);
+
+       /* what if the trie contains the root? */
+       INSIST(insert[i][0] == '.');
+       insert_str(qp, insert[i++]);
+
+       static struct check_partialmatch check2[] = {
+               { "b.c.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH, "." },
+               { "bar.", 0, DNS_R_PARTIALMATCH, "." },
+               { "foo.bar.", 0, ISC_R_SUCCESS, "foo.bar." },
+               { "foo.bar.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH, "." },
+               { NULL, 0, 0, NULL },
+       };
+       check_partialmatch(qp, check2);
+
+       /* what if entries in the trie are relative to the zone apex? */
+       dns_qpkey_t rootkey = { SHIFT_NOBYTE };
+       result = dns_qp_deletekey(qp, rootkey, 1);
+       assert_int_equal(result, ISC_R_SUCCESS);
+       INSIST(insert[i][0] == '\0');
+       insert_str(qp, insert[i++]);
+       check_partialmatch(qp, (struct check_partialmatch[]){
+                                      { "bar", 0, DNS_R_PARTIALMATCH, "" },
+                                      { "bar.", 0, DNS_R_PARTIALMATCH, "" },
+                                      { NULL, 0, 0, NULL },
+                              });
+
+       dns_qp_destroy(&qp);
+}
+
 ISC_TEST_LIST_START
 ISC_TEST_ENTRY(qpkey_name)
 ISC_TEST_ENTRY(qpkey_sort)
 ISC_TEST_ENTRY(qpiter)
+ISC_TEST_ENTRY(partialmatch)
 ISC_TEST_LIST_END
 
 ISC_TEST_MAIN