In many network protocols e.g. DHCP, the TLV format is used.
Let's introduce a simple parser and builder of the data format.
'sd-ndisc-router.c',
'sd-ndisc-router-solicit.c',
'sd-radv.c',
+ 'tlv-util.c',
)
sources += libsystemd_network_sources
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'),
},
--- /dev/null
+/* 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);
--- /dev/null
+/* 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;
+}
--- /dev/null
+/* 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);