[Concept,3/8] boot: Add BLS Type #1 entry parser

Message ID 20260213202417.223068-4-sjg@u-boot.org
State New
Headers
Series Add BLS Type #1 bootmethod with FIT and multi-initrd support |

Commit Message

Simon Glass Feb. 13, 2026, 8:24 p.m. UTC
  From: Simon Glass <simon.glass@canonical.com>

Add a parser for Boot Loader Specification (BLS) Type #1 entry files,
which use a simple key-value format with one field per line.

The parser modifies the input buffer in place, adding null terminators
to delimit fields. Entry structure fields (except options) point
directly into the caller's buffer, which must remain valid for the
lifetime of the entry. The options field is allocated separately to
support concatenation of multiple option lines.

Supported fields:
- title: Human-readable entry name
- version: OS version string
- linux: Kernel path (required)
- options: Kernel command line (concatenated if multiple)
- initrd: Initramfs paths (can appear multiple times)
- devicetree: Device tree blob path
- devicetree-overlay: Device tree overlay path
- architecture: Target architecture
- machine-id: OS identifier
- sort-key: Sorting identifier

Unknown fields are ignored for forward compatibility. Comments (lines
starting with #) and blank lines are skipped.

The implementation includes comprehensive unit tests covering basic
parsing, multiple options/initrds, error handling, and edge cases.

Co-developed-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Simon Glass <simon.glass@canonical.com>
---

 boot/Kconfig          |  21 +++
 boot/Makefile         |   1 +
 boot/bls_parse.c      | 297 ++++++++++++++++++++++++++++++++++++++++++
 include/bls.h         | 100 ++++++++++++++
 test/boot/Makefile    |   1 +
 test/boot/bls_parse.c | 176 +++++++++++++++++++++++++
 6 files changed, 596 insertions(+)
 create mode 100644 boot/bls_parse.c
 create mode 100644 include/bls.h
 create mode 100644 test/boot/bls_parse.c
  

Patch

diff --git a/boot/Kconfig b/boot/Kconfig
index 63f7d228ea8..63a0cf0bd7a 100644
--- a/boot/Kconfig
+++ b/boot/Kconfig
@@ -690,6 +690,27 @@  config BOOTMETH_EXTLINUX_LOCALBOOT
 	  This attempts to find a kernel and initrd on the disk and boot it,
 	  in the case where there is no "localcmd" in the environment.
 
+config BOOTMETH_BLS
+	bool "Bootdev support for Boot Loader Specification (BLS) Type #1"
+	select PXE_UTILS
+	default y if SANDBOX
+	help
+	  Enables support for Boot Loader Specification (BLS) Type #1 entries.
+	  This makes bootdevs look for '*.conf' files in 'loader/entries/'
+	  directories on each filesystem they scan.
+
+	  BLS is a standard for boot entry configuration used by Fedora, RHEL,
+	  and other distributions. Each .conf file describes a single boot
+	  entry with fields like kernel path, initrd, and boot options.
+
+	  The specification is here:
+
+	    https://uapi-group.org/specifications/specs/boot_loader_specification/
+
+	  This supports standard BLS fields (title, linux, initrd, options,
+	  devicetree) and works with U-Boot FIT images using the path#config
+	  syntax.
+
 config BOOTMETH_EFI
 	bool "Bootdev support for EFI boot"
 	depends on EFI_BINARY_EXEC
diff --git a/boot/Makefile b/boot/Makefile
index b2a475d4917..1e75f7ece79 100644
--- a/boot/Makefile
+++ b/boot/Makefile
@@ -29,6 +29,7 @@  obj-$(CONFIG_$(PHASE_)BOOTSTD_PROG) += prog_boot.o
 
 obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX) += ext_pxe_common.o bootmeth_extlinux.o
 obj-$(CONFIG_$(PHASE_)BOOTMETH_EXTLINUX_PXE) += ext_pxe_common.o bootmeth_pxe.o
+obj-$(CONFIG_$(PHASE_)BOOTMETH_BLS) += bls_parse.o
 obj-$(CONFIG_$(PHASE_)BOOTMETH_EFI) += bootmeth_efi.o
 obj-$(CONFIG_$(PHASE_)BOOTMETH_CROS) += bootmeth_cros.o
 obj-$(CONFIG_$(PHASE_)BOOTMETH_FEL) += bootmeth_fel.o
diff --git a/boot/bls_parse.c b/boot/bls_parse.c
new file mode 100644
index 00000000000..9a067736655
--- /dev/null
+++ b/boot/bls_parse.c
@@ -0,0 +1,297 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Boot Loader Specification (BLS) Type #1 parser
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#define LOG_CATEGORY UCLASS_BOOTSTD
+
+#include <bls.h>
+#include <log.h>
+#include <malloc.h>
+#include <linux/ctype.h>
+#include <linux/string.h>
+
+/**
+ * enum bls_token_t - BLS Type #1 field tokens
+ *
+ * Token identifiers for BLS entry fields. Using an enum avoids repeated
+ * string comparisons during parsing.
+ *
+ * Note: The enum values correspond to indices in bls_token_names[]
+ */
+enum bls_token_t {
+	TOK_TITLE = 0,
+	TOK_VERSION,
+	TOK_LINUX,
+	TOK_OPTIONS,
+	TOK_INITRD,
+	TOK_DEVICETREE,
+	TOK_DEVICETREE_OVERLAY,
+	TOK_ARCHITECTURE,
+	TOK_MACHINE_ID,
+	TOK_SORT_KEY,
+
+	TOK_COUNT,
+};
+
+/* BLS field names indexed by enum bls_token_t */
+static const char *const bls_token_names[] = {
+	[TOK_TITLE]		= "title",
+	[TOK_VERSION]		= "version",
+	[TOK_LINUX]		= "linux",
+	[TOK_OPTIONS]		= "options",
+	[TOK_INITRD]		= "initrd",
+	[TOK_DEVICETREE]	= "devicetree",
+	[TOK_DEVICETREE_OVERLAY] = "devicetree-overlay",
+	[TOK_ARCHITECTURE]	= "architecture",
+	[TOK_MACHINE_ID]	= "machine-id",
+	[TOK_SORT_KEY]		= "sort-key",
+};
+
+/**
+ * bls_lookup_token() - Look up a token by name
+ *
+ * @key: Field name to look up
+ * Return: Token enum value, or TOK_COUNT if not recognized
+ */
+static enum bls_token_t bls_lookup_token(const char *key)
+{
+	int i;
+
+	for (i = 0; i < TOK_COUNT; i++) {
+		if (!strcmp(key, bls_token_names[i]))
+			return i;
+	}
+
+	return TOK_COUNT;
+}
+
+/**
+ * bls_append_str() - Append a string to an existing field
+ *
+ * Used for fields that can appear multiple times (e.g., options).
+ * Concatenates with a space separator.
+ *
+ * @fieldp: Pointer to field to append to (may be NULL)
+ * @value: String to append
+ * Return: 0 on success, -ENOMEM if allocation fails
+ */
+static int bls_append_str(char **fieldp, const char *value)
+{
+	size_t old_len, val_len, new_len;
+	char *new_str;
+
+	if (!*fieldp) {
+		*fieldp = strdup(value);
+		return *fieldp ? 0 : -ENOMEM;
+	}
+
+	old_len = strlen(*fieldp);
+	val_len = strlen(value);
+	new_len = old_len + 1 + val_len + 1;  /* +1 for space, +1 for nul */
+
+	new_str = realloc(*fieldp, new_len);
+	if (!new_str)
+		return -ENOMEM;
+
+	new_str[old_len] = ' ';
+	memcpy(new_str + old_len + 1, value, val_len + 1);
+
+	*fieldp = new_str;
+
+	return 0;
+}
+
+/**
+ * bls_skip_whitespace() - Skip leading whitespace
+ *
+ * @strp: Pointer to string pointer (updated to first non-whitespace char)
+ */
+static void bls_skip_whitespace(char **strp)
+{
+	char *p = *strp;
+
+	while (*p && isspace(*p))
+		p++;
+	*strp = p;
+}
+
+/**
+ * bls_parse_line() - Parse a single line from a BLS entry file
+ *
+ * Parses one line of a BLS entry. Lines are in "key value" format where
+ * the key and value are separated by whitespace. The value extends to
+ * the end of the line.
+ *
+ * @line: Line to parse (will be modified)
+ * @keyp: Returns pointer to key string
+ * @valuep: Returns pointer to value string
+ * Return: 0 on success, -EINVAL if line is invalid
+ */
+static int bls_parse_line(char *line, char **keyp, char **valuep)
+{
+	char *p = line;
+	char *key, *value;
+
+	/* Skip leading whitespace */
+	bls_skip_whitespace(&p);
+
+	/* Skip blank lines and comments */
+	if (!*p || *p == '#')
+		return -EINVAL;
+
+	/* Extract key */
+	key = p;
+	while (*p && !isspace(*p))
+		p++;
+	if (!*p)
+		return -EINVAL;	/* No value */
+
+	*p++ = '\0';
+
+	/* Skip whitespace before value */
+	bls_skip_whitespace(&p);
+	if (!*p)
+		return -EINVAL;	/* No value */
+
+	value = p;
+
+	/* Remove trailing whitespace from value */
+	p = value + strlen(value) - 1;
+	while (p >= value && isspace(*p))
+		*p-- = '\0';
+
+	*keyp = key;
+	*valuep = value;
+
+	return 0;
+}
+
+int bls_parse_entry(const char *buf, size_t size, struct bls_entry *entry)
+{
+	char *data = (char *)buf;
+	char *line, *next;
+	bool err = false;
+
+	/* Initialize entry to zero */
+	memset(entry, '\0', sizeof(*entry));
+
+	/* Initialize initrds list */
+	alist_init_struct(&entry->initrds, char *);
+
+	log_debug("parsing BLS entry, size %zx\n", size);
+
+	/* Parse buffer line by line, modifying it in place */
+	line = data;
+	while (line < data + size) {
+		enum bls_token_t token;
+		char *key, *value;
+		int ret;
+
+		/* Find end of line */
+		next = memchr(line, '\n', data + size - line);
+		if (next) {
+			*next = '\0';
+			next++;
+		} else {
+			next = data + size;
+		}
+
+		ret = bls_parse_line(line, &key, &value);
+		if (ret) {
+			line = next;
+			continue;	/* Skip blank lines and comments */
+		}
+
+		log_debug("BLS field: '%s' = '%s'\n", key, value);
+		line = next;
+
+		/* Look up token and parse supported fields */
+		token = bls_lookup_token(key);
+		switch (token) {
+		case TOK_TITLE:
+			/* Point into buffer */
+			entry->title = value;
+			break;
+		case TOK_VERSION:
+			/* Point into buffer */
+			entry->version = value;
+			break;
+		case TOK_LINUX:
+			/* Point into buffer */
+			entry->kernel = value;
+			break;
+		case TOK_OPTIONS:
+			/* Multiple times - allocate and concatenate */
+			if (bls_append_str(&entry->options, value))
+				err = true;
+			break;
+		case TOK_INITRD:
+			/* Multiple times - add pointer to buffer */
+			if (!alist_add(&entry->initrds, value))
+				err = true;
+			break;
+		case TOK_DEVICETREE:
+			/* Point into buffer */
+			entry->devicetree = value;
+			break;
+		case TOK_DEVICETREE_OVERLAY:
+			/* Point into buffer */
+			entry->dt_overlays = value;
+			break;
+		case TOK_ARCHITECTURE:
+			/* Point into buffer */
+			entry->architecture = value;
+			break;
+		case TOK_MACHINE_ID:
+			/* Point into buffer */
+			entry->machine_id = value;
+			break;
+		case TOK_SORT_KEY:
+			/* Point into buffer */
+			entry->sort_key = value;
+			break;
+		default:
+			/* Ignore unknown fields for forward compatibility */
+			log_debug("Ignoring unknown BLS field: %s\n", key);
+			break;
+		}
+	}
+
+	/* Check for errors during parsing */
+	if (err)
+		return -ENOMEM;
+
+	/*
+	 * Validate required fields: BLS spec requires at least one of
+	 * 'linux' or 'efi'. We only support 'linux' for Type #1 entries.
+	 */
+	if (!entry->kernel) {
+		log_err("BLS entry missing required 'linux' field\n");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+void bls_entry_uninit(struct bls_entry *entry)
+{
+	if (!entry)
+		return;
+
+	/*
+	 * Most fields point into the parsed buffer and don't need freeing.
+	 * Only options is allocated (for concatenation of multiple lines).
+	 */
+	free(entry->options);
+
+	/*
+	 * Uninit initrds list. The strings in the list point into the buffer,
+	 * so we don't free them, just the list structure.
+	 */
+	alist_uninit(&entry->initrds);
+}
+
diff --git a/include/bls.h b/include/bls.h
new file mode 100644
index 00000000000..eb32b323f1a
--- /dev/null
+++ b/include/bls.h
@@ -0,0 +1,100 @@ 
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Boot Loader Specification (BLS) Type #1 support
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#ifndef __BLS_H
+#define __BLS_H
+
+#include <alist.h>
+#include <linux/types.h>
+
+/**
+ * struct bls_entry - represents a single BLS boot entry
+ *
+ * This structure holds the parsed fields from a BLS Type #1 entry file.
+ * BLS entries use a simple key-value format with one field per line.
+ *
+ * Most fields point directly into the parsed buffer and are only valid while
+ * the buffer remains valid. The exception is @options which is allocated
+ * because multiple option lines must be concatenated.
+ *
+ * @title: Human-readable name (points into buffer)
+ * @version: OS version string (points into buffer)
+ * @kernel: Kernel path or FIT image - required (points into buffer)
+ *          Can include FIT config syntax: path#config
+ * @options: Kernel command line - ALLOCATED, must be freed
+ *           Multiple options lines are concatenated with spaces
+ * @initrds: List of initrd paths (alist of char * pointing into buffer)
+ *           Multiple initrd lines are supported and accumulated
+ * @devicetree: Device tree blob path (points into buffer)
+ * @dt_overlays: Device tree overlays (points into buffer)
+ * @architecture: Target architecture (points into buffer)
+ * @machine_id: OS identifier (points into buffer)
+ * @sort_key: Sorting identifier (points into buffer)
+ * @filename: Path to .conf file (points into buffer)
+ */
+struct bls_entry {
+	char *title;
+	char *version;
+	char *kernel;
+	char *options;		/* Allocated */
+	struct alist initrds;	/* list of char * into buffer */
+	char *devicetree;
+	char *dt_overlays;
+	char *architecture;
+	char *machine_id;
+	char *sort_key;
+	char *filename;
+};
+
+/**
+ * bls_parse_entry() - Parse a BLS entry file
+ *
+ * Parses the contents of a BLS Type #1 entry file into a pre-allocated
+ * entry structure. The format is simple key-value pairs with one field per
+ * line. Lines starting with '#' are comments and blank lines are ignored.
+ *
+ * The entry is initialized to zero before parsing. Most entry fields will
+ * point directly into the buffer (which is modified to add null terminators).
+ * The buffer must remain valid for the lifetime of the entry. Only the
+ * 'options' field is allocated separately because multiple option lines must
+ * be concatenated.
+ *
+ * The caller must call bls_entry_uninit() on the entry when done, regardless
+ * of whether this function succeeds or fails, to free any allocated memory.
+ *
+ * Supported fields:
+ *   title       - Human-readable name
+ *   version     - OS version string
+ *   linux       - Kernel path (required)
+ *   options     - Kernel command line (allocated, can appear multiple times)
+ *   initrd      - Initramfs path (can appear multiple times)
+ *   devicetree  - Device tree blob path
+ *
+ * Unknown fields are ignored for forward compatibility.
+ *
+ * @buf: Buffer containing the BLS entry file contents (will be modified)
+ * @size: Size of the buffer in bytes
+ * @entry: BLS entry structure to fill in (will be initialized)
+ * Return: 0 on success, -ENOMEM if out of memory, -EINVAL if required fields
+ *         are missing
+ */
+int bls_parse_entry(const char *buf, size_t size, struct bls_entry *entry);
+
+
+/**
+ * bls_entry_uninit() - Clean up a BLS entry's fields
+ *
+ * Frees all allocated fields within the entry but does not free the entry
+ * structure itself. Use this for stack-allocated entries.
+ *
+ * @entry: Entry to clean up
+ */
+void bls_entry_uninit(struct bls_entry *entry);
+
+
+#endif /* __BLS_H */
diff --git a/test/boot/Makefile b/test/boot/Makefile
index 329f4acbd52..a57e8553b2e 100644
--- a/test/boot/Makefile
+++ b/test/boot/Makefile
@@ -4,6 +4,7 @@ 
 
 ifdef CONFIG_UT_BOOTSTD
 obj-$(CONFIG_BOOTSTD) += bootdev.o bootstd_common.o bootflow.o bootmeth.o
+obj-$(CONFIG_BOOTMETH_BLS) += bls_parse.o
 obj-$(CONFIG_CMDLINE) += pxe.o
 obj-$(CONFIG_FIT) += image.o
 obj-$(CONFIG_$(PHASE_)FIT_PRINT) += fit_print.o
diff --git a/test/boot/bls_parse.c b/test/boot/bls_parse.c
new file mode 100644
index 00000000000..01da816aee8
--- /dev/null
+++ b/test/boot/bls_parse.c
@@ -0,0 +1,176 @@ 
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Test for Boot Loader Specification (BLS) parser
+ *
+ * Copyright 2026 Canonical Ltd
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#include <bls.h>
+#include <test/ut.h>
+
+/* Test basic BLS entry parsing */
+static int bls_test_parse_basic(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Test Entry\n"
+		"version 1.0\n"
+		"linux /vmlinuz\n"
+		"options root=/dev/sda\n"
+		"initrd /initrd.img\n"
+		"devicetree /test.dtb\n";
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq_str("Test Entry", entry.title);
+	ut_asserteq_str("1.0", entry.version);
+	ut_asserteq_str("/vmlinuz", entry.kernel);
+	ut_asserteq_str("root=/dev/sda", entry.options);
+	ut_asserteq(1, entry.initrds.count);
+	ut_asserteq_str("/test.dtb", entry.devicetree);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_basic, 0, bootstd);
+
+/* Test multiple options lines are concatenated */
+static int bls_test_parse_multi_options(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Test\n"
+		"linux /vmlinuz\n"
+		"options root=/dev/sda\n"
+		"options quiet splash\n";
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq_str("root=/dev/sda quiet splash", entry.options);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_multi_options, 0, bootstd);
+
+/* Test multiple initrd lines */
+static int bls_test_parse_multi_initrd(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Test\n"
+		"linux /vmlinuz\n"
+		"initrd /initrd1.img\n"
+		"initrd /initrd2.img\n";
+	const char **initrd;
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq(2, entry.initrds.count);
+
+	initrd = alist_get(&entry.initrds, 0, char *);
+	ut_asserteq_str("/initrd1.img", *initrd);
+
+	initrd = alist_get(&entry.initrds, 1, char *);
+	ut_asserteq_str("/initrd2.img", *initrd);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_multi_initrd, 0, bootstd);
+
+/* Test comments and blank lines are ignored */
+static int bls_test_parse_comments(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"# This is a comment\n"
+		"title Test\n"
+		"\n"
+		"# Another comment\n"
+		"linux /vmlinuz\n"
+		"\n";
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq_str("Test", entry.title);
+	ut_asserteq_str("/vmlinuz", entry.kernel);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_comments, 0, bootstd);
+
+/* Test missing required field returns error */
+static int bls_test_parse_missing_kernel(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Test\n"
+		"options root=/dev/sda\n";
+
+	ut_asserteq(-EINVAL, bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_missing_kernel, 0, bootstd);
+
+/* Test unknown fields are ignored */
+static int bls_test_parse_unknown_field(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Test\n"
+		"linux /vmlinuz\n"
+		"unknown_field some_value\n"
+		"another_unknown 123\n";
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq_str("Test", entry.title);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_unknown_field, 0, bootstd);
+
+/* Test all supported fields */
+static int bls_test_parse_all_fields(struct unit_test_state *uts)
+{
+	struct bls_entry entry;
+	char buf[] =
+		"title Full Test\n"
+		"version 2.0.1\n"
+		"linux /boot/vmlinuz-2.0.1\n"
+		"options root=/dev/sda1 ro\n"
+		"options quiet splash\n"
+		"initrd /boot/initrd-2.0.1.img\n"
+		"devicetree /boot/dtb-2.0.1.dtb\n"
+		"devicetree-overlay /boot/overlay.dtbo\n"
+		"architecture x86_64\n"
+		"machine-id abc123\n"
+		"sort-key 001\n";
+	const char **initrd;
+
+	ut_assertok(bls_parse_entry(buf, sizeof(buf) - 1, &entry));
+	ut_asserteq_str("Full Test", entry.title);
+	ut_asserteq_str("2.0.1", entry.version);
+	ut_asserteq_str("/boot/vmlinuz-2.0.1", entry.kernel);
+	ut_asserteq_str("root=/dev/sda1 ro quiet splash", entry.options);
+	ut_asserteq(1, entry.initrds.count);
+	initrd = alist_get(&entry.initrds, 0, char *);
+	ut_asserteq_str("/boot/initrd-2.0.1.img", *initrd);
+	ut_asserteq_str("/boot/dtb-2.0.1.dtb", entry.devicetree);
+	ut_asserteq_str("/boot/overlay.dtbo", entry.dt_overlays);
+	ut_asserteq_str("x86_64", entry.architecture);
+	ut_asserteq_str("abc123", entry.machine_id);
+	ut_asserteq_str("001", entry.sort_key);
+
+	bls_entry_uninit(&entry);
+
+	return 0;
+}
+UNIT_TEST(bls_test_parse_all_fields, 0, bootstd);