]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tlv-util: introduce tlv-util that handles Tag-Length-Value data format 41802/head
authorYu Watanabe <watanabe.yu+github@gmail.com>
Mon, 20 Apr 2026 20:05:32 +0000 (05:05 +0900)
committerYu Watanabe <watanabe.yu+github@gmail.com>
Tue, 12 May 2026 06:53:12 +0000 (15:53 +0900)
In many network protocols e.g. DHCP, the TLV format is used.
Let's introduce a simple parser and builder of the data format.

src/libsystemd-network/meson.build
src/libsystemd-network/test-tlv-util.c [new file with mode: 0644]
src/libsystemd-network/tlv-util.c [new file with mode: 0644]
src/libsystemd-network/tlv-util.h [new file with mode: 0644]

index b0443c3695206fd728e622d6c8b322c4d570af7f..6239056e3b4b1250e9a23daadbcdfaaf6ca9c8ef 100644 (file)
@@ -37,6 +37,7 @@ libsystemd_network_sources = files(
         'sd-ndisc-router.c',
         'sd-ndisc-router-solicit.c',
         'sd-radv.c',
+        'tlv-util.c',
 )
 
 sources += libsystemd_network_sources
@@ -113,6 +114,9 @@ executables += [
         network_test_template + {
                 'sources' : files('test-sd-dhcp-lease.c'),
         },
+        network_test_template + {
+                'sources' : files('test-tlv-util.c'),
+        },
         network_fuzz_template + {
                 'sources' : files('fuzz-dhcp-client.c'),
         },
diff --git a/src/libsystemd-network/test-tlv-util.c b/src/libsystemd-network/test-tlv-util.c
new file mode 100644 (file)
index 0000000..4747afa
--- /dev/null
@@ -0,0 +1,207 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "sd-dhcp-protocol.h"
+
+#include "hashmap.h"
+#include "iovec-util.h"
+#include "iovec-wrapper.h"
+#include "random-util.h"
+#include "tests.h"
+#include "tlv-util.h"
+
+TEST(tlv_constant) {
+        ASSERT_EQ(TLV_TAG_PAD, (uint32_t) SD_DHCP_OPTION_PAD);
+        ASSERT_EQ(TLV_TAG_END, (uint32_t) SD_DHCP_OPTION_END);
+}
+
+TEST(tlv) {
+        _cleanup_(tlv_done) TLV tlv = TLV_INIT(TLV_DHCP4);
+
+        _cleanup_(iovec_done) struct iovec data0 = {}, data1 = {}, data2a = {}, data2b = {}, data3 = {}, data4 = {};
+        ASSERT_OK(random_bytes_allocate_iovec(0, &data0));
+        ASSERT_OK(random_bytes_allocate_iovec(111, &data1));
+        ASSERT_OK(random_bytes_allocate_iovec(123, &data2a));
+        ASSERT_OK(random_bytes_allocate_iovec(321, &data2b));
+        ASSERT_OK(random_bytes_allocate_iovec(333, &data3));
+        ASSERT_OK(random_bytes_allocate_iovec(444, &data4));
+
+        /* tlv_append() */
+        ASSERT_OK(tlv_append(&tlv, 10, data0.iov_len, data0.iov_base));
+        ASSERT_OK(tlv_append(&tlv, 11, data1.iov_len, data1.iov_base));
+        ASSERT_OK(tlv_append(&tlv, 22, data2a.iov_len, data2a.iov_base));
+        ASSERT_OK(tlv_append(&tlv, 22, data2b.iov_len, data2b.iov_base));
+        ASSERT_OK(tlv_append(&tlv, 33, data3.iov_len, data3.iov_base));
+        ASSERT_OK(tlv_append(&tlv, 44, data4.iov_len, data4.iov_base));
+        ASSERT_ERROR(tlv_append(&tlv, 0x00, data4.iov_len, data4.iov_base), EINVAL);
+        ASSERT_ERROR(tlv_append(&tlv, 0xFF, data4.iov_len, data4.iov_base), EINVAL);
+        ASSERT_EQ(hashmap_size(tlv.entries), 5u);
+
+        /* tlv_remove() */
+        tlv_remove(&tlv, 44);
+        ASSERT_EQ(hashmap_size(tlv.entries), 4u);
+        tlv_remove(&tlv, 55);
+        ASSERT_EQ(hashmap_size(tlv.entries), 4u);
+
+        /* tlv_append_tlv() */
+        _cleanup_(tlv_done) TLV tlv_copy = TLV_INIT(TLV_DHCP4);
+        ASSERT_ERROR(tlv_append_tlv(&tlv_copy, &tlv_copy), EINVAL);
+        ASSERT_OK(tlv_append_tlv(&tlv_copy, NULL));
+        ASSERT_OK(tlv_append_tlv(&tlv_copy, &tlv));
+        ASSERT_EQ(hashmap_size(tlv_copy.entries), hashmap_size(tlv.entries));
+
+        /* tlv_isempty() */
+        ASSERT_TRUE(tlv_isempty(NULL));
+        ASSERT_TRUE(tlv_isempty(&TLV_INIT(TLV_DHCP4)));
+        ASSERT_FALSE(tlv_isempty(&tlv));
+
+        /* tlv_contains() */
+        ASSERT_TRUE(tlv_contains(&tlv, 10));
+        ASSERT_TRUE(tlv_contains(&tlv, 11));
+        ASSERT_TRUE(tlv_contains(&tlv, 22));
+        ASSERT_TRUE(tlv_contains(&tlv, 33));
+        ASSERT_FALSE(tlv_contains(&tlv, 44));
+
+        /* tlv_get_all() */
+        struct iovec_wrapper *iovw;
+
+        iovw = ASSERT_NOT_NULL(tlv_get_all(&tlv, 10));
+        ASSERT_EQ(iovw->count, 1u);
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[0], &data0));
+
+        iovw = ASSERT_NOT_NULL(tlv_get_all(&tlv, 11));
+        ASSERT_EQ(iovw->count, 1u);
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[0], &data1));
+
+        iovw = ASSERT_NOT_NULL(tlv_get_all(&tlv, 22));
+        ASSERT_EQ(iovw->count, 3u);
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[0], &data2a));
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[1], &IOVEC_MAKE(data2b.iov_base, UINT8_MAX)));
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[2], &IOVEC_SHIFT(&data2b, UINT8_MAX)));
+
+        iovw = ASSERT_NOT_NULL(tlv_get_all(&tlv, 33));
+        ASSERT_EQ(iovw->count, 2u);
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[0], &IOVEC_MAKE(data3.iov_base, UINT8_MAX)));
+        ASSERT_TRUE(iovec_equal(&iovw->iovec[1], &IOVEC_SHIFT(&data3, UINT8_MAX)));
+
+        ASSERT_NULL(tlv_get_all(&tlv, 44));
+
+        /* tlv_get_full() */
+        struct iovec iov;
+
+        ASSERT_OK(tlv_get(&tlv, 10, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data0));
+        ASSERT_OK(tlv_get_full(&tlv, 10, data0.iov_len, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data0));
+        ASSERT_ERROR(tlv_get_full(&tlv, 10, 123, &iov), ENODATA);
+
+        ASSERT_OK(tlv_get(&tlv, 11, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data1));
+        ASSERT_OK(tlv_get_full(&tlv, 11, data1.iov_len, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data1));
+        ASSERT_ERROR(tlv_get_full(&tlv, 11, 123, &iov), ENODATA);
+
+        ASSERT_OK(tlv_get(&tlv, 22, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data2a));
+        ASSERT_OK(tlv_get_full(&tlv, 22, data2a.iov_len, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &data2a));
+        ASSERT_ERROR(tlv_get_full(&tlv, 22, data2b.iov_len, &iov), ENODATA);
+        ASSERT_OK(tlv_get_full(&tlv, 22, UINT8_MAX, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &IOVEC_MAKE(data2b.iov_base, UINT8_MAX)));
+        ASSERT_OK(tlv_get_full(&tlv, 22, data2b.iov_len - UINT8_MAX, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &IOVEC_SHIFT(&data2b, UINT8_MAX)));
+
+        ASSERT_OK(tlv_get(&tlv, 33, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &IOVEC_MAKE(data3.iov_base, UINT8_MAX)));
+        ASSERT_ERROR(tlv_get_full(&tlv, 33, data3.iov_len, &iov), ENODATA);
+        ASSERT_OK(tlv_get_full(&tlv, 33, UINT8_MAX, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &IOVEC_MAKE(data3.iov_base, UINT8_MAX)));
+        ASSERT_OK(tlv_get_full(&tlv, 33, data3.iov_len - UINT8_MAX, &iov));
+        ASSERT_TRUE(iovec_equal(&iov, &IOVEC_SHIFT(&data3, UINT8_MAX)));
+
+        ASSERT_ERROR(tlv_get(&tlv, 44, NULL), ENODATA);
+
+        /* tlv_get_alloc() */
+        _cleanup_(iovec_done) struct iovec v = {};
+
+        ASSERT_OK(tlv_get_alloc(&tlv, 10, &v));
+        ASSERT_TRUE(iovec_equal(&v, &data0));
+        iovec_done(&v);
+
+        ASSERT_OK(tlv_get_alloc(&tlv, 11, &v));
+        ASSERT_TRUE(iovec_equal(&v, &data1));
+        iovec_done(&v);
+
+        ASSERT_OK(tlv_get_alloc(&tlv, 22, &v));
+        ASSERT_EQ(v.iov_len, data2a.iov_len + data2b.iov_len);
+        ASSERT_EQ(memcmp(v.iov_base, data2a.iov_base, data2a.iov_len), 0);
+        ASSERT_EQ(memcmp((uint8_t*) v.iov_base + data2a.iov_len, data2b.iov_base, data2b.iov_len), 0);
+        iovec_done(&v);
+
+        ASSERT_OK(tlv_get_alloc(&tlv, 33, &v));
+        ASSERT_TRUE(iovec_equal(&v, &data3));
+        iovec_done(&v);
+
+        ASSERT_ERROR(tlv_get_alloc(&tlv, 44, NULL), ENODATA);
+
+        /* tlv_size() */
+        size_t sz = tlv_size(&tlv);
+        /* The tlv contains the 7 entries with a 2-byte header:
+         * tag 10: 1 entry, tag 11: 1 entry, tag 22: 3 entries, tag 33: 2 entries = 7 entries total. */
+        ASSERT_EQ(sz, 7 * 2 + data0.iov_len + data1.iov_len + data2a.iov_len + data2b.iov_len + data3.iov_len + 1);
+
+        /* tlv_build() */
+        ASSERT_OK(tlv_build(&tlv, &v));
+        ASSERT_EQ(v.iov_len, sz);
+        uint8_t *p = v.iov_base;
+        ASSERT_EQ(*p++, 10u);
+        ASSERT_EQ(*p++, data0.iov_len);
+
+        ASSERT_EQ(*p++, 11u);
+        ASSERT_EQ(*p++, data1.iov_len);
+        ASSERT_EQ(memcmp(p, data1.iov_base, data1.iov_len), 0);
+        p += data1.iov_len;
+
+        ASSERT_EQ(*p++, 22u);
+        ASSERT_EQ(*p++, data2a.iov_len);
+        ASSERT_EQ(memcmp(p, data2a.iov_base, data2a.iov_len), 0);
+        p += data2a.iov_len;
+
+        ASSERT_EQ(*p++, 22u);
+        ASSERT_EQ(*p++, UINT8_MAX);
+        ASSERT_EQ(memcmp(p, data2b.iov_base, UINT8_MAX), 0);
+        p += UINT8_MAX;
+
+        ASSERT_EQ(*p++, 22u);
+        ASSERT_EQ(*p++, data2b.iov_len - UINT8_MAX);
+        ASSERT_EQ(memcmp(p, (uint8_t*) data2b.iov_base + UINT8_MAX, data2b.iov_len - UINT8_MAX), 0);
+        p += data2b.iov_len - UINT8_MAX;
+
+        ASSERT_EQ(*p++, 33u);
+        ASSERT_EQ(*p++, UINT8_MAX);
+        ASSERT_EQ(memcmp(p, data3.iov_base, UINT8_MAX), 0);
+        p += UINT8_MAX;
+
+        ASSERT_EQ(*p++, 33u);
+        ASSERT_EQ(*p++, data3.iov_len - UINT8_MAX);
+        ASSERT_EQ(memcmp(p, (uint8_t*) data3.iov_base + UINT8_MAX, data3.iov_len - UINT8_MAX), 0);
+        p += data3.iov_len - UINT8_MAX;
+
+        ASSERT_EQ(*p, 255u);
+
+        /* tlv_new() and tlv_parse() */
+        _cleanup_(tlv_unrefp) TLV *tlv2 = ASSERT_NOT_NULL(tlv_new(TLV_DHCP4 | TLV_TEMPORARY));
+        ASSERT_OK(tlv_parse(tlv2, &v));
+        ASSERT_EQ(hashmap_size(tlv.entries), hashmap_size(tlv2->entries));
+        void *tagp;
+        HASHMAP_FOREACH_KEY(iovw, tagp, tlv.entries) {
+                struct iovec_wrapper *iovw2 = ASSERT_PTR(hashmap_get(tlv2->entries, tagp));
+                ASSERT_TRUE(iovw_equal(iovw, iovw2));
+        }
+
+        /* tlv_build() again, and check the reproducibility. */
+        _cleanup_(iovec_done) struct iovec v2 = {};
+        ASSERT_OK(tlv_build(tlv2, &v2));
+        ASSERT_TRUE(iovec_equal(&v, &v2));
+}
+
+DEFINE_TEST_MAIN(LOG_DEBUG);
diff --git a/src/libsystemd-network/tlv-util.c b/src/libsystemd-network/tlv-util.c
new file mode 100644 (file)
index 0000000..68574e7
--- /dev/null
@@ -0,0 +1,505 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "alloc-util.h"
+#include "hashmap.h"
+#include "iovec-util.h"
+#include "iovec-wrapper.h"
+#include "tlv-util.h"
+#include "unaligned.h"
+
+#define TLV_MAX_ENTRIES 4096u
+
+TLVFlag tlv_flags_verify(TLVFlag flags) {
+        assert(IN_SET(flags & _TLV_TAG_MASK, TLV_TAG_U8, TLV_TAG_U16, TLV_TAG_U32));
+        assert(IN_SET(flags & _TLV_LENGTH_MASK, TLV_LENGTH_U8, TLV_LENGTH_U16, TLV_LENGTH_U32));
+
+        /* TLV_PAD and TLV_END are for DHCPv4 options, hence here we assume TLV_TAG_U8 is set. */
+        assert(!FLAGS_SET(flags, TLV_PAD) || FLAGS_SET(flags, TLV_TAG_U8));
+        assert(!FLAGS_SET(flags, TLV_END) || FLAGS_SET(flags, TLV_TAG_U8));
+
+        /* When we requested to append the END tag, then we should understand the END tag on parse. */
+        assert(!FLAGS_SET(flags, TLV_APPEND_END) || FLAGS_SET(flags, TLV_END));
+
+        return flags;
+}
+
+void tlv_done(TLV *tlv) {
+        assert(tlv);
+
+        tlv->entries = hashmap_free(tlv->entries);
+        tlv->n_entries = 0;
+}
+
+static TLV* tlv_free(TLV *tlv) {
+        if (!tlv)
+                return NULL;
+
+        tlv_done(tlv);
+        return mfree(tlv);
+}
+
+DEFINE_TRIVIAL_REF_UNREF_FUNC(TLV, tlv, tlv_free);
+
+TLV* tlv_new(TLVFlag flags) {
+        TLV *tlv = new(TLV, 1);
+        if (!tlv)
+                return NULL;
+
+        *tlv = TLV_INIT(flags);
+        return tlv;
+}
+
+bool tlv_isempty(const TLV *tlv) {
+        return !tlv || hashmap_isempty(tlv->entries);
+}
+
+struct iovec_wrapper* tlv_get_all(const TLV *tlv, uint32_t tag) {
+        assert(tlv);
+        return hashmap_get(tlv->entries, UINT32_TO_PTR(tag));
+}
+
+int tlv_get_full(const TLV *tlv, uint32_t tag, size_t length, struct iovec *ret) {
+        assert(tlv);
+
+        /* Do not free the result iovec, the data is still owned by TLV (or the original input data when
+         * TLV_TEMPORARY is set). */
+
+        struct iovec_wrapper *iovw = tlv_get_all(tlv, tag);
+        if (iovw_isempty(iovw))
+                return -ENODATA;
+
+        /* When multiple entries exist, use the first one matching the length. */
+        FOREACH_ARRAY(iov, iovw->iovec, iovw->count) {
+                if (length != SIZE_MAX && iov->iov_len != length)
+                        continue;
+
+                if (ret)
+                        *ret = *iov;
+                return 0;
+        }
+
+        return -ENODATA;
+}
+
+int tlv_get_alloc(const TLV *tlv, uint32_t tag, struct iovec *ret) {
+        assert(tlv);
+
+        /* Free the result iovec. */
+
+        struct iovec_wrapper *iovw = tlv_get_all(tlv, tag);
+        if (iovw_isempty(iovw))
+                return -ENODATA;
+
+        if (!ret)
+                return 0;
+
+        if (FLAGS_SET(tlv->flags, TLV_MERGE))
+                return iovw_concat(iovw, ret);
+
+        /* When TLV_MERGE is unset, provides the first entry. */
+        if (!iovec_memdup(&iovw->iovec[0], ret))
+                return -ENOMEM;
+
+        return 0;
+}
+
+void tlv_remove(TLV *tlv, uint32_t tag) {
+        assert(tlv);
+
+        struct iovec_wrapper *iovw = hashmap_remove(tlv->entries, UINT32_TO_PTR(tag));
+        if (!iovw)
+                return;
+
+        assert(tlv->n_entries >= iovw->count);
+        tlv->n_entries -= iovw->count;
+
+        if (FLAGS_SET(tlv->flags, TLV_TEMPORARY))
+                iovw_free(iovw);
+        else
+                iovw_free_free(iovw);
+}
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
+                tlv_hash_ops,
+                void,
+                trivial_hash_func,
+                trivial_compare_func,
+                struct iovec_wrapper,
+                iovw_free);
+
+DEFINE_PRIVATE_HASH_OPS_WITH_VALUE_DESTRUCTOR(
+                tlv_hash_ops_free,
+                void,
+                trivial_hash_func,
+                trivial_compare_func,
+                struct iovec_wrapper,
+                iovw_free_free);
+
+static int tlv_append_impl(TLV *tlv, uint32_t tag, size_t length, const void *data) {
+        int r;
+
+        assert(tlv);
+        assert(length == 0 || data);
+
+        if (tlv->n_entries >= TLV_MAX_ENTRIES)
+                return -E2BIG;
+
+        if (FLAGS_SET(tlv->flags, TLV_TEMPORARY)) {
+                struct iovec_wrapper *e = tlv_get_all(tlv, tag);
+                if (e) {
+                        r = iovw_put_full(e, /* accept_zero= */ true, (void*) data, length);
+                        if (r < 0)
+                                return r;
+                } else {
+                        _cleanup_(iovw_freep) struct iovec_wrapper *v = new0(struct iovec_wrapper, 1);
+                        if (!v)
+                                return -ENOMEM;
+
+                        r = iovw_put_full(v, /* accept_zero= */ true, (void*) data, length);
+                        if (r < 0)
+                                return r;
+
+                        r = hashmap_ensure_put(&tlv->entries, &tlv_hash_ops, UINT32_TO_PTR(tag), v);
+                        if (r < 0)
+                                return r;
+
+                        TAKE_PTR(v);
+                }
+        } else {
+                struct iovec_wrapper *e = tlv_get_all(tlv, tag);
+                if (e) {
+                        r = iovw_extend_full(e, /* accept_zero= */ true, data, length);
+                        if (r < 0)
+                                return r;
+                } else {
+                        _cleanup_(iovw_free_freep) struct iovec_wrapper *v = new0(struct iovec_wrapper, 1);
+                        if (!v)
+                                return -ENOMEM;
+
+                        r = iovw_extend_full(v, /* accept_zero= */ true, data, length);
+                        if (r < 0)
+                                return r;
+
+                        r = hashmap_ensure_put(&tlv->entries, &tlv_hash_ops_free, UINT32_TO_PTR(tag), v);
+                        if (r < 0)
+                                return r;
+
+                        TAKE_PTR(v);
+                }
+        }
+
+        tlv->n_entries++;
+        return 0;
+}
+
+int tlv_append(TLV *tlv, uint32_t tag, size_t length, const void *data) {
+        int r;
+
+        assert(tlv);
+        assert(length == 0 || data);
+
+        switch (tlv->flags & _TLV_TAG_MASK) {
+        case TLV_TAG_U8:
+                if (tag > UINT8_MAX)
+                        return -EINVAL;
+                break;
+        case TLV_TAG_U16:
+                if (tag > UINT16_MAX)
+                        return -EINVAL;
+                break;
+        case TLV_TAG_U32:
+                break;
+        default:
+                assert_not_reached();
+        }
+
+        if ((FLAGS_SET(tlv->flags, TLV_PAD) && tag == TLV_TAG_PAD) ||
+            (FLAGS_SET(tlv->flags, TLV_END) && tag == TLV_TAG_END))
+                return -EINVAL;
+
+        size_t max_length;
+        switch (tlv->flags & _TLV_LENGTH_MASK) {
+        case TLV_LENGTH_U8:
+                max_length = UINT8_MAX;
+                break;
+        case TLV_LENGTH_U16:
+                max_length = UINT16_MAX;
+                break;
+        case TLV_LENGTH_U32:
+                max_length = UINT32_MAX;
+                break;
+        default:
+                assert_not_reached();
+        }
+
+        if (FLAGS_SET(tlv->flags, TLV_MERGE)) {
+                /* If TLV_MERGE is set and the length is larger than the allowed maximum, then split the data
+                 * and store them in multiple entries.
+                 *
+                 * Note, if tlv_append_impl() fails below, we do not rollback the entries, hence the caller
+                 * of this function needs to discard the entire data in that case. */
+                const uint8_t *p = data;
+                while (length > max_length) {
+                        r = tlv_append_impl(tlv, tag, max_length, p);
+                        if (r < 0)
+                                return r;
+
+                        p += max_length;
+                        length -= max_length;
+                }
+
+                return tlv_append_impl(tlv, tag, length, p);
+        }
+
+        /* Otherwise, refuse too long data. */
+        if (length > max_length)
+                return -EINVAL;
+
+        return tlv_append_impl(tlv, tag, length, data);
+}
+
+int tlv_append_iov(TLV *tlv, uint32_t tag, const struct iovec *iov) {
+        assert(tlv);
+        assert(iovec_is_valid(iov));
+
+        return tlv_append(tlv, tag, iov ? iov->iov_len : 0, iov ? iov->iov_base : NULL);
+}
+
+int tlv_append_tlv(TLV *tlv, const TLV *source) {
+        int r;
+
+        assert(tlv);
+
+        /* Note, this does not rollback entries on failure, hence the caller of this function needs to
+         * discard the entire data in that case. */
+
+        if (!source)
+                return 0;
+
+        if (source == tlv)
+                return -EINVAL;
+
+        void *tagp;
+        struct iovec_wrapper *iovw;
+        HASHMAP_FOREACH_KEY(iovw, tagp, source->entries) {
+                uint32_t tag = PTR_TO_UINT32(tagp);
+
+                FOREACH_ARRAY(iov, iovw->iovec, iovw->count) {
+                        r = tlv_append(tlv, tag, iov->iov_len, iov->iov_base);
+                        if (r < 0)
+                                return r;
+                }
+        }
+
+        return 0;
+}
+
+int tlv_parse(TLV *tlv, const struct iovec *iov) {
+        int r;
+
+        assert(tlv);
+        assert(iovec_is_valid(iov));
+
+        /* Note, this does not rollback entries on failure, hence the caller of this function needs to
+         * discard the entire data in that case. */
+
+        if (!iovec_is_set(iov))
+                return 0;
+
+        for (struct iovec i = *iov; iovec_is_set(&i); ) {
+                uint32_t tag;
+                switch (tlv->flags & _TLV_TAG_MASK) {
+                case TLV_TAG_U8:
+                        if (i.iov_len < sizeof(uint8_t))
+                                return -EBADMSG;
+                        tag = *(uint8_t*) i.iov_base;
+                        iovec_inc(&i, sizeof(uint8_t));
+                        break;
+                case TLV_TAG_U16:
+                        if (i.iov_len < sizeof(uint16_t))
+                                return -EBADMSG;
+                        tag = unaligned_read_be16(i.iov_base);
+                        iovec_inc(&i, sizeof(uint16_t));
+                        break;
+                case TLV_TAG_U32:
+                        if (i.iov_len < sizeof(uint32_t))
+                                return -EBADMSG;
+                        tag = unaligned_read_be32(i.iov_base);
+                        iovec_inc(&i, sizeof(uint32_t));
+                        break;
+                default:
+                        assert_not_reached();
+                }
+
+                if (FLAGS_SET(tlv->flags, TLV_PAD) && tag == TLV_TAG_PAD)
+                        continue;
+                if (FLAGS_SET(tlv->flags, TLV_END) && tag == TLV_TAG_END)
+                        break;
+
+                size_t len;
+                switch (tlv->flags & _TLV_LENGTH_MASK) {
+                case TLV_LENGTH_U8:
+                        if (i.iov_len < sizeof(uint8_t))
+                                return -EBADMSG;
+                        len = *(uint8_t*) i.iov_base;
+                        iovec_inc(&i, sizeof(uint8_t));
+                        break;
+                case TLV_LENGTH_U16:
+                        if (i.iov_len < sizeof(uint16_t))
+                                return -EBADMSG;
+                        len = unaligned_read_be16(i.iov_base);
+                        iovec_inc(&i, sizeof(uint16_t));
+                        break;
+                case TLV_LENGTH_U32:
+                        if (i.iov_len < sizeof(uint32_t))
+                                return -EBADMSG;
+                        len = unaligned_read_be32(i.iov_base);
+                        iovec_inc(&i, sizeof(uint32_t));
+                        break;
+                default:
+                        assert_not_reached();
+                }
+
+                if (i.iov_len < len)
+                        return -EBADMSG;
+
+                r = tlv_append_impl(tlv, tag, len, i.iov_base);
+                if (r < 0)
+                        return r;
+
+                iovec_inc(&i, len);
+        }
+
+        return 0;
+}
+
+size_t tlv_size(const TLV *tlv) {
+        assert(tlv);
+
+        size_t header_sz;
+        switch (tlv->flags & _TLV_TAG_MASK) {
+        case TLV_TAG_U8:
+                header_sz = sizeof(uint8_t);
+                break;
+        case TLV_TAG_U16:
+                header_sz = sizeof(uint16_t);
+                break;
+        case TLV_TAG_U32:
+                header_sz = sizeof(uint32_t);
+                break;
+        default:
+                assert_not_reached();
+        }
+
+        switch (tlv->flags & _TLV_LENGTH_MASK) {
+        case TLV_LENGTH_U8:
+                header_sz += sizeof(uint8_t);
+                break;
+        case TLV_LENGTH_U16:
+                header_sz += sizeof(uint16_t);
+                break;
+        case TLV_LENGTH_U32:
+                header_sz += sizeof(uint32_t);
+                break;
+        default:
+                assert_not_reached();
+        }
+
+        size_t sz = FLAGS_SET(tlv->flags, TLV_APPEND_END);
+
+        struct iovec_wrapper *iovw;
+        HASHMAP_FOREACH(iovw, tlv->entries) {
+                if (size_multiply_overflow(header_sz, iovw->count))
+                        return SIZE_MAX;
+
+                sz = size_add(sz, size_add(header_sz * iovw->count, iovw_size(iovw)));
+        }
+
+        return sz;
+}
+
+int tlv_build(const TLV *tlv, struct iovec *ret) {
+        int r;
+
+        assert(tlv);
+        assert(ret);
+
+        size_t sz = tlv_size(tlv);
+        if (sz == SIZE_MAX)
+                return -ENOBUFS;
+
+        _cleanup_free_ uint8_t *buf = new(uint8_t, sz);
+        if (!buf)
+                return -ENOMEM;
+
+        /* Sort by tags, for reproducibility. */
+        _cleanup_free_ void **sorted = NULL;
+        size_t n;
+        r = hashmap_dump_keys_sorted(tlv->entries, &sorted, &n);
+        if (r < 0)
+                return r;
+
+        uint8_t *p = buf;
+        FOREACH_ARRAY(tagp, sorted, n) {
+                uint32_t tag = PTR_TO_UINT32(*tagp);
+                struct iovec_wrapper *iovw = ASSERT_PTR(tlv_get_all(tlv, tag));
+
+                if ((FLAGS_SET(tlv->flags, TLV_PAD) && tag == TLV_TAG_PAD) ||
+                    (FLAGS_SET(tlv->flags, TLV_END) && tag == TLV_TAG_END))
+                        return -EINVAL;
+
+                FOREACH_ARRAY(iov, iovw->iovec, iovw->count) {
+                        switch (tlv->flags & _TLV_TAG_MASK) {
+                        case TLV_TAG_U8:
+                                if (tag > UINT8_MAX)
+                                        return -EINVAL;
+                                *p++ = tag;
+                                break;
+                        case TLV_TAG_U16:
+                                if (tag > UINT16_MAX)
+                                        return -EINVAL;
+                                unaligned_write_be16(p, tag);
+                                p += sizeof(uint16_t);
+                                break;
+                        case TLV_TAG_U32:
+                                unaligned_write_be32(p, tag);
+                                p += sizeof(uint32_t);
+                                break;
+                        default:
+                                assert_not_reached();
+                        }
+
+                        switch (tlv->flags & _TLV_LENGTH_MASK) {
+                        case TLV_LENGTH_U8:
+                                if (iov->iov_len > UINT8_MAX)
+                                        return -EINVAL;
+                                *p++ = iov->iov_len;
+                                break;
+                        case TLV_LENGTH_U16:
+                                if (iov->iov_len > UINT16_MAX)
+                                        return -EINVAL;
+                                unaligned_write_be16(p, iov->iov_len);
+                                p += sizeof(uint16_t);
+                                break;
+                        case TLV_LENGTH_U32:
+                                if (iov->iov_len > UINT32_MAX)
+                                        return -EINVAL;
+                                unaligned_write_be32(p, iov->iov_len);
+                                p += sizeof(uint32_t);
+                                break;
+                        default:
+                                assert_not_reached();
+                        }
+
+                        p = mempcpy_safe(p, iov->iov_base, iov->iov_len);
+                }
+        }
+
+        if (FLAGS_SET(tlv->flags, TLV_APPEND_END))
+                *p++ = TLV_TAG_END;
+
+        assert(sz == (size_t) (p - buf));
+
+        *ret = IOVEC_MAKE(TAKE_PTR(buf), sz);
+        return 0;
+}
diff --git a/src/libsystemd-network/tlv-util.h b/src/libsystemd-network/tlv-util.h
new file mode 100644 (file)
index 0000000..5344c28
--- /dev/null
@@ -0,0 +1,82 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+#pragma once
+
+#include "sd-forward.h"
+
+#define TLV_TAG_PAD UINT32_C(0)
+#define TLV_TAG_END UINT32_C(0xFF)
+
+typedef enum TLVFlag {
+        TLV_TAG_U8       = 1 << 0,
+        TLV_TAG_U16      = 1 << 1,
+        TLV_TAG_U32      = 1 << 2,
+        _TLV_TAG_MASK    = TLV_TAG_U8 | TLV_TAG_U16 | TLV_TAG_U32,
+        TLV_LENGTH_U8    = 1 << 3,
+        TLV_LENGTH_U16   = 1 << 4,
+        TLV_LENGTH_U32   = 1 << 5,
+        _TLV_LENGTH_MASK = TLV_LENGTH_U8 | TLV_LENGTH_U16 | TLV_LENGTH_U32,
+        TLV_PAD          = 1 << 6,  /* If set, tag == 0 is a pad, and does not have the length field. */
+        TLV_END          = 1 << 7,  /* If set, tag == 0xFF is a sign of the end of the sequence. */
+        TLV_APPEND_END   = 1 << 8,  /* If set, append the END tag at the end of the sequence on build. */
+        TLV_MERGE        = 1 << 9,  /* If set, tlv_get_alloc() merges them, and tlv_append() split long data. */
+        TLV_TEMPORARY    = 1 << 10, /* If set, tlv_append() and tlv_parse() do not copy the data. */
+
+        /* DHCPv4 options. */
+        TLV_DHCP4        = TLV_TAG_U8 | TLV_LENGTH_U8 | TLV_PAD | TLV_END | TLV_APPEND_END | TLV_MERGE,
+        /* Used for DHCPv4 sub-options, e.g.
+         * DHCPv4 Vendor Specific Information sub-option (43),
+         * DHCPv4 Relay Agent Information sub-option (82), or
+         * DHCPv4 Vendor-Identifying Vendor Specific Information sub-sub-option (125).
+         * Note that the PAD is not mentioned in RFC, but some implementations use it, hence let's gracefully
+         * handle it. Also note that the END tag is prohibited in most options, but we also gracefully handle
+         * it on parse, but of course do not append it on build. */
+        TLV_DHCP4_SUBOPTION
+                         = TLV_TAG_U8 | TLV_LENGTH_U8 | TLV_PAD | TLV_END,
+        /* DHCPv4 Vendor-Identifying Vendor Class sub-option (124) and
+         * DHCPv4 Vendor-Identifying Vendor Specific Information sub-option (125).
+         * The tag is called 'enterprise-number', and in uint32. */
+        TLV_DHCP4_VENDOR_IDENTIFYING_OPTION
+                         = TLV_TAG_U32 | TLV_LENGTH_U8 | TLV_MERGE,
+} TLVFlag;
+
+typedef struct TLV {
+        unsigned n_ref;
+        TLVFlag flags;
+        unsigned n_entries;
+        Hashmap *entries;
+} TLV;
+
+#define TLV_INIT(f)                             \
+        (TLV) {                                 \
+                .n_ref = 1,                     \
+                .flags = tlv_flags_verify(f),   \
+        }
+
+TLVFlag tlv_flags_verify(TLVFlag flags);
+
+void tlv_done(TLV *tlv);
+TLV* tlv_ref(TLV *p);
+TLV* tlv_unref(TLV *p);
+DEFINE_TRIVIAL_CLEANUP_FUNC(TLV*, tlv_unref);
+TLV* tlv_new(TLVFlag flags);
+
+bool tlv_isempty(const TLV *tlv);
+
+struct iovec_wrapper* tlv_get_all(const TLV *tlv, uint32_t tag);
+static inline bool tlv_contains(const TLV *tlv, uint32_t tag) {
+        return tlv_get_all(tlv, tag);
+}
+int tlv_get_full(const TLV *tlv, uint32_t tag, size_t length, struct iovec *ret);
+static inline int tlv_get(const TLV *tlv, uint32_t tag, struct iovec *ret) {
+        return tlv_get_full(tlv, tag, SIZE_MAX, ret);
+}
+int tlv_get_alloc(const TLV *tlv, uint32_t tag, struct iovec *ret);
+
+void tlv_remove(TLV *tlv, uint32_t tag);
+int tlv_append(TLV *tlv, uint32_t tag, size_t length, const void *data);
+int tlv_append_iov(TLV *tlv, uint32_t tag, const struct iovec *iov);
+int tlv_append_tlv(TLV *tlv, const TLV *source);
+
+int tlv_parse(TLV *tlv, const struct iovec *iov);
+size_t tlv_size(const TLV *tlv);
+int tlv_build(const TLV *tlv, struct iovec *ret);