diff --git a/lib/dns/include/dns/qp.h b/lib/dns/include/dns/qp.h index 5befed68ca..a860c5a901 100644 --- a/lib/dns/include/dns/qp.h +++ b/lib/dns/include/dns/qp.h @@ -19,7 +19,11 @@ * * 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); /*%< diff --git a/lib/dns/qp.c b/lib/dns/qp.c index 935dba21cf..c6bfa74953 100644 --- a/lib/dns/qp.c +++ b/lib/dns/qp.c @@ -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); +} + /**********************************************************************/ diff --git a/tests/dns/qp_test.c b/tests/dns/qp_test.c index 3b096d28db..9391804b15 100644 --- a/tests/dns/qp_test.c +++ b/tests/dns/qp_test.c @@ -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