2
0
mirror of https://gitlab.isc.org/isc-projects/bind9 synced 2025-08-29 05:28:00 +00:00

Support for finding the longest parent domain in a qp-trie

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.
This commit is contained in:
Tony Finch 2023-02-10 16:53:31 +00:00
parent 8a3a216f40
commit fa1b57ee6e
3 changed files with 294 additions and 3 deletions

View File

@ -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);
/*%<

View 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);
}
/**********************************************************************/

View 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