]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
add a node chain traversal mechanism
authorEvan Hunt <each@isc.org>
Sat, 9 Sep 2023 00:44:53 +0000 (17:44 -0700)
committerEvan Hunt <each@isc.org>
Thu, 28 Sep 2023 07:30:43 +0000 (00:30 -0700)
dns_qp_findname_ancestor() now takes an optional 'chain' parameter;
if set, the dns_qpchain object it points to will be updated with an
array of pointers to the populated nodes between the tree root and the
requested name. the number of nodes in the chain can then be accessed
using dns_qpchain_length() and the individual nodes using
dns_qpchain_node().

lib/dns/forward.c
lib/dns/include/dns/qp.h
lib/dns/keytable.c
lib/dns/nametree.c
lib/dns/nta.c
lib/dns/qp.c
lib/dns/qp_p.h
lib/dns/zt.c
tests/bench/qplookups.c
tests/dns/qp_test.c

index 1cc61115a872764e5bb6b6e7d5185e90bbbc3d82..d885df68b47ececc27a5bae070ecc12c6b856886 100644 (file)
@@ -168,7 +168,8 @@ dns_fwdtable_find(dns_fwdtable_t *fwdtable, const dns_name_t *name,
        REQUIRE(VALID_FWDTABLE(fwdtable));
 
        dns_qpmulti_query(fwdtable->table, &qpr);
-       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, &pval, NULL);
+       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, NULL, &pval,
+                                         NULL);
        if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
                dns_forwarders_t *fwdrs = pval;
                *forwardersp = fwdrs;
index f855f12908f1fdc5766e6383289e5cfb5e83875a..07850d176cbe759864b2be42707dfe0103080350 100644 (file)
@@ -86,6 +86,7 @@
 
 #include <isc/attributes.h>
 
+#include <dns/name.h>
 #include <dns/types.h>
 
 /*%
@@ -182,8 +183,9 @@ typedef union dns_qpreadable {
 typedef uint8_t dns_qpkey_t[DNS_QP_MAXKEY];
 
 /*%
- * A trie iterator describes a path through the trie from the root to
- * a leaf node, for use with `dns_qpiter_init()` and `dns_qpiter_next()`.
+ * A QP iterator traverses a trie starting with the root and passing
+ * though each leaf node in lexicographic order; it is used with
+ * `dns_qpiter_init()` and `dns_qpiter_next()`.
  */
 typedef struct dns_qpiter {
        unsigned int    magic;
@@ -195,6 +197,22 @@ typedef struct dns_qpiter {
        } stack[DNS_QP_MAXKEY];
 } dns_qpiter_t;
 
+/*%
+ * A QP chain holds references to each populated node between
+ * the root and a given leaf. It is set while running
+ * `dns_qp_findname_ancestor()`, and can optionally be passed
+ * back to the caller so that individual nodes can be accessed.
+ */
+typedef struct dns_qpchain {
+       unsigned int    magic;
+       dns_qpreader_t *qp;
+       uint8_t         len;
+       struct {
+               void  *node;
+               size_t offset;
+       } chain[DNS_NAME_MAXLABELS];
+} dns_qpchain_t;
+
 /*%
  * These leaf methods allow the qp-trie code to call back to the code
  * responsible for the leaf values that are stored in the trie. The
@@ -481,7 +499,7 @@ dns_qp_getname(dns_qpreadable_t qpr, const dns_name_t *name, void **pval_r,
 isc_result_t
 dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
                         dns_qpfind_t options, dns_name_t *foundname,
-                        void **pval_r, uint32_t *ival_r);
+                        dns_qpchain_t *chain, void **pval_r, uint32_t *ival_r);
 /*%<
  * Find a leaf in a qp-trie that is an ancestor domain of, or equal to, the
  * given DNS name.
@@ -491,6 +509,10 @@ dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
  *
  * If 'foundname' is not NULL, it is updated to contain the name found.
  *
+ * If 'chain' is not NULL, it is updated to contain a QP chain with
+ * references to the populated nodes in the tree between the root and
+ * the name found.
+ *
  * The leaf values are assigned to whichever of `*pval_r` and `*ival_r`
  * are not null, unless the return value is ISC_R_NOTFOUND.
  *
@@ -600,6 +622,39 @@ dns_qpiter_next(dns_qpiter_t *qpi, void **pval_r, uint32_t *ival_r);
  * \li  ISC_R_NOMORE otherwise
  */
 
+void
+dns_qpchain_init(dns_qpreadable_t qpr, dns_qpchain_t *chain);
+/*%<
+ * Initialize a QP chain.
+ *
+ * Requires:
+ * \li  `qpr` is a pointer to a valid qp-trie
+ * \li  `chain` is not NULL.
+ */
+
+unsigned int
+dns_qpchain_length(dns_qpchain_t *chain);
+/*%<
+ * Returns the length of a QP chain.
+ *
+ * Requires:
+ * \li  `chain` is a pointer to an initialized QP chain object.
+ */
+
+void
+dns_qpchain_node(dns_qpchain_t *chain, unsigned int level, dns_name_t *name,
+                void **pval_r, uint32_t *ival_r);
+/*%<
+ * Sets 'name' to the name of the leaf referenced at `chain->stack[level]`.
+ *
+ * The leaf values are assigned to whichever of `*pval_r` and `*ival_r`
+ * are not null.
+ *
+ * Requires:
+ * \li  `chain` is a pointer to an initialized QP chain object.
+ * \li  `level` is less than `chain->len`.
+ */
+
 /***********************************************************************
  *
  *  functions - transactions
index 0b243b3c5564f762fae9772872bd1577ebeed157..8d5b51e9b02673ef62395eb5d5d95d3d7d2166a5 100644 (file)
@@ -518,7 +518,8 @@ dns_keytable_finddeepestmatch(dns_keytable_t *keytable, const dns_name_t *name,
        REQUIRE(foundname != NULL);
 
        dns_qpmulti_query(keytable->table, &qpr);
-       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, &pval, NULL);
+       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, NULL, &pval,
+                                         NULL);
        keynode = pval;
 
        if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
@@ -547,7 +548,8 @@ dns_keytable_issecuredomain(dns_keytable_t *keytable, const dns_name_t *name,
        REQUIRE(wantdnssecp != NULL);
 
        dns_qpmulti_query(keytable->table, &qpr);
-       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, &pval, NULL);
+       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, NULL, &pval,
+                                         NULL);
        if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
                keynode = pval;
                if (foundname != NULL) {
index 0f906c58de8a58c3c78b108a9912fa0e5f7a74eb..0fd1110a16a7986395ec11ee4886f75c7fa5124b 100644 (file)
@@ -289,8 +289,8 @@ dns_nametree_covered(dns_nametree_t *nametree, const dns_name_t *name,
        REQUIRE(VALID_NAMETREE(nametree));
 
        dns_qpmulti_query(nametree->table, &qpr);
-       result = dns_qp_findname_ancestor(&qpr, name, 0, found, (void **)&node,
-                                         NULL);
+       result = dns_qp_findname_ancestor(&qpr, name, 0, found, NULL,
+                                         (void **)&node, NULL);
        if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
                switch (nametree->type) {
                case DNS_NAMETREE_BOOL:
index bcaf8eee573bc080dff03e6a0aaf05e573852738..0d5a90ac3400387ec29240171513960b1a097a1c 100644 (file)
@@ -413,7 +413,8 @@ dns_ntatable_covered(dns_ntatable_t *ntatable, isc_stdtime_t now,
 
        RWLOCK(&ntatable->rwlock, isc_rwlocktype_read);
        dns_qpmulti_query(ntatable->table, &qpr);
-       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, &pval, NULL);
+       result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, NULL, &pval,
+                                         NULL);
        nta = pval;
 
        switch (result) {
index b3fe7926bb1950c4400a356742cb923348dc3224..82bc399dac78bf691239f4caa2f10e0ea7486ff9 100644 (file)
@@ -1778,8 +1778,56 @@ dns_qp_deletename(dns_qp_t *qp, const dns_name_t *name, void **pval_r,
 }
 
 /***********************************************************************
- *
- *  iterate
+ *  chains
+ */
+static void
+maybe_set_name(dns_qpreader_t *qp, qp_node_t *node, dns_name_t *name) {
+       dns_qpkey_t key;
+       size_t len;
+
+       if (name == NULL) {
+               return;
+       }
+
+       len = leaf_qpkey(qp, node, key);
+       dns_qpkey_toname(key, len, name);
+}
+
+void
+dns_qpchain_init(dns_qpreadable_t qpr, dns_qpchain_t *chain) {
+       dns_qpreader_t *qp = dns_qpreader(qpr);
+       REQUIRE(QP_VALID(qp));
+       REQUIRE(chain != NULL);
+
+       *chain = (dns_qpchain_t){
+               .magic = QPCHAIN_MAGIC,
+               .qp = qp,
+       };
+}
+
+unsigned int
+dns_qpchain_length(dns_qpchain_t *chain) {
+       REQUIRE(QPCHAIN_VALID(chain));
+
+       return (chain->len);
+}
+
+void
+dns_qpchain_node(dns_qpchain_t *chain, unsigned int level, dns_name_t *name,
+                void **pval_r, uint32_t *ival_r) {
+       qp_node_t *node = NULL;
+
+       REQUIRE(QPCHAIN_VALID(chain));
+       REQUIRE(level < chain->len);
+
+       node = chain->chain[level].node;
+       maybe_set_name(chain->qp, node, name);
+       SET_IF_NOT_NULL(pval_r, leaf_pval(node));
+       SET_IF_NOT_NULL(ival_r, leaf_ival(node));
+}
+
+/***********************************************************************
+ *  iterators
  */
 
 void
@@ -1892,22 +1940,26 @@ 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));
 }
 
+static inline void
+add_link(dns_qpchain_t *chain, qp_node_t *node, size_t offset) {
+       chain->chain[chain->len].node = node;
+       chain->chain[chain->len].offset = offset;
+       chain->len++;
+       INSIST(chain->len <= DNS_NAME_MAXLABELS);
+}
+
 isc_result_t
 dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
                         dns_qpfind_t options, dns_name_t *foundname,
-                        void **pval_r, uint32_t *ival_r) {
+                        dns_qpchain_t *chain, 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 = NULL, *twigs = NULL;
+       qp_node_t *n = NULL;
        isc_result_t result;
-       unsigned int labels = 0;
-       struct offref {
-               uint32_t off;
-               qp_ref_t ref;
-       } label[DNS_NAME_MAXLABELS];
+       dns_qpchain_t oc;
 
        REQUIRE(QP_VALID(qp));
        REQUIRE(foundname == NULL || ISC_MAGIC_VALID(name, DNS_NAME_MAGIC));
@@ -1920,6 +1972,11 @@ dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
                result = ISC_R_SUCCESS;
        }
 
+       if (chain == NULL) {
+               chain = &oc;
+       }
+       dns_qpchain_init(qp, chain);
+
        n = get_root(qp);
        if (n == NULL) {
                return (ISC_R_NOTFOUND);
@@ -1931,37 +1988,52 @@ dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
         * 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);
+
+               qp_node_t *twigs = branch_twigs_vector(qp, n);
                offset = branch_key_offset(n);
-               bit = qpkey_bit(search, searchlen, offset);
+               qp_shift_t bit = qpkey_bit(search, searchlen, offset);
+
+               /*
+                * 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 `off - 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 `off == 0`.
+                *
+                * Note 2: Any SHIFT_NOBYTE twig is always `twigs[0]`.
+                */
                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);
+                       add_link(chain, twigs, offset);
                }
                if (branch_has_twig(n, bit)) {
+                       /* we have the twig we wanted */
                        n = branch_twig_ptr(qp, n, bit);
-               } else if (labels == 0) {
-                       /* any twig will do */
+               } else if (chain->len == 0) {
+                       /*
+                        * the twig we're looking for isn't here,
+                        * and we haven't found an ancestor yet either.
+                        * continue the search with whatever this branch's
+                        * first twig is.
+                        */
                        n = &twigs[0];
                } else {
-                       n = ref_ptr(qp, label[labels - 1].ref);
-                       break;
+                       /*
+                        * this branch is a dead end, but we do have
+                        * a previous leaf node saved in the chain.
+                        * we go back to that. it's a leaf, so we'll
+                        * fall out of the loop here.
+                        */
+                       n = chain->chain[chain->len - 1].node;
                }
        }
 
@@ -1972,27 +2044,41 @@ dns_qp_findname_ancestor(dns_qpreadable_t qpr, const dns_name_t *name,
        if (offset == QPKEY_EQUAL || offset == foundlen) {
                SET_IF_NOT_NULL(pval_r, leaf_pval(n));
                SET_IF_NOT_NULL(ival_r, leaf_ival(n));
-               if (foundname != NULL) {
-                       dns_qpkey_toname(found, foundlen, foundname);
-               }
+               maybe_set_name(qp, n, foundname);
                if (offset == QPKEY_EQUAL) {
+                       if ((options & DNS_QPFIND_NOEXACT) == 0) {
+                               /* add the exact match to the chain */
+                               add_link(chain, n, offset);
+                       }
                        return (result);
                } else {
                        return (DNS_R_PARTIALMATCH);
                }
        }
-       while (labels-- > 0) {
-               if (offset > label[labels].off) {
-                       n = ref_ptr(qp, label[labels].ref);
+
+       /*
+        * the requested name was not found, but if an ancestor
+        * was, we can retrieve that from the chain.
+        */
+       int len = chain->len;
+       while (len-- > 0) {
+               if (offset >= chain->chain[len].offset) {
+                       n = chain->chain[len].node;
                        SET_IF_NOT_NULL(pval_r, leaf_pval(n));
                        SET_IF_NOT_NULL(ival_r, leaf_ival(n));
-                       if (foundname != NULL) {
-                               foundlen = leaf_qpkey(qp, n, found);
-                               dns_qpkey_toname(found, foundlen, foundname);
-                       }
+                       maybe_set_name(qp, n, foundname);
                        return (DNS_R_PARTIALMATCH);
+               } else {
+                       /*
+                        * oops, during the search we found and added
+                        * a leaf that's longer than the requested
+                        * name; remove it from the chain.
+                        */
+                       chain->len--;
                }
        }
+
+       /* nothing was found at all */
        return (ISC_R_NOTFOUND);
 }
 
index c0f69c35686e1a5966f5ea86e7ad84fcee102ad1..2a92227d9ba47172bd4ad9836ba35c07d15243df 100644 (file)
@@ -393,6 +393,7 @@ ref_ptr(dns_qpreadable_t qpr, qp_ref_t ref) {
 
 #define QP_MAGIC       ISC_MAGIC('t', 'r', 'i', 'e')
 #define QPITER_MAGIC   ISC_MAGIC('q', 'p', 'i', 't')
+#define QPCHAIN_MAGIC  ISC_MAGIC('q', 'p', 'c', 'h')
 #define QPMULTI_MAGIC  ISC_MAGIC('q', 'p', 'm', 'v')
 #define QPREADER_MAGIC ISC_MAGIC('q', 'p', 'r', 'x')
 #define QPBASE_MAGIC   ISC_MAGIC('q', 'p', 'b', 'p')
@@ -400,6 +401,7 @@ ref_ptr(dns_qpreadable_t qpr, qp_ref_t ref) {
 
 #define QP_VALID(qp)     ISC_MAGIC_VALID(qp, QP_MAGIC)
 #define QPITER_VALID(qp)  ISC_MAGIC_VALID(qp, QPITER_MAGIC)
+#define QPCHAIN_VALID(qp) ISC_MAGIC_VALID(qp, QPCHAIN_MAGIC)
 #define QPMULTI_VALID(qp) ISC_MAGIC_VALID(qp, QPMULTI_MAGIC)
 #define QPBASE_VALID(qp)  ISC_MAGIC_VALID(qp, QPBASE_MAGIC)
 #define QPRCU_VALID(qp)          ISC_MAGIC_VALID(qp, QPRCU_MAGIC)
index a65d527b4244659dc682226684acede0f474653a..2f21896015876ec3ad251a2b97f35a2563b705f1 100644 (file)
@@ -180,11 +180,12 @@ dns_zt_find(dns_zt_t *zt, const dns_name_t *name, dns_ztfind_t options,
        if (exactopts == DNS_ZTFIND_EXACT) {
                result = dns_qp_getname(&qpr, name, &pval, NULL);
        } else if (exactopts == DNS_ZTFIND_NOEXACT) {
-               result = dns_qp_findname_ancestor(
-                       &qpr, name, DNS_QPFIND_NOEXACT, NULL, &pval, NULL);
+               result = dns_qp_findname_ancestor(&qpr, name,
+                                                 DNS_QPFIND_NOEXACT, NULL,
+                                                 NULL, &pval, NULL);
        } else {
-               result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, &pval,
-                                                 NULL);
+               result = dns_qp_findname_ancestor(&qpr, name, 0, NULL, NULL,
+                                                 &pval, NULL);
        }
        dns_qpread_destroy(zt->multi, &qpr);
 
index 71564d6cab3594fe721b7ca91fc314f205369066..c269edb750c2f752f944982a099c76fd45969512 100644 (file)
@@ -254,7 +254,7 @@ main(int argc, char **argv) {
        start = isc_time_monotonic();
        for (i = 0; i < n; i++) {
                name = dns_fixedname_name(&items[i]);
-               dns_qp_findname_ancestor(qp, name, 0, NULL, NULL, NULL);
+               dns_qp_findname_ancestor(qp, name, 0, NULL, NULL, NULL, NULL);
        }
        stop = isc_time_monotonic();
 
@@ -280,7 +280,7 @@ main(int argc, char **argv) {
                        ++search->ndata[1];
                }
 
-               dns_qp_findname_ancestor(qp, search, 0, NULL, NULL, NULL);
+               dns_qp_findname_ancestor(qp, search, 0, NULL, NULL, NULL, NULL);
        }
        stop = isc_time_monotonic();
 
index 80d076a4a38d2705f77c5d6941f3d58049c5d9ce..a456e5e639a1c0ca40bcda8522a1ce104d736132 100644 (file)
@@ -286,7 +286,7 @@ check_partialmatch(dns_qp_t *qp, struct check_partialmatch check[]) {
 
                dns_test_namefromstring(check[i].query, &fn1);
                result = dns_qp_findname_ancestor(qp, name, check[i].options,
-                                                 foundname, &pval, NULL);
+                                                 foundname, NULL, &pval, NULL);
 
 #if 0
                fprintf(stderr, "%s (flags %u) %s (expected %s) "
@@ -432,11 +432,108 @@ ISC_RUN_TEST_IMPL(partialmatch) {
        dns_qp_destroy(&qp);
 }
 
+struct check_qpchain {
+       const char *query;
+       dns_qpfind_t options;
+       isc_result_t result;
+       unsigned int length;
+       const char *names[10];
+};
+
+static void
+check_qpchain(dns_qp_t *qp, struct check_qpchain check[]) {
+       for (int i = 0; check[i].query != NULL; i++) {
+               isc_result_t result;
+               dns_fixedname_t fn1;
+               dns_name_t *name = dns_fixedname_initname(&fn1);
+               dns_qpchain_t chain;
+
+               dns_qpchain_init(qp, &chain);
+               dns_test_namefromstring(check[i].query, &fn1);
+               result = dns_qp_findname_ancestor(qp, name, check[i].options,
+                                                 NULL, &chain, NULL, NULL);
+
+#if 0
+               fprintf(stderr, "%s (%d) %s (expected %s), "
+                       "len %d (expected %d)\n",
+                       check[i].query, check[i].options,
+                       isc_result_totext(result),
+                       isc_result_totext(check[i].result),
+                       dns_qpchain_length(&chain), check[i].length);
+#endif
+               assert_int_equal(result, check[i].result);
+               assert_int_equal(dns_qpchain_length(&chain), check[i].length);
+               for (unsigned int j = 0; j < check[i].length; j++) {
+                       dns_fixedname_t fn2, fn3;
+                       dns_name_t *expected = dns_fixedname_initname(&fn2);
+                       dns_name_t *found = dns_fixedname_initname(&fn3);
+
+                       dns_test_namefromstring(check[i].names[j], &fn2);
+                       dns_qpchain_node(&chain, j, found, NULL, NULL);
+#if 0
+                       char nb[DNS_NAME_FORMATSIZE];
+                       dns_name_format(found, nb, sizeof(nb));
+                       fprintf(stderr, "got %s, expected %s\n", nb,
+                               check[i].names[j]);
+#endif
+                       assert_true(dns_name_equal(found, expected));
+               }
+       }
+}
+
+ISC_RUN_TEST_IMPL(qpchain) {
+       dns_qp_t *qp = NULL;
+       int i = 0;
+
+       dns_qp_create(mctx, &string_methods, NULL, &qp);
+
+       /*
+        * Fixed size strings [16] should ensure leaf-compatible alignment.
+        */
+       const char insert[][16] = { ".",          "a.",     "b.",   "c.b.a.",
+                                   "e.d.c.b.a.", "c.b.b.", "c.d.", "a.b.c.d.",
+                                   "a.b.c.d.e.", "" };
+
+       while (insert[i][0] != '\0') {
+               insert_str(qp, insert[i++]);
+       }
+
+       static struct check_qpchain check1[] = {
+               { "b.", 0, ISC_R_SUCCESS, 2 },
+               { "c.", 0, DNS_R_PARTIALMATCH, 1 },
+               { "e.d.c.b.a.", 0, ISC_R_SUCCESS, 4 },
+               { "a.b.c.d.", 0, ISC_R_SUCCESS, 3 },
+               { "a.b.c.d.", DNS_QPFIND_NOEXACT, DNS_R_PARTIALMATCH, 2 },
+               { NULL, 0, 0, 0 },
+       };
+
+       check1[0].names[0] = ".";
+       check1[0].names[1] = "b.";
+
+       check1[1].names[0] = ".";
+
+       check1[2].names[0] = ".";
+       check1[2].names[1] = "a.";
+       check1[2].names[2] = "c.b.a.";
+       check1[2].names[3] = "e.d.c.b.a.";
+
+       check1[3].names[0] = ".";
+       check1[3].names[1] = "c.d.";
+       check1[3].names[2] = "a.b.c.d.";
+
+       check1[4].names[0] = ".";
+       check1[4].names[1] = "c.d.";
+
+       check_qpchain(qp, check1);
+       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_ENTRY(qpchain)
 ISC_TEST_LIST_END
 
 ISC_TEST_MAIN