Commit e89aca4aa6 for bind
commit e89aca4aa6137bd8c3b65d64860a0f7423efcc57
Author: Alessio Podda <alessio@isc.org>
Date: Fri May 22 10:48:31 2026 +0200
Add dns_diff_appendlistminimal
Previously, the implementation of dns_diff_appendminimal could lead to
quadratic complexity when used in a loop. We introduce an
dns_diff_appendlistminimal method for diffs that uses an hashmap to
minimize the diff, leading to linear complexity.
Care has been taken to ensure that the order in the diff is not changed
by the minimization, as some operations are sensitive to the order of
the difftuples in the diff.
diff --git a/lib/dns/diff.c b/lib/dns/diff.c
index cfb7b14ab5..67b3bdc563 100644
--- a/lib/dns/diff.c
+++ b/lib/dns/diff.c
@@ -20,6 +20,8 @@
#include <isc/buffer.h>
#include <isc/file.h>
+#include <isc/hash.h>
+#include <isc/hashmap.h>
#include <isc/log.h>
#include <isc/mem.h>
#include <isc/result.h>
@@ -182,6 +184,15 @@ dns_diff_size(const dns_diff_t *diff) {
return diff->size;
}
+static bool
+minimal_match(void *node, const void *key) {
+ const dns_difftuple_t *a = node;
+ const dns_difftuple_t *b = key;
+
+ return dns_name_caseequal(&a->name, &b->name) &&
+ dns_rdata_compare(&a->rdata, &b->rdata) == 0 && a->ttl == b->ttl;
+}
+
/* XXX this is O(N) */
void
@@ -202,10 +213,7 @@ dns_diff_appendminimal(dns_diff_t *diff, dns_difftuple_t **tuplep) {
* error. We report it but try to continue anyway.
*/
ISC_LIST_FOREACH(diff->tuples, ot, link) {
- if (dns_name_caseequal(&ot->name, &(*tuplep)->name) &&
- dns_rdata_compare(&ot->rdata, &(*tuplep)->rdata) == 0 &&
- ot->ttl == (*tuplep)->ttl)
- {
+ if (minimal_match(ot, *tuplep)) {
ISC_LIST_UNLINK(diff->tuples, ot, link);
INSIST(diff->size > 0);
diff->size -= 1;
@@ -225,6 +233,86 @@ dns_diff_appendminimal(dns_diff_t *diff, dns_difftuple_t **tuplep) {
}
}
+static uint8_t
+minimal_hashbits(size_t count) {
+ uint8_t bits = 4;
+
+ /* Leave room for the hash map's 90% load threshold. */
+ while (bits < 24 && count > (((size_t)9 << bits) / 10)) {
+ bits++;
+ }
+
+ return bits;
+}
+
+static uint32_t
+minimal_hash(const dns_difftuple_t *tuple) {
+ isc_hash32_t hash;
+
+ isc_hash32_init(&hash);
+ dns_name_hash_ex(&hash, &tuple->name);
+ isc_hash32_hash(&hash, &tuple->rdata.rdclass,
+ sizeof(tuple->rdata.rdclass), true);
+ isc_hash32_hash(&hash, &tuple->rdata.type, sizeof(tuple->rdata.type),
+ true);
+ isc_hash32_hash(&hash, tuple->rdata.data, tuple->rdata.length, false);
+ isc_hash32_hash(&hash, &tuple->ttl, sizeof(tuple->ttl), true);
+
+ return isc_hash32_finalize(&hash);
+}
+
+void
+dns_diff_appendlistminimal(dns_diff_t *diff, dns_diff_t *source) {
+ isc_hashmap_t *index = NULL;
+ isc_result_t result;
+
+ REQUIRE(DNS_DIFF_VALID(diff));
+ REQUIRE(DNS_DIFF_VALID(source));
+ REQUIRE(diff != source);
+
+ dns_diff_appendlist(diff, source);
+
+ isc_hashmap_create(diff->mctx, minimal_hashbits(diff->size), &index);
+
+ ISC_LIST_FOREACH(diff->tuples, tuple, link) {
+ dns_difftuple_t *found = NULL;
+ uint32_t hashval = minimal_hash(tuple);
+
+ result = isc_hashmap_add(index, hashval, minimal_match, tuple,
+ tuple, (void **)&found);
+
+ switch (result) {
+ case ISC_R_SUCCESS:
+ break;
+
+ case ISC_R_EXISTS:
+ INSIST(found != NULL);
+
+ if (found->op == tuple->op) {
+ UNEXPECTED_ERROR("unexpected non-minimal diff");
+ dns_diff_unlink(diff, tuple);
+ dns_difftuple_free(&tuple);
+ } else {
+ result = isc_hashmap_delete(
+ index, hashval, minimal_match, found);
+ INSIST(result == ISC_R_SUCCESS);
+
+ dns_diff_unlink(diff, found);
+ dns_diff_unlink(diff, tuple);
+
+ dns_difftuple_free(&found);
+ dns_difftuple_free(&tuple);
+ }
+ break;
+
+ default:
+ UNREACHABLE();
+ }
+ }
+
+ isc_hashmap_destroy(&index);
+}
+
static void
getownercase(dns_rdataset_t *rdataset, dns_name_t *name) {
if (dns_rdataset_isassociated(rdataset)) {
diff --git a/lib/dns/include/dns/diff.h b/lib/dns/include/dns/diff.h
index 936fa1b28b..3349a868a3 100644
--- a/lib/dns/include/dns/diff.h
+++ b/lib/dns/include/dns/diff.h
@@ -255,6 +255,23 @@ dns_diff_appendminimal(dns_diff_t *diff, dns_difftuple_t **tuple);
*
*/
+void
+dns_diff_appendlistminimal(dns_diff_t *diff, dns_diff_t *source);
+/*%<
+ * Append all tuples from 'source' to 'diff', removing any duplicate
+ * or conflicting updates as needed to create a minimal diff.
+ *
+ * Requires:
+ *\li 'diff' is a valid diff.
+ *\li 'source' is a valid diff.
+ *
+ * Ensures:
+ *\li 'diff' is a minimal diff.
+ *\li 'source' is empty.
+ *\li The tuples from 'source' have been freed, or will be freed when
+ * 'diff' is cleared.
+ */
+
isc_result_t
dns_diff_sort(dns_diff_t *diff, dns_diff_compare_func *compare);
/*%<
diff --git a/lib/dns/update.c b/lib/dns/update.c
index e991263034..e859106020 100644
--- a/lib/dns/update.c
+++ b/lib/dns/update.c
@@ -1743,10 +1743,8 @@ next_state:
* have to regenerate the RRSIG NSECs for NSECs that were
* replaced with identical ones.
*/
- ISC_LIST_FOREACH(state->nsec_diff.tuples, t, link) {
- dns_diff_unlink(&state->nsec_diff, t);
- dns_diff_appendminimal(&state->nsec_mindiff, &t);
- }
+ dns_diff_appendlistminimal(&state->nsec_mindiff,
+ &state->nsec_diff);
update_log(log, zone, ISC_LOG_DEBUG(3),
"signing rebuilt NSEC chain");
@@ -1784,14 +1782,8 @@ next_state:
state->state = update_nsec3;
/* Record our changes for the journal. */
- ISC_LIST_FOREACH(state->sig_diff.tuples, t, link) {
- dns_diff_unlink(&state->sig_diff, t);
- dns_diff_appendminimal(diff, &t);
- }
- ISC_LIST_FOREACH(state->nsec_mindiff.tuples, t, link) {
- dns_diff_unlink(&state->nsec_mindiff, t);
- dns_diff_appendminimal(diff, &t);
- }
+ dns_diff_appendlistminimal(diff, &state->sig_diff);
+ dns_diff_appendlistminimal(diff, &state->nsec_mindiff);
INSIST(ISC_LIST_EMPTY(state->sig_diff.tuples));
INSIST(ISC_LIST_EMPTY(state->nsec_diff.tuples));
@@ -1915,10 +1907,8 @@ next_state:
* have to regenerate the RRSIG NSEC3s for NSEC3s that were
* replaced with identical ones.
*/
- ISC_LIST_FOREACH(state->nsec_diff.tuples, t, link) {
- dns_diff_unlink(&state->nsec_diff, t);
- dns_diff_appendminimal(&state->nsec_mindiff, &t);
- }
+ dns_diff_appendlistminimal(&state->nsec_mindiff,
+ &state->nsec_diff);
update_log(log, zone, ISC_LOG_DEBUG(3),
"signing rebuilt NSEC3 chain");
@@ -1953,14 +1943,8 @@ next_state:
dns_diff_appendlist(&state->nsec_mindiff, &state->work);
/* Record our changes for the journal. */
- ISC_LIST_FOREACH(state->sig_diff.tuples, t, link) {
- dns_diff_unlink(&state->sig_diff, t);
- dns_diff_appendminimal(diff, &t);
- }
- ISC_LIST_FOREACH(state->nsec_mindiff.tuples, t, link) {
- dns_diff_unlink(&state->nsec_mindiff, t);
- dns_diff_appendminimal(diff, &t);
- }
+ dns_diff_appendlistminimal(diff, &state->sig_diff);
+ dns_diff_appendlistminimal(diff, &state->nsec_mindiff);
INSIST(ISC_LIST_EMPTY(state->sig_diff.tuples));
INSIST(ISC_LIST_EMPTY(state->nsec_diff.tuples));
diff --git a/tests/bench/dns_diff_appendlistminimal.c b/tests/bench/dns_diff_appendlistminimal.c
new file mode 100644
index 0000000000..3a2500d2b8
--- /dev/null
+++ b/tests/bench/dns_diff_appendlistminimal.c
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * SPDX-License-Identifier: MPL-2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#include <errno.h>
+#include <inttypes.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <isc/buffer.h>
+#include <isc/file.h>
+#include <isc/hash.h>
+#include <isc/hashmap.h>
+#include <isc/lib.h>
+#include <isc/region.h>
+#include <isc/result.h>
+#include <isc/time.h>
+#include <isc/util.h>
+
+#include <dns/diff.h>
+#include <dns/fixedname.h>
+#include <dns/lib.h>
+#include <dns/name.h>
+#include <dns/rdataclass.h>
+#include <dns/rdatatype.h>
+
+static uint8_t
+minimal_hashbits(size_t count) {
+ uint8_t bits = 4;
+
+ while (bits < 24 && count > ((size_t)1 << bits)) {
+ bits++;
+ }
+
+ return bits;
+}
+
+static uint32_t
+minimal_hash(const dns_difftuple_t *tuple) {
+ isc_hash32_t hash;
+
+ isc_hash32_init(&hash);
+ dns_name_hash_ex(&hash, &tuple->name);
+ isc_hash32_hash(&hash, &tuple->rdata.rdclass,
+ sizeof(tuple->rdata.rdclass), true);
+ isc_hash32_hash(&hash, &tuple->rdata.type, sizeof(tuple->rdata.type),
+ true);
+ isc_hash32_hash(&hash, tuple->rdata.data, tuple->rdata.length, false);
+ isc_hash32_hash(&hash, &tuple->ttl, sizeof(tuple->ttl), true);
+
+ return isc_hash32_finalize(&hash);
+}
+
+static bool
+minimal_match(void *node, const void *key) {
+ const dns_difftuple_t *a = node;
+ const dns_difftuple_t *b = key;
+
+ return dns_name_caseequal(&a->name, &b->name) &&
+ dns_rdata_compare(&a->rdata, &b->rdata) == 0 && a->ttl == b->ttl;
+}
+
+/*
+ * This is a copy of dns_diff_appendlistminimal(), with an explicit size hint
+ * so that the benchmark can compare different hint strategies.
+ */
+static void
+appendlistminimal(dns_diff_t *diff, dns_diff_t *source, size_t size_hint) {
+ isc_hashmap_t *index = NULL;
+ isc_result_t result;
+
+ REQUIRE(DNS_DIFF_VALID(diff));
+ REQUIRE(DNS_DIFF_VALID(source));
+ REQUIRE(diff != source);
+
+ dns_diff_appendlist(diff, source);
+
+ isc_hashmap_create(diff->mctx, minimal_hashbits(size_hint), &index);
+
+ ISC_LIST_FOREACH(diff->tuples, tuple, link) {
+ dns_difftuple_t *found = NULL;
+ uint32_t hashval = minimal_hash(tuple);
+
+ result = isc_hashmap_add(index, hashval, minimal_match, tuple,
+ tuple, (void **)&found);
+
+ switch (result) {
+ case ISC_R_SUCCESS:
+ break;
+
+ case ISC_R_EXISTS:
+ INSIST(found != NULL);
+
+ if (found->op == tuple->op) {
+ UNEXPECTED_ERROR("unexpected non-minimal diff");
+ dns_diff_unlink(diff, tuple);
+ dns_difftuple_free(&tuple);
+ } else {
+ result = isc_hashmap_delete(
+ index, minimal_hash(found),
+ minimal_match, found);
+ INSIST(result == ISC_R_SUCCESS);
+
+ dns_diff_unlink(diff, found);
+ dns_diff_unlink(diff, tuple);
+
+ dns_difftuple_free(&found);
+ dns_difftuple_free(&tuple);
+ }
+ break;
+
+ default:
+ UNREACHABLE();
+ }
+ }
+
+ isc_hashmap_destroy(&index);
+}
+
+static void
+append_tuple(dns_diff_t *diff, const dns_name_t *name, uint32_t value) {
+ unsigned char data[4] = {
+ (unsigned char)(value >> 24),
+ (unsigned char)(value >> 16),
+ (unsigned char)(value >> 8),
+ (unsigned char)value,
+ };
+ isc_region_t region = { .base = data, .length = sizeof(data) };
+ dns_rdata_t rdata = DNS_RDATA_INIT;
+ dns_difftuple_t *tuple = NULL;
+
+ dns_rdata_fromregion(&rdata, dns_rdataclass_in, dns_rdatatype_a,
+ ®ion);
+ dns_difftuple_create(isc_g_mctx, DNS_DIFFOP_ADD, name, 300, &rdata,
+ &tuple);
+ dns_diff_append(diff, &tuple);
+}
+
+static void
+file_error(const char *filename, size_t line, const char *message) {
+ fprintf(stderr, "%s:%zu: %s\n", filename, line, message);
+ exit(EXIT_FAILURE);
+}
+
+static size_t
+load_tuples(const char *filename, size_t limit, dns_diff_t *diff,
+ dns_diff_t *source) {
+ isc_result_t result;
+ off_t fileoff;
+ FILE *fp = NULL;
+ char *filetext = NULL;
+ char *pos = NULL;
+ char *file_end = NULL;
+ size_t filesize;
+ size_t tuples = 0;
+
+ result = isc_file_getsize(filename, &fileoff);
+ if (result != ISC_R_SUCCESS) {
+ fprintf(stderr, "stat(%s): %s\n", filename,
+ isc_result_totext(result));
+ exit(EXIT_FAILURE);
+ }
+ filesize = (size_t)fileoff;
+
+ filetext = isc_mem_get(isc_g_mctx, filesize + 1);
+ fp = fopen(filename, "r");
+ if (fp == NULL || fread(filetext, 1, filesize, fp) < filesize) {
+ fprintf(stderr, "read(%s): %s\n", filename, strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+ fclose(fp);
+ filetext[filesize] = '\0';
+
+ pos = filetext;
+ file_end = pos + filesize;
+ while (pos < file_end && tuples < limit) {
+ char *domain = NULL;
+ char *newline = NULL;
+ size_t len;
+
+ pos += strspn(pos, "0123456789");
+ if (*pos++ != ',') {
+ file_error(filename, tuples + 1, "missing comma");
+ }
+
+ domain = pos;
+ pos += strcspn(pos, "\r\n");
+ newline = pos;
+ pos += strspn(pos, "\r\n");
+ len = (size_t)(newline - domain);
+ *newline = '\0';
+
+ dns_fixedname_t fixed;
+ dns_name_t *name = dns_fixedname_initname(&fixed);
+ isc_buffer_t buffer;
+ isc_buffer_init(&buffer, domain, len);
+ isc_buffer_add(&buffer, len);
+ result = dns_name_fromtext(name, &buffer, dns_rootname, 0);
+ if (result != ISC_R_SUCCESS) {
+ file_error(filename, tuples + 1,
+ isc_result_totext(result));
+ }
+
+ append_tuple((tuples & 1) == 0 ? diff : source, name,
+ (uint32_t)tuples);
+ tuples++;
+ }
+
+ isc_mem_put(isc_g_mctx, filetext, filesize + 1);
+ return tuples;
+}
+
+typedef enum {
+ hint_scaled_sum,
+ hint_simple_sum,
+ hint_zero,
+} hint_strategy_t;
+
+static void
+run_benchmark(const char *filename, size_t limit, hint_strategy_t strategy) {
+ dns_diff_t diff, source;
+ const char *label = NULL;
+ size_t size_hint;
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+
+ size_t tuple_count = load_tuples(filename, limit, &diff, &source);
+ size_t sum = diff.size + source.size;
+
+ switch (strategy) {
+ case hint_scaled_sum:
+ label = "sum * 10 / 9";
+ size_hint = sum * 10 / 9;
+ break;
+ case hint_simple_sum:
+ label = "simple sum";
+ size_hint = sum;
+ break;
+ case hint_zero:
+ label = "zero hint";
+ size_hint = 0;
+ break;
+ default:
+ UNREACHABLE();
+ }
+
+ isc_nanosecs_t start = isc_time_monotonic();
+ appendlistminimal(&diff, &source, size_hint);
+ isc_nanosecs_t elapsed = isc_time_monotonic() - start;
+
+ INSIST(dns_diff_size(&diff) == tuple_count);
+ INSIST(dns_diff_size(&source) == 0);
+
+ printf("%-14s %zu tuples (%u bits): %.6f seconds\n", label, tuple_count,
+ minimal_hashbits(size_hint), (double)elapsed / NS_PER_SEC);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+static size_t
+parse_limit(const char *arg) {
+ char *end = NULL;
+
+ errno = 0;
+ uintmax_t count = strtoumax(arg, &end, 10);
+ if (errno != 0 || end == arg || *end != '\0' || count > UINT32_MAX) {
+ fprintf(stderr, "invalid name limit: %s\n", arg);
+ exit(EXIT_FAILURE);
+ }
+
+ return (size_t)count;
+}
+
+int
+main(int argc, char **argv) {
+ if (argc < 2 || argc > 3) {
+ fprintf(stderr, "usage: %s <names.csv> [name-limit]\n",
+ argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ size_t limit = argc == 3 ? parse_limit(argv[2]) : UINT32_MAX;
+
+ run_benchmark(argv[1], limit, hint_scaled_sum);
+ run_benchmark(argv[1], limit, hint_simple_sum);
+ run_benchmark(argv[1], limit, hint_zero);
+
+ return EXIT_SUCCESS;
+}
diff --git a/tests/bench/meson.build b/tests/bench/meson.build
index ec1bf9dee9..e53e86e84e 100644
--- a/tests/bench/meson.build
+++ b/tests/bench/meson.build
@@ -12,6 +12,7 @@
foreach bench : [
'ascii',
'compress',
+ 'dns_diff_appendlistminimal',
'iterated_hash',
'load-names',
'qp-dump',
diff --git a/tests/dns/diff_test.c b/tests/dns/diff_test.c
index 38e6681394..83048ca4f9 100644
--- a/tests/dns/diff_test.c
+++ b/tests/dns/diff_test.c
@@ -58,17 +58,28 @@ count_elements(dns_diff_t *diff) {
}
static void
-prepare_rdata(dns_rdata_t *rdata, unsigned char *dest, size_t dest_size) {
- dns_rdataclass_t rdclass = dns_rdataclass_in;
- dns_rdatatype_t type = dns_rdatatype_wallet;
- const char text[] = "cid-example wid-example";
-
+prepare_rdata_text(dns_rdata_t *rdata, unsigned char *dest, size_t dest_size,
+ const char *text) {
*rdata = (dns_rdata_t)DNS_RDATA_INIT;
isc_result_t result = dns_test_rdatafromstring(
- rdata, rdclass, type, dest, dest_size, text, false);
+ rdata, dns_rdataclass_in, dns_rdatatype_wallet, dest, dest_size,
+ text, false);
INSIST(result == ISC_R_SUCCESS);
}
+static void
+prepare_rdata(dns_rdata_t *rdata, unsigned char *dest, size_t dest_size) {
+ prepare_rdata_text(rdata, dest, dest_size, "cid-example wid-example");
+}
+
+static void
+append_tuple(dns_diff_t *diff, dns_diffop_t op, const dns_name_t *name,
+ dns_rdata_t *rdata) {
+ dns_difftuple_t *tup = NULL;
+ dns_difftuple_create(isc_g_mctx, op, name, 1, rdata, &tup);
+ dns_diff_append(diff, &tup);
+}
+
ISC_RUN_TEST_IMPL(dns_diff_size) {
dns_diff_t diff;
dns_diff_init(isc_g_mctx, &diff);
@@ -132,8 +143,217 @@ ISC_RUN_TEST_IMPL(dns_diff_size) {
}
}
+/*
+ * Basic: source entries pass through into diff, and a source entry
+ * that matches a pre-existing diff entry with the opposite op cancels
+ * it (both are dropped).
+ */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_basic) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdatas[3] = { 0 };
+ unsigned char bufs[3][128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+ for (size_t i = 0; i < 3; i++) {
+ prepare_rdata(&rdatas[i], bufs[i], sizeof(bufs[i]));
+ }
+
+ /* diff: [ADD name_1] */
+ append_tuple(&diff, DNS_DIFFOP_ADD, &name_1, &rdatas[0]);
+
+ /* source: [DEL name_2, DEL name_1 (cancels), DEL name_3] */
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_2, &rdatas[1]);
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_1, &rdatas[0]);
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_3, &rdatas[2]);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&source) == 0);
+ assert_true(count_elements(&source) == 0);
+ assert_true(dns_diff_size(&diff) == 2);
+ assert_true(dns_diff_size(&diff) == count_elements(&diff));
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/* An empty source must leave a non-empty diff untouched. */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_empty_source) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdata = { 0 };
+ unsigned char buf[128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+ prepare_rdata(&rdata, buf, sizeof(buf));
+
+ append_tuple(&diff, DNS_DIFFOP_ADD, &name_1, &rdata);
+ append_tuple(&diff, DNS_DIFFOP_DEL, &name_2, &rdata);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 2);
+ assert_true(dns_diff_size(&diff) == count_elements(&diff));
+ assert_true(dns_diff_size(&source) == 0);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/* Appending into an empty diff must move all source entries over. */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_empty_diff) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdata = { 0 };
+ unsigned char buf[128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+ prepare_rdata(&rdata, buf, sizeof(buf));
+
+ append_tuple(&source, DNS_DIFFOP_ADD, &name_1, &rdata);
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_2, &rdata);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 2);
+ assert_true(dns_diff_size(&diff) == count_elements(&diff));
+ assert_true(dns_diff_size(&source) == 0);
+ assert_true(count_elements(&source) == 0);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/* Both empty: no-op. */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_both_empty) {
+ dns_diff_t diff, source;
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 0);
+ assert_true(dns_diff_size(&source) == 0);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/*
+ * Two entries within 'source' that cancel each other (neither
+ * pre-existing in 'diff') must both be dropped.
+ */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_source_cancel) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdatas[2] = { 0 };
+ unsigned char bufs[2][128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+ for (size_t i = 0; i < 2; i++) {
+ prepare_rdata(&rdatas[i], bufs[i], sizeof(bufs[i]));
+ }
+
+ append_tuple(&source, DNS_DIFFOP_ADD, &name_1, &rdatas[0]);
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_1, &rdatas[1]);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 0);
+ assert_true(count_elements(&diff) == 0);
+ assert_true(dns_diff_size(&source) == 0);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/* A non-minimal destination must be minimized along with the source. */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_nonminimal_diff) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdatas[3] = { 0 };
+ unsigned char bufs[3][128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+ for (size_t i = 0; i < 3; i++) {
+ prepare_rdata(&rdatas[i], bufs[i], sizeof(bufs[i]));
+ }
+
+ append_tuple(&diff, DNS_DIFFOP_ADD, &name_1, &rdatas[0]);
+ append_tuple(&diff, DNS_DIFFOP_DEL, &name_1, &rdatas[1]);
+ append_tuple(&source, DNS_DIFFOP_ADD, &name_2, &rdatas[2]);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 1);
+ assert_true(dns_diff_size(&diff) == count_elements(&diff));
+ assert_true(
+ dns_name_caseequal(&ISC_LIST_HEAD(diff.tuples)->name, &name_2));
+ assert_true(dns_diff_size(&source) == 0);
+ assert_true(count_elements(&source) == 0);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
+/*
+ * Ordering: original 'diff' entries keep their relative order and
+ * appear first; 'source' entries are appended in source order. Uses
+ * distinct rdata so nothing cancels.
+ */
+ISC_RUN_TEST_IMPL(dns_diff_appendlistminimal_order) {
+ dns_diff_t diff, source;
+ dns_rdata_t rdatas[5] = { 0 };
+ unsigned char bufs[5][128] = { 0 };
+
+ dns_diff_init(isc_g_mctx, &diff);
+ dns_diff_init(isc_g_mctx, &source);
+
+ prepare_rdata_text(&rdatas[0], bufs[0], sizeof(bufs[0]), "cid-1 wid-1");
+ prepare_rdata_text(&rdatas[1], bufs[1], sizeof(bufs[1]), "cid-2 wid-2");
+ prepare_rdata_text(&rdatas[2], bufs[2], sizeof(bufs[2]), "cid-3 wid-3");
+ prepare_rdata_text(&rdatas[3], bufs[3], sizeof(bufs[3]), "cid-4 wid-4");
+ prepare_rdata_text(&rdatas[4], bufs[4], sizeof(bufs[4]), "cid-5 wid-5");
+
+ append_tuple(&diff, DNS_DIFFOP_ADD, &name_1, &rdatas[0]);
+ append_tuple(&diff, DNS_DIFFOP_ADD, &name_2, &rdatas[1]);
+
+ append_tuple(&source, DNS_DIFFOP_ADD, &name_3, &rdatas[2]);
+ append_tuple(&source, DNS_DIFFOP_DEL, &name_1, &rdatas[3]);
+ append_tuple(&source, DNS_DIFFOP_ADD, &name_2, &rdatas[4]);
+
+ dns_diff_appendlistminimal(&diff, &source);
+
+ assert_true(dns_diff_size(&diff) == 5);
+ assert_true(dns_diff_size(&diff) == count_elements(&diff));
+
+ const dns_name_t *expected_names[] = { &name_1, &name_2, &name_3,
+ &name_1, &name_2 };
+ dns_diffop_t expected_ops[] = { DNS_DIFFOP_ADD, DNS_DIFFOP_ADD,
+ DNS_DIFFOP_ADD, DNS_DIFFOP_DEL,
+ DNS_DIFFOP_ADD };
+ size_t i = 0;
+ ISC_LIST_FOREACH(diff.tuples, t, link) {
+ assert_true(dns_name_caseequal(&t->name, expected_names[i]));
+ assert_true(t->op == expected_ops[i]);
+ i++;
+ }
+ assert_true(i == 5);
+
+ dns_diff_clear(&source);
+ dns_diff_clear(&diff);
+}
+
ISC_TEST_LIST_START
ISC_TEST_ENTRY(dns_diff_size)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_basic)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_empty_source)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_empty_diff)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_both_empty)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_source_cancel)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_nonminimal_diff)
+ISC_TEST_ENTRY(dns_diff_appendlistminimal_order)
ISC_TEST_LIST_END
ISC_TEST_MAIN