[RFC][PATCH v3 1/9] lib: Add TLV parser

From: Roberto Sassu
Date: Thu Jul 20 2023 - 11:34:54 EST


From: Roberto Sassu <roberto.sassu@xxxxxxxxxx>

Add a parser of a generic TLV format:

+-----------------+------------------+-----------------+
| data type (u64) | num fields (u64) | total len (u64) | # header
+--------------+--+---------+--------+---------+-------+
| field1 (u64) | len1 (u64) | value1 (u8 len1) |
+--------------+------------+------------------+
| ... | ... | ... | # data
+--------------+------------+------------------+
| fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+--------------+------------+------------------+

Each adopter can define its own data types and fields. The TLV parser does
not need to be aware of those, and calls a callback function, supplied by
the adopter, for every encountered field during parsing. The adopter can
decide in the callback function how each defined field should be
handled/parsed.

Normally, calling tlv_parse() is sufficient for most of the use cases. In
addition, tlv_parse_hdr() and tlv_parse_data() are also provided for more
advanced use cases.

Nesting TLVs is also possible, the parser of one field can call tlv_parse()
to parse the inner structure.

Signed-off-by: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
---
MAINTAINERS | 8 ++
include/linux/tlv_parser.h | 28 +++++
include/uapi/linux/tlv_parser.h | 59 ++++++++++
lib/Kconfig | 3 +
lib/Makefile | 3 +
lib/tlv_parser.c | 203 ++++++++++++++++++++++++++++++++
lib/tlv_parser.h | 17 +++
7 files changed, 321 insertions(+)
create mode 100644 include/linux/tlv_parser.h
create mode 100644 include/uapi/linux/tlv_parser.h
create mode 100644 lib/tlv_parser.c
create mode 100644 lib/tlv_parser.h

diff --git a/MAINTAINERS b/MAINTAINERS
index aee340630ec..220463b2e51 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -21456,6 +21456,14 @@ W: http://sourceforge.net/projects/tlan/
F: Documentation/networking/device_drivers/ethernet/ti/tlan.rst
F: drivers/net/ethernet/ti/tlan.*

+TLV PARSER
+M: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+L: linux-kernel@xxxxxxxxxxxxxxx
+S: Maintained
+F: include/linux/tlv_parser.h
+F: include/uapi/linux/tlv_parser.h
+F: lib/tlv_parser.*
+
TMIO/SDHI MMC DRIVER
M: Wolfram Sang <wsa+renesas@xxxxxxxxxxxxxxxxxxxx>
L: linux-mmc@xxxxxxxxxxxxxxx
diff --git a/include/linux/tlv_parser.h b/include/linux/tlv_parser.h
new file mode 100644
index 00000000000..7c673b5635e
--- /dev/null
+++ b/include/linux/tlv_parser.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Header file of TLV parser.
+ */
+
+#ifndef _LINUX_TLV_PARSER_H
+#define _LINUX_TLV_PARSER_H
+
+#include <uapi/linux/tlv_parser.h>
+
+typedef int (*parse_callback)(void *, __u64, const __u8 *, __u64);
+
+int tlv_parse_hdr(const __u8 **data, size_t *data_len, __u64 *parsed_data_type,
+ __u64 *parsed_num_fields, __u64 *parsed_total_len,
+ const char **data_types, __u64 num_data_types);
+int tlv_parse_data(parse_callback callback, void *callback_data,
+ __u64 parsed_num_fields, const __u8 *data, size_t data_len,
+ const char **fields, __u64 num_fields);
+int tlv_parse(__u64 expected_data_type, parse_callback callback,
+ void *callback_data, const __u8 *data, size_t data_len,
+ const char **data_types, __u64 num_data_types,
+ const char **fields, __u64 num_fields);
+
+#endif /* _LINUX_TLV_PARSER_H */
diff --git a/include/uapi/linux/tlv_parser.h b/include/uapi/linux/tlv_parser.h
new file mode 100644
index 00000000000..fe87be4914d
--- /dev/null
+++ b/include/uapi/linux/tlv_parser.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+/*
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Implement the user space interface for the TLV parser.
+ */
+
+#ifndef _UAPI_LINUX_TLV_PARSER_H
+#define _UAPI_LINUX_TLV_PARSER_H
+
+#include <linux/types.h>
+
+/*
+ * TLV format:
+ *
+ * +-----------------+------------------+-----------------+
+ * | data type (u64) | num fields (u64) | total len (u64) | # header
+ * +--------------+--+---------+--------+---------+-------+
+ * | field1 (u64) | len1 (u64) | value1 (u8 len1) |
+ * +--------------+------------+------------------+
+ * | ... | ... | ... | # data
+ * +--------------+------------+------------------+
+ * | fieldN (u64) | lenN (u64) | valueN (u8 lenN) |
+ * +--------------+------------+------------------+
+ */
+
+/**
+ * struct tlv_hdr - Header of TLV format
+ * @data_type: Type of data to parse
+ * @num_fields: Number of fields provided
+ * @_reserved: Reserved for future use
+ * @total_len: Total length of the data blob, excluding the header
+ *
+ * This structure represents the header of the TLV data format.
+ */
+struct tlv_hdr {
+ __u64 data_type;
+ __u64 num_fields;
+ __u64 _reserved;
+ __u64 total_len;
+} __packed;
+
+/**
+ * struct tlv_entry - Data entry of TLV format
+ * @field: Data field identifier
+ * @length: Data length
+ * @data: Data
+ *
+ * This structure represents a TLV entry of the data part of TLV data format.
+ */
+struct tlv_entry {
+ __u64 field;
+ __u64 length;
+ __u8 data[];
+} __packed;
+
+#endif /* _UAPI_LINUX_TLV_PARSER_H */
diff --git a/lib/Kconfig b/lib/Kconfig
index 5c2da561c51..cea8d2c87b1 100644
--- a/lib/Kconfig
+++ b/lib/Kconfig
@@ -763,3 +763,6 @@ config ASN1_ENCODER

config POLYNOMIAL
tristate
+
+config TLV_PARSER
+ bool
diff --git a/lib/Makefile b/lib/Makefile
index 42d307ade22..ad55cd6c25b 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -432,3 +432,6 @@ $(obj)/$(TEST_FORTIFY_LOG): $(addprefix $(obj)/, $(TEST_FORTIFY_LOGS)) FORCE
ifeq ($(CONFIG_FORTIFY_SOURCE),y)
$(obj)/string.o: $(obj)/$(TEST_FORTIFY_LOG)
endif
+
+obj-$(CONFIG_TLV_PARSER) += tlv_parser.o
+CFLAGS_tlv_parser.o += -I lib
diff --git a/lib/tlv_parser.c b/lib/tlv_parser.c
new file mode 100644
index 00000000000..c28e5584968
--- /dev/null
+++ b/lib/tlv_parser.c
@@ -0,0 +1,203 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Implement the TLV parser.
+ */
+
+#define pr_fmt(fmt) "TLV PARSER: "fmt
+#include <tlv_parser.h>
+
+/**
+ * tlv_parse_hdr - Parse a TLV header
+ * @data: Data to parse (updated)
+ * @data_len: Length of @data (updated)
+ * @parsed_data_type: Parsed data type (updated)
+ * @parsed_num_fields: Parsed data fields (updated)
+ * @parsed_total_len: Length of parsed data part, excluding the header (updated)
+ * @data_types: Array of data type strings
+ * @num_data_types: Number of elements of @data_types
+ *
+ * Parse the header of the TLV data format, update the data pointer and length,
+ * and provide the data type, number of fields and the length of that element.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse_hdr(const __u8 **data, size_t *data_len, __u64 *parsed_data_type,
+ __u64 *parsed_num_fields, __u64 *parsed_total_len,
+ const char **data_types, __u64 num_data_types)
+{
+ struct tlv_hdr *hdr;
+
+ if (*data_len < sizeof(*hdr)) {
+ pr_debug("Data blob too short, %lu bytes, expected %lu\n",
+ *data_len, sizeof(*hdr));
+ return -EBADMSG;
+ }
+
+ hdr = (struct tlv_hdr *)*data;
+
+ *data += sizeof(*hdr);
+ *data_len -= sizeof(*hdr);
+
+ *parsed_data_type = __be64_to_cpu(hdr->data_type);
+ if (*parsed_data_type >= num_data_types) {
+ pr_debug("Invalid data type %llu, max: %llu\n",
+ *parsed_data_type, num_data_types - 1);
+ return -EBADMSG;
+ }
+
+ *parsed_num_fields = __be64_to_cpu(hdr->num_fields);
+
+ if (hdr->_reserved != 0) {
+ pr_debug("_reserved must be zero\n");
+ return -EBADMSG;
+ }
+
+ *parsed_total_len = __be64_to_cpu(hdr->total_len);
+ if (*parsed_total_len > *data_len) {
+ pr_debug("Invalid total length %llu, expected: %lu\n",
+ *parsed_total_len, *data_len);
+ return -EBADMSG;
+ }
+
+ pr_debug("Header: type: %s, num fields: %llu, total len: %lld\n",
+ data_types[*parsed_data_type], *parsed_num_fields,
+ *parsed_total_len);
+
+ return 0;
+}
+
+/**
+ * tlv_parse_data - Parse a TLV data
+ * @callback: Callback function to call to parse the fields
+ * @callback_data: Opaque data to supply to the callback function
+ * @parsed_num_fields: Parsed data fields
+ * @data: Data to parse
+ * @data_len: Length of @data
+ * @fields: Array of field strings
+ * @num_fields: Number of elements of @fields
+ *
+ * Parse the data part of the TLV data format and call the supplied callback
+ * function for each data field, passing also the opaque data pointer.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse_data(parse_callback callback, void *callback_data,
+ __u64 parsed_num_fields, const __u8 *data, size_t data_len,
+ const char **fields, __u64 num_fields)
+{
+ const __u8 *data_ptr = data;
+ struct tlv_entry *entry;
+ __u64 parsed_field;
+ __u64 len;
+ int ret, i;
+
+ for (i = 0; i < parsed_num_fields; i++) {
+ if (data_len < sizeof(*entry))
+ return -EBADMSG;
+
+ entry = (struct tlv_entry *)data_ptr;
+ data_ptr += sizeof(*entry);
+ data_len -= sizeof(*entry);
+
+ parsed_field = __be64_to_cpu(entry->field);
+ if (parsed_field >= num_fields) {
+ pr_debug("Invalid field %llu, max: %llu\n",
+ parsed_field, num_fields - 1);
+ return -EBADMSG;
+ }
+
+ len = __be64_to_cpu(entry->length);
+
+ if (data_len < len)
+ return -EBADMSG;
+
+ pr_debug("Data: field: %s, len: %llu\n", fields[parsed_field],
+ len);
+
+ if (!len)
+ continue;
+
+ ret = callback(callback_data, parsed_field, data_ptr, len);
+ if (ret < 0) {
+ pr_debug("Parsing of field %s failed, ret: %d\n",
+ fields[parsed_field], ret);
+ return -EBADMSG;
+ }
+
+ data_ptr += len;
+ data_len -= len;
+ }
+
+ if (data_len) {
+ pr_debug("Excess data: %ld bytes\n", data_len);
+ return -EBADMSG;
+ }
+
+ return 0;
+}
+
+/**
+ * tlv_parse - Parse data in TLV format
+ * @expected_data_type: Desired data type
+ * @callback: Callback function to call to parse the fields
+ * @callback_data: Opaque data to supply to the callback function
+ * @data: Data to parse
+ * @data_len: Length of @data
+ * @data_types: Array of data type strings
+ * @num_data_types: Number of elements of @data_types
+ * @fields: Array of field strings
+ * @num_fields: Number of elements of @fields
+ *
+ * Parse data in TLV format and call the supplied callback function for each
+ * data field, passing also the opaque data pointer.
+ *
+ * Return: Zero on success, a negative value on error.
+ */
+int tlv_parse(__u64 expected_data_type, parse_callback callback,
+ void *callback_data, const __u8 *data, size_t data_len,
+ const char **data_types, __u64 num_data_types,
+ const char **fields, __u64 num_fields)
+{
+ __u64 parsed_data_type;
+ __u64 parsed_num_fields;
+ __u64 parsed_total_len;
+ int ret = 0;
+
+ pr_debug("Start parsing data blob, size: %ld, expected data type: %s\n",
+ data_len, data_types[expected_data_type]);
+
+ while (data_len) {
+ ret = tlv_parse_hdr(&data, &data_len, &parsed_data_type,
+ &parsed_num_fields, &parsed_total_len,
+ data_types, num_data_types);
+ if (ret < 0)
+ goto out;
+
+ if (parsed_data_type == expected_data_type)
+ break;
+
+ /*
+ * tlv_parse_hdr() already checked that
+ * parsed_total_len <= data_len.
+ */
+ data += parsed_total_len;
+ data_len -= parsed_total_len;
+ }
+
+ if (!data_len) {
+ pr_debug("Data type %s not found\n",
+ data_types[expected_data_type]);
+ ret = -ENOENT;
+ goto out;
+ }
+
+ ret = tlv_parse_data(callback, callback_data, parsed_num_fields, data,
+ parsed_total_len, fields, num_fields);
+out:
+ pr_debug("End of parsing data blob, ret: %d\n", ret);
+ return ret;
+}
diff --git a/lib/tlv_parser.h b/lib/tlv_parser.h
new file mode 100644
index 00000000000..b196c6edbf0
--- /dev/null
+++ b/lib/tlv_parser.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2023 Huawei Technologies Duesseldorf GmbH
+ *
+ * Author: Roberto Sassu <roberto.sassu@xxxxxxxxxx>
+ *
+ * Header file of TLV parser.
+ */
+
+#ifndef _LIB_TLV_PARSER_H
+#define _LIB_TLV_PARSER_H
+
+#include <linux/kernel.h>
+#include <linux/err.h>
+#include <linux/tlv_parser.h>
+
+#endif /* _LIB_TLV_PARSER_H */
--
2.34.1