Commit 6a80b94b04 for bind

commit 6a80b94b041a92a213db1a4275df94ff86f6d9ca
Author: Alessio Podda <alessio@isc.org>
Date:   Fri Sep 25 12:55:43 2026 +0200

    Add hypothesis tests

    Hypothesis tests for vecheader operations.

diff --git a/tests/hypothesis/meson.build b/tests/hypothesis/meson.build
index d37982152e..d02b7b703e 100644
--- a/tests/hypothesis/meson.build
+++ b/tests/hypothesis/meson.build
@@ -55,3 +55,51 @@ test(
     timeout: 300,
     workdir: meson.current_build_dir(),
 )
+
+vec_proto_c = custom_target(
+    'vecheader-protobuf-c',
+    input: 'vecheader.proto',
+    output: ['vecheader.pb-c.c', 'vecheader.pb-c.h'],
+    command: [
+        hypothesis_protoc_c,
+        '--proto_path=@CURRENT_SOURCE_DIR@',
+        '--c_out=@OUTDIR@',
+        '@INPUT@',
+    ],
+)
+vec_proto_py = custom_target(
+    'vecheader-protobuf-python',
+    input: 'vecheader.proto',
+    output: 'vecheader_pb2.py',
+    command: [
+        hypothesis_protoc,
+        '--proto_path=@CURRENT_SOURCE_DIR@',
+        '--python_out=@OUTDIR@',
+        '@INPUT@',
+    ],
+)
+vec_server = executable(
+    'vecheader_server',
+    'vecheader_server.c',
+    vec_proto_c,
+    default_sanitize_options,
+    include_directories: test_inc,
+    dependencies: [libdns_dep, libisc_dep, hypothesis_proto_dep],
+    build_by_default: false,
+    install: false,
+)
+vec_env = environment(test_env_vars)
+vec_env.set('VECHEADER_SERVER', vec_server.full_path())
+vec_env.set('PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION', 'python')
+vec_env.prepend('PYTHONPATH', meson.current_build_dir())
+vec_env.append('PYTHONPATH', meson.project_source_root() / 'bin/tests/system')
+test(
+    'vecheader-hypothesis',
+    python,
+    args: files('vecheader.py'),
+    depends: [vec_server, vec_proto_py],
+    env: vec_env,
+    suite: ['dns', 'hypothesis'],
+    timeout: 300,
+    workdir: meson.current_build_dir(),
+)
diff --git a/tests/hypothesis/vecheader.proto b/tests/hypothesis/vecheader.proto
new file mode 100644
index 0000000000..ef61302f46
--- /dev/null
+++ b/tests/hypothesis/vecheader.proto
@@ -0,0 +1,76 @@
+// Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+// SPDX-License-Identifier: MPL-2.0
+syntax = "proto2";
+package vec;
+
+message Header {
+  repeated bytes records = 1;
+  repeated bool offline = 2;
+  optional bytes owner = 3;
+  optional uint32 ttl = 4 [default = 300];
+  optional uint32 trust = 5 [default = 0];
+  optional uint32 serial = 6 [default = 0];
+  optional bool resign = 7 [default = false];
+  optional int64 resign_time = 8 [default = 0];
+}
+message Construct {
+  required uint32 type = 1;
+  required Header header = 2;
+  optional uint32 limit = 3 [default = 0];
+}
+message Merge {
+  required uint32 type = 1;
+  required Header left = 2;
+  required Header right = 3;
+  optional uint32 flags = 4 [default = 0];
+  optional uint32 limit = 5 [default = 0];
+}
+message Subtract {
+  required uint32 type = 1;
+  required Header left = 2;
+  required Header right = 3;
+  optional uint32 flags = 4 [default = 0];
+}
+message Copy {
+  required uint32 type = 1;
+  required Header header = 2;
+}
+message SetOwnerCase {
+  required uint32 type = 1;
+  required Header header = 2;
+  required bytes owner = 3;
+}
+message Request {
+  oneof command {
+    Construct construct = 1;
+    Merge merge = 2;
+    Subtract subtract = 3;
+    Copy copy = 4;
+    SetOwnerCase set_owner_case = 5;
+  }
+}
+enum Result {
+  SUCCESS = 0;
+  TOO_MANY_RECORDS = 1;
+  NOT_EXACT = 2;
+  UNCHANGED = 3;
+  SINGLETON = 4;
+  NXRRSET = 5;
+  NO_SPACE = 6;
+  FAILURE = 7;
+}
+
+message Response {
+  required Result result = 1;
+  optional Header header = 2;
+  optional uint32 count = 3;
+  optional uint32 raw_length = 4;
+  optional uint32 size = 5;
+  optional uint32 header_size = 6;
+  optional uint32 type = 7;
+  optional bool case_set = 8;
+  optional bool case_lower = 9;
+  optional bytes bitmap = 10;
+  optional bytes applied_owner = 11;
+  optional uint32 covers = 12;
+}
diff --git a/tests/hypothesis/vecheader.py b/tests/hypothesis/vecheader.py
new file mode 100644
index 0000000000..b290b30187
--- /dev/null
+++ b/tests/hypothesis/vecheader.py
@@ -0,0 +1,378 @@
+# 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.
+
+"""
+Model tests for the real C vecheader implementation.
+
+Run through Meson: meson test -C builddir --suite hypothesis --print-errorlogs
+Each protobuf request is independent; only the adapter process is reused.
+"""
+
+import os
+import struct
+import sys
+
+from isc_hypothesis import RestartingProtobufServer
+
+try:
+    from hypothesis import example, given, settings
+    from hypothesis import strategies as st
+
+    import dns.rdatatype
+
+    from isctest.hypothesis.strategies import dns_names
+except ModuleNotFoundError as error:
+    print(f"skipping vecheader hypothesis tests: {error}", file=sys.stderr)
+    sys.exit(77)
+
+import vecheader_pb2 as proto  # pylint: disable=import-error,wrong-import-position
+
+OPAQUE_TYPE = dns.rdatatype.RdataType(65280)
+
+settings.register_profile(
+    "vecheader-ci", max_examples=300, deadline=None, derandomize=True
+)
+settings.register_profile("extended", max_examples=3000, deadline=None)
+
+settings.load_profile(os.environ.get("HYPOTHESIS_PROFILE", "vecheader-ci"))
+
+server = None
+
+
+@st.composite
+def owners(draw):
+    if draw(st.booleans()):
+        return None
+    name = draw(dns_names()).to_wire()
+    return name.lower() if draw(st.booleans()) else name
+
+
+@st.composite
+def headers(draw, records):
+    values = draw(st.lists(st.sampled_from(records), max_size=15))
+    header = proto.Header(
+        records=values,
+        # A record's flag is deterministic across duplicates within an input;
+        # differing flags across operands still exercise left-wins merging.
+        offline=[False] * len(values),
+        ttl=draw(st.integers(0, 2**32 - 1)),
+        trust=draw(st.integers(0, 10)),
+        serial=draw(st.integers(0, 2**32 - 1)),
+        resign=draw(st.booleans()),
+        resign_time=draw(st.integers(0, 2**40)),
+    )
+    owner = draw(owners())
+    if owner is not None:
+        header.owner = owner
+    return header
+
+
+@st.composite
+def cases(draw):
+    rrtype = draw(
+        st.sampled_from(
+            [
+                dns.rdatatype.A,
+                dns.rdatatype.AAAA,
+                dns.rdatatype.TXT,
+                dns.rdatatype.RRSIG,
+                dns.rdatatype.CNAME,
+                OPAQUE_TYPE,
+            ]
+        )
+    )
+    tokens = draw(st.lists(st.integers(0, 15), min_size=1, max_size=8, unique=True))
+
+    def wire(token):
+        if rrtype == dns.rdatatype.A:
+            return bytes([192, 0, 2, token])
+        if rrtype == dns.rdatatype.AAAA:
+            return bytes(15) + bytes([token])
+        if rrtype == dns.rdatatype.TXT:
+            return bytes([token]) + bytes([token]) * token
+        if rrtype == dns.rdatatype.CNAME:
+            return b"\x01" + bytes([97 + token]) + b"\x00"
+        if rrtype == dns.rdatatype.RRSIG:
+            # A/algorithm/labels/TTL/expiration/inception/key tag/root signer.
+            return (
+                struct.pack("!HBBIIIH", dns.rdatatype.A, 8, 0, 300, 1000, 0, token)
+                + b"\x00sig"
+            )
+        return bytes([token]) * token
+
+    pool = [wire(token) for token in tokens]
+    left = draw(headers(pool))
+    right = draw(headers(pool))
+    if rrtype == dns.rdatatype.CNAME:
+        # Valid singleton operands; union can still violate singleton rules.
+        del left.records[1:]
+        del left.offline[1:]
+        del right.records[1:]
+        del right.offline[1:]
+    if rrtype == dns.rdatatype.RRSIG:
+        for header in (left, right):
+            flag = draw(st.booleans())
+            header.offline[:] = [flag] * len(header.records)
+    count = len(set(left.records) | set(right.records))
+    limit = draw(st.sampled_from(sorted({0, 1, max(1, count - 1), count, count + 1})))
+    return rrtype, left, right, limit
+
+
+def contents(header):
+    return dict(zip(header.records, header.offline))
+
+
+def check_header(response, expected, metadata, owner, rrtype):
+    assert response.HasField("header")
+    actual = response.header
+    # For these generated types, canonical DNSSEC order is wire byte order.
+    assert list(actual.records) == sorted(expected)
+    assert list(actual.offline) == [expected[key] for key in sorted(expected)]
+    assert response.count == len(expected)
+    assert response.raw_length == sum(
+        2 + len(key) + (rrtype == dns.rdatatype.RRSIG) for key in expected
+    )
+    assert response.size == response.header_size + response.raw_length
+    assert response.type == rrtype
+    assert response.covers == (
+        dns.rdatatype.A if rrtype == dns.rdatatype.RRSIG else dns.rdatatype.NONE
+    )
+    for field in ("ttl", "trust", "serial", "resign", "resign_time"):
+        assert getattr(actual, field) == getattr(metadata, field), field
+    name = owner.owner if owner.HasField("owner") else None
+    bitmap = bytearray(32)
+    if name is not None:
+        for index, byte in enumerate(name):
+            if 65 <= byte <= 90:
+                bitmap[index // 8] |= 1 << (index % 8)
+    assert response.case_set == (name is not None)
+    assert response.case_lower == (name is not None and name == name.lower())
+    assert response.bitmap == bytes(bitmap)
+    if name is not None:
+        assert response.applied_owner == name
+
+
+@given(cases(), st.integers(0, 3))
+def test_merge(case, flags):
+    rrtype, left, right, limit = case
+    a, b = contents(left), contents(right)
+    expected = b | a  # Old RDATA wins on equality, including offline metadata.
+    if limit and len(expected) > limit:
+        result = proto.TOO_MANY_RECORDS
+    elif flags & 2 and a.keys() & b.keys():
+        result = proto.NOT_EXACT
+    elif b.keys() <= a.keys() and not flags & 1:
+        result = proto.UNCHANGED
+    elif rrtype == dns.rdatatype.CNAME and len(expected) > 1:
+        result = proto.SINGLETON
+    else:
+        result = proto.SUCCESS
+    response = server.command(
+        proto.Request(
+            merge=proto.Merge(
+                type=rrtype, left=left, right=right, flags=flags, limit=limit
+            )
+        )
+    )
+    assert response.result == result
+    if result == proto.SUCCESS:
+        check_header(response, expected, right, left, rrtype)
+    else:
+        assert not response.HasField("header")
+
+
+@given(cases(), st.booleans())
+def test_subtract(case, exact):
+    rrtype, left, right, _ = case
+    a, b = contents(left), contents(right)
+    expected = {key: flag for key, flag in a.items() if key not in b}
+    if exact and not b.keys() <= a.keys():
+        result = proto.NOT_EXACT
+    elif not expected:
+        result = proto.NXRRSET
+    elif expected == a:
+        result = proto.UNCHANGED
+    else:
+        result = proto.SUCCESS
+    response = server.command(
+        proto.Request(
+            subtract=proto.Subtract(
+                type=rrtype, left=left, right=right, flags=2 if exact else 0
+            )
+        )
+    )
+    assert response.result == result
+    if result == proto.SUCCESS:
+        check_header(response, expected, left, left, rrtype)
+    else:
+        assert not response.HasField("header")
+
+
+@given(cases())
+def test_construction(case):
+    rrtype, left, _, _ = case
+    response = server.command(
+        proto.Request(construct=proto.Construct(type=rrtype, header=left))
+    )
+    assert response.result == proto.SUCCESS
+    check_header(response, contents(left), left, left, rrtype)
+
+
+@given(st.integers(65507, 65515))
+@example(65510)
+@example(65511)
+def test_size_boundary(length):
+    # Opaque RDATA avoids type-specific parsing; two length bytes count too.
+    header = proto.Header(records=[bytes(length)], offline=[False])
+    response = server.command(
+        proto.Request(construct=proto.Construct(type=OPAQUE_TYPE, header=header))
+    )
+    expected = proto.SUCCESS if length + 2 <= 65512 else proto.NO_SPACE
+    assert response.result == expected
+    if expected == proto.SUCCESS:
+        check_header(response, contents(header), header, header, OPAQUE_TYPE)
+
+
+@given(st.integers(32750, 32760))
+def test_merge_size_boundary(length):
+    left = proto.Header(records=[b"a" * length], offline=[False])
+    right = proto.Header(records=[b"b" * length], offline=[False])
+    response = server.command(
+        proto.Request(merge=proto.Merge(type=OPAQUE_TYPE, left=left, right=right))
+    )
+    expected = proto.SUCCESS if 2 * (length + 2) <= 65512 else proto.NO_SPACE
+    assert response.result == expected
+    if expected == proto.SUCCESS:
+        check_header(
+            response, contents(left) | contents(right), right, left, OPAQUE_TYPE
+        )
+
+
+@given(st.integers(2, 30), st.booleans())
+def test_overlapping_limit(count, force):
+    records = [bytes([192, 0, 2, i]) for i in range(count)]
+    left = proto.Header(records=records[:-1], offline=[False] * (count - 1))
+    right = proto.Header(records=records[-2:], offline=[False] * 2)
+    response = server.command(
+        proto.Request(
+            merge=proto.Merge(
+                type=dns.rdatatype.A,
+                left=left,
+                right=right,
+                limit=count,
+                flags=1 if force else 0,
+            )
+        )
+    )
+    assert response.result == proto.SUCCESS
+    check_header(
+        response, contents(left) | contents(right), right, left, dns.rdatatype.A
+    )
+    response = server.command(
+        proto.Request(
+            merge=proto.Merge(
+                type=dns.rdatatype.A,
+                left=right,
+                right=right,
+                limit=2,
+                flags=1 if force else 0,
+            )
+        )
+    )
+    assert response.result == (proto.SUCCESS if force else proto.UNCHANGED)
+    if force:
+        check_header(response, contents(right), right, right, dns.rdatatype.A)
+
+
+@given(owners())
+@example(b"\x07ExAmPlE\x03com\x00")
+@example(b"\x07example\x03com\x00")
+def test_subtract_owner_case(owner):
+    left = proto.Header(
+        records=[b"\xc0\x00\x02\x01", b"\xc0\x00\x02\x02"], offline=[False, False]
+    )
+    if owner is not None:
+        left.owner = owner
+    right = proto.Header(records=[left.records[0]], offline=[False])
+    response = server.command(
+        proto.Request(
+            subtract=proto.Subtract(type=dns.rdatatype.A, left=left, right=right)
+        )
+    )
+    assert response.result == proto.SUCCESS
+    check_header(response, {left.records[1]: False}, left, left, dns.rdatatype.A)
+
+
+@given(st.integers(1, 20), st.integers(0, 21))
+def test_construction_limit(count, limit):
+    # Construction currently checks input count, before deduplication.
+    header = proto.Header(
+        records=[b"\xc0\x00\x02\x01"] * count, offline=[False] * count
+    )
+    response = server.command(
+        proto.Request(
+            construct=proto.Construct(type=dns.rdatatype.A, header=header, limit=limit)
+        )
+    )
+    expected = proto.TOO_MANY_RECORDS if limit and count > limit else proto.SUCCESS
+    assert response.result == expected
+    if expected == proto.SUCCESS:
+        check_header(response, contents(header), header, header, dns.rdatatype.A)
+
+
+@given(cases())
+def test_copy(case):
+    rrtype, left, _, _ = case
+    response = server.command(proto.Request(copy=proto.Copy(type=rrtype, header=left)))
+    assert response.result == proto.SUCCESS
+    # The copy constructor initializes a fresh header from rdataset fields;
+    # serial, case and resign metadata are deliberately not copied.
+    fresh = proto.Header(ttl=left.ttl, trust=left.trust)
+    check_header(response, contents(left), fresh, fresh, rrtype)
+    if left.HasField("owner"):
+        assert response.applied_owner == left.owner.lower()
+
+
+@given(cases(), dns_names())
+def test_set_owner_case(case, name):
+    rrtype, left, _, _ = case
+    header = proto.Header()
+    header.CopyFrom(left)
+    header.ClearField("owner")
+    owner = name.to_wire()
+    response = server.command(
+        proto.Request(
+            set_owner_case=proto.SetOwnerCase(type=rrtype, header=header, owner=owner)
+        )
+    )
+    assert response.result == proto.SUCCESS
+    header.owner = owner
+    check_header(response, contents(header), header, header, rrtype)
+
+
+if __name__ == "__main__":
+    with RestartingProtobufServer(
+        os.environ["VECHEADER_SERVER"], proto.Response
+    ) as server:
+        for test in (
+            test_construction,
+            test_merge,
+            test_subtract,
+            test_size_boundary,
+            test_merge_size_boundary,
+            test_overlapping_limit,
+            test_subtract_owner_case,
+            test_construction_limit,
+            test_copy,
+            test_set_owner_case,
+        ):
+            test()
+            print(f"{test.__name__}: passed", flush=True)
diff --git a/tests/hypothesis/vecheader_server.c b/tests/hypothesis/vecheader_server.c
new file mode 100644
index 0000000000..7dfd774881
--- /dev/null
+++ b/tests/hypothesis/vecheader_server.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ * SPDX-License-Identifier: MPL-2.0
+ */
+
+#include <arpa/inet.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <isc/lib.h>
+#include <isc/mem.h>
+#include <isc/region.h>
+#include <isc/util.h>
+
+#include <dns/lib.h>
+#include <dns/name.h>
+#include <dns/rdata.h>
+#include <dns/rdatalist.h>
+#include <dns/rdataset.h>
+#include <dns/rdatatype.h>
+#include <dns/rdatavec.h>
+
+#include "rdatavec_p.h"
+#include "vecheader.pb-c.h"
+
+static void
+set_owner_case(dns_vecheader_t *header, const ProtobufCBinaryData *owner) {
+	dns_name_t name = DNS_NAME_INITEMPTY;
+	isc_region_t region = {
+		.base = owner->data,
+		.length = owner->len,
+	};
+	dns_name_fromregion(&name, &region);
+	dns_vecheader_setownercase(header, &name);
+}
+
+/* All input bytes are logical RDATA, never a serialized C header. */
+static isc_result_t
+make_header(isc_mem_t *mctx, const Vec__Header *input, dns_rdatatype_t type,
+	    uint32_t limit, dns_vecheader_t **output) {
+	dns_rdatalist_t list = {
+		.rdata = ISC_LIST_INITIALIZER,
+		.link = ISC_LINK_INITIALIZER,
+		.type = type,
+		.covers = type == dns_rdatatype_rrsig ? dns_rdatatype_a : 0,
+		.rdclass = dns_rdataclass_in,
+		.ttl = input->ttl,
+	};
+	dns_rdataset_t set = DNS_RDATASET_INIT;
+	dns_rdata_t *records = calloc(input->n_records + 1, sizeof(*records));
+	isc_region_t region;
+	isc_result_t result = ISC_R_SUCCESS;
+	RUNTIME_CHECK(records != NULL);
+	RUNTIME_CHECK(input->n_offline == input->n_records);
+	memset(list.upper, 0xeb, sizeof(list.upper));
+	list.upper[0] &= ~0x01;
+	for (size_t i = 0; i < input->n_records; i++) {
+		/* protobuf-c uses NULL for empty bytes; comparators need a
+		 * base. */
+		static unsigned char empty;
+		region = (isc_region_t){
+			.base = input->records[i].len == 0
+					? &empty
+					: input->records[i].data,
+			.length = input->records[i].len,
+		};
+		dns_rdata_init(&records[i]);
+		dns_rdata_fromregion(&records[i], list.rdclass, type, &region);
+		if (input->offline[i]) {
+			records[i].flags |= DNS_RDATA_OFFLINE;
+		}
+		ISC_LIST_APPEND(list.rdata, &records[i], link);
+	}
+	dns_rdatalist_tordataset(&list, &set);
+	set.trust = input->trust;
+	if (input->n_records == 0) {
+		/* Empty headers represent tombstones, not empty rdatasets. */
+		*output = dns_vecheader_new(mctx);
+		(*output)->typepair = DNS_TYPEPAIR_VALUE(type, list.covers);
+		(*output)->ttl = input->ttl;
+		atomic_store(&(*output)->trust, input->trust);
+	} else {
+		result = dns_rdatavec_fromrdataset(&set, mctx, &region, limit);
+		if (result == ISC_R_SUCCESS) {
+			*output = (dns_vecheader_t *)region.base;
+		}
+	}
+	dns_rdataset_disassociate(&set);
+	free(records);
+	if (result != ISC_R_SUCCESS) {
+		return result;
+	}
+	(*output)->serial = input->serial;
+	(*output)->resign = input->resign_time;
+	if (input->resign) {
+		DNS_VECHEADER_SETATTR(*output, DNS_VECHEADERATTR_RESIGN);
+	}
+	if (input->has_owner) {
+		set_owner_case(*output, &input->owner);
+	}
+	return result;
+}
+
+static void
+write_response(Vec__Response *response) {
+	size_t length = vec__response__get_packed_size(response);
+	unsigned char *data = malloc(length);
+	uint32_t wire_length = htonl(length);
+	RUNTIME_CHECK(data != NULL);
+	vec__response__pack(response, data);
+	RUNTIME_CHECK(fwrite(&wire_length, 1, 4, stdout) == 4);
+	RUNTIME_CHECK(fwrite(data, 1, length, stdout) == length);
+	RUNTIME_CHECK(fflush(stdout) == 0);
+	free(data);
+}
+
+static Vec__Result
+result_to_proto(isc_result_t result) {
+	switch (result) {
+	case ISC_R_SUCCESS:
+		return VEC__RESULT__SUCCESS;
+	case DNS_R_TOOMANYRECORDS:
+		return VEC__RESULT__TOO_MANY_RECORDS;
+	case DNS_R_NOTEXACT:
+		return VEC__RESULT__NOT_EXACT;
+	case DNS_R_UNCHANGED:
+		return VEC__RESULT__UNCHANGED;
+	case DNS_R_SINGLETON:
+		return VEC__RESULT__SINGLETON;
+	case DNS_R_NXRRSET:
+		return VEC__RESULT__NXRRSET;
+	case ISC_R_NOSPACE:
+		return VEC__RESULT__NO_SPACE;
+	case ISC_R_FAILURE:
+		return VEC__RESULT__FAILURE;
+	default:
+		fprintf(stderr, "unexpected vecheader result: %s\n",
+			isc_result_toid(result));
+		abort();
+	}
+}
+
+static void
+reply(isc_result_t result, dns_vecheader_t *header, const Vec__Header *owner) {
+	Vec__Response response = {
+		.base = PROTOBUF_C_MESSAGE_INIT(&vec__response__descriptor),
+		.result = result_to_proto(result),
+	};
+	unsigned char namebytes[255];
+	if (header == NULL) {
+		write_response(&response);
+		return;
+	}
+	Vec__Header value = {
+		.base = PROTOBUF_C_MESSAGE_INIT(&vec__header__descriptor),
+		.has_ttl = true,
+		.ttl = header->ttl,
+		.has_trust = true,
+		.trust = atomic_load(&header->trust),
+		.has_serial = true,
+		.serial = header->serial,
+		.has_resign = true,
+		.resign = RESIGN(header),
+		.has_resign_time = true,
+		.resign_time = header->resign,
+		.records = calloc(header->count + 1, sizeof(*value.records)),
+		.offline = calloc(header->count + 1, sizeof(*value.offline)),
+	};
+	response = (Vec__Response){
+		.base = PROTOBUF_C_MESSAGE_INIT(&vec__response__descriptor),
+		.result = result_to_proto(result),
+		.header = &value,
+		.has_count = true,
+		.count = dns_rdatavec_count(header),
+		.has_raw_length = true,
+		.raw_length = header->raw_length,
+		.has_size = true,
+		.size = dns_rdatavec_size(header),
+		.has_header_size = true,
+		.header_size = sizeof(*header),
+		.has_type = true,
+		.type = DNS_TYPEPAIR_TYPE(header->typepair),
+		.has_covers = true,
+		.covers = DNS_TYPEPAIR_COVERS(header->typepair),
+		.has_case_set = true,
+		.case_set = CASESET(header),
+		.has_case_lower = true,
+		.case_lower = CASEFULLYLOWER(header),
+		.has_bitmap = true,
+		.bitmap = {
+			.len = sizeof(header->upper),
+			.data = header->upper,
+		},
+	};
+	RUNTIME_CHECK(value.records != NULL && value.offline != NULL);
+	rdatavec_iter_t iter;
+	DNS_VECHEADER_FOREACH(&iter, header, dns_rdataclass_in) {
+		dns_rdata_t data = DNS_RDATA_INIT;
+		vecheader_current(&iter, &data);
+		dns_rdata_t again = DNS_RDATA_INIT;
+		vecheader_current(&iter, &again);
+		RUNTIME_CHECK(data.data == again.data &&
+			      data.length == again.length &&
+			      data.flags == again.flags);
+		RUNTIME_CHECK(value.n_records < header->count);
+		value.records[value.n_records++] = (ProtobufCBinaryData){
+			.len = data.length,
+			.data = data.data,
+		};
+		value.offline[value.n_offline++] = (data.flags &
+						    DNS_RDATA_OFFLINE) != 0;
+	}
+	RUNTIME_CHECK(value.n_records == header->count);
+	RUNTIME_CHECK(vecheader_next(&iter) == ISC_R_NOMORE);
+	RUNTIME_CHECK(vecheader_next(&iter) == ISC_R_NOMORE);
+	if (owner->has_owner) {
+		RUNTIME_CHECK(owner->owner.len <= sizeof(namebytes));
+		memmove(namebytes, owner->owner.data, owner->owner.len);
+		isc_region_t region = {
+			.base = namebytes,
+			.length = owner->owner.len,
+		};
+		dns_name_t name = DNS_NAME_INITEMPTY;
+		dns_name_fromregion(&name, &region);
+		RUNTIME_CHECK(dns_name_downcase(&name, &name) == ISC_R_SUCCESS);
+		dns_rdataset_t set = {
+			.magic = DNS_RDATASET_MAGIC,
+			.link = ISC_LINK_INITIALIZER,
+			.methods = &dns_rdatavec_rdatasetmethods,
+			.vec.header = dns_vecheader_ref(header),
+		};
+		dns_rdataset_getownercase(&set, &name);
+		dns_rdataset_disassociate(&set);
+		response.has_applied_owner = true;
+		response.applied_owner = (ProtobufCBinaryData){
+			.len = name.length,
+			.data = name.ndata,
+		};
+	}
+	write_response(&response);
+	free(value.records);
+	free(value.offline);
+}
+
+static void
+execute(const Vec__Request *request) {
+	isc_mem_t *mctx = NULL;
+	dns_vecheader_t *left = NULL, *right = NULL, *output = NULL;
+	const Vec__Header *owner = NULL;
+	Vec__Header owner_input;
+	isc_result_t result = ISC_R_FAILURE;
+	isc_mem_create("vecheader-test", &mctx);
+	switch (request->command_case) {
+	case VEC__REQUEST__COMMAND_CONSTRUCT: {
+		const Vec__Construct *command = request->construct;
+		owner = command->header;
+		result = make_header(mctx, command->header, command->type,
+				     command->limit, &output);
+		break;
+	}
+	case VEC__REQUEST__COMMAND_COPY: {
+		const Vec__Copy *command = request->copy;
+		owner = command->header;
+		result = make_header(mctx, command->header, command->type, 0,
+				     &left);
+		if (result != ISC_R_SUCCESS) {
+			break;
+		}
+		dns_rdataset_t set = {
+			.magic = DNS_RDATASET_MAGIC,
+			.link = ISC_LINK_INITIALIZER,
+			.methods = &dns_rdatavec_rdatasetmethods,
+			.vec.header = dns_vecheader_ref(left),
+			.type = command->type,
+			.covers = DNS_TYPEPAIR_COVERS(left->typepair),
+			.rdclass = dns_rdataclass_in,
+			.ttl = left->ttl,
+			.trust = atomic_load(&left->trust),
+		};
+		isc_region_t region;
+		result = dns_rdatavec_fromrdataset(&set, mctx, &region, 0);
+		if (result == ISC_R_SUCCESS) {
+			output = (dns_vecheader_t *)region.base;
+		}
+		dns_rdataset_disassociate(&set);
+		break;
+	}
+	case VEC__REQUEST__COMMAND_MERGE: {
+		const Vec__Merge *command = request->merge;
+		owner = command->left;
+		result = make_header(mctx, command->left, command->type, 0,
+				     &left);
+		if (result != ISC_R_SUCCESS) {
+			break;
+		}
+		result = make_header(mctx, command->right, command->type, 0,
+				     &right);
+		if (result != ISC_R_SUCCESS) {
+			break;
+		}
+		result = dns_rdatavec_merge(
+			left, right, mctx, dns_rdataclass_in, command->type,
+			command->flags, command->limit, &output);
+		break;
+	}
+	case VEC__REQUEST__COMMAND_SUBTRACT: {
+		const Vec__Subtract *command = request->subtract;
+		owner = command->left;
+		result = make_header(mctx, command->left, command->type, 0,
+				     &left);
+		if (result != ISC_R_SUCCESS) {
+			break;
+		}
+		result = make_header(mctx, command->right, command->type, 0,
+				     &right);
+		if (result != ISC_R_SUCCESS) {
+			break;
+		}
+		result = dns_rdatavec_subtract(left, right, mctx,
+					       dns_rdataclass_in, command->type,
+					       command->flags, &output);
+		break;
+	}
+	case VEC__REQUEST__COMMAND_SET_OWNER_CASE: {
+		const Vec__SetOwnerCase *command = request->set_owner_case;
+		RUNTIME_CHECK(!command->header->has_owner);
+		owner_input = *command->header;
+		result = make_header(mctx, &owner_input, command->type, 0,
+				     &output);
+		owner_input.has_owner = true;
+		owner_input.owner = command->owner;
+		owner = &owner_input;
+		if (result == ISC_R_SUCCESS) {
+			set_owner_case(output, &command->owner);
+		}
+		break;
+	}
+	default:
+		break;
+	}
+	reply(result, output, owner);
+	if (output != NULL) {
+		dns_vecheader_unref(output);
+	}
+	if (right != NULL) {
+		dns_vecheader_unref(right);
+	}
+	if (left != NULL) {
+		dns_vecheader_unref(left);
+	}
+	RUNTIME_CHECK(isc_mem_inuse(mctx) == 0);
+	RUNTIME_CHECK(isc_mem_references(mctx) == 1);
+	isc_mem_detach(&mctx);
+}
+
+int
+main(void) {
+	uint32_t wire_length;
+	while (true) {
+		size_t n = fread(&wire_length, 1, 4, stdin);
+		if (n == 0 && feof(stdin)) {
+			break;
+		}
+		RUNTIME_CHECK(n == 4);
+		size_t length = ntohl(wire_length);
+		RUNTIME_CHECK(length > 0 && length <= 4 * 1024 * 1024);
+		unsigned char *data = malloc(length);
+		RUNTIME_CHECK(data != NULL);
+		RUNTIME_CHECK(fread(data, 1, length, stdin) == length);
+		Vec__Request *request = vec__request__unpack(NULL, length,
+							     data);
+		RUNTIME_CHECK(request != NULL);
+		execute(request);
+		vec__request__free_unpacked(request, NULL);
+		free(data);
+	}
+	return 0;
+}