From: Lennart Poettering Date: Wed, 15 Apr 2026 14:09:48 +0000 (+0200) Subject: bootctl: add helpers that format a type1 menu entry filename from a commit nr X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=4d0f6ac5931c051871e46e98a2ff7eb37136ea57;p=thirdparty%2Fsystemd.git bootctl: add helpers that format a type1 menu entry filename from a commit nr --- diff --git a/src/bootctl/bootspec-util.c b/src/bootctl/bootspec-util.c index b96687430ca..5f9842c9d80 100644 --- a/src/bootctl/bootspec-util.c +++ b/src/bootctl/bootspec-util.c @@ -1,11 +1,18 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include "alloc-util.h" +#include "boot-entry.h" #include "bootspec-util.h" #include "devnum-util.h" #include "efi-loader.h" #include "errno-util.h" #include "log.h" +#include "parse-util.h" +#include "path-util.h" +#include "stdio-util.h" +#include "string-util.h" #include "strv.h" +#include "utf8.h" int boot_config_load_and_select( BootConfig *config, @@ -39,3 +46,210 @@ int boot_config_load_and_select( return boot_config_select_special_entries(config, /* skip_efivars= */ !!root); } + +int boot_entry_make_commit_filename( + const char *entry_token, + uint64_t entry_commit, + const char *version, + unsigned profile_nr, + unsigned tries_left, + char **ret) { + + assert(entry_token); + assert(ret); + + /* Generate a new entry filename from the entry token, the commit number, and (optionally) the + * image/OS version, (if non-zero) the profile number, and (unless UINT_MAX) the number of tries + * left. */ + + if (!boot_entry_token_valid(entry_token)) + return -EINVAL; + if (!entry_commit_valid(entry_commit)) + return -EINVAL; + + _cleanup_free_ char *filename = asprintf_safe("%s-commit_%" PRIu64, entry_token, entry_commit); + if (!filename) + return -ENOMEM; + if (version && !strextend(&filename, ".", version)) + return -ENOMEM; + if (profile_nr > 0 && strextendf(&filename, "@%u", profile_nr) < 0) + return -ENOMEM; + if (tries_left != UINT_MAX && strextendf(&filename, "+%u", tries_left) < 0) + return -ENOMEM; + if (!strextend(&filename, ".conf")) + return -ENOMEM; + + if (!filename_is_valid(filename) || string_has_cc(filename, /* ok= */ NULL) || !utf8_is_valid(filename)) + return -EINVAL; + + *ret = TAKE_PTR(filename); + return 0; +} + +int boot_entry_parse_commit_filename( + const char *filename, + char **ret_entry_token, + uint64_t *ret_entry_commit) { + + int r; + + assert(filename); + + if (!filename_is_valid(filename)) + return -EINVAL; + + _cleanup_free_ char *stripped = NULL; + r = boot_filename_extract_tries(filename, &stripped, /* ret_tries_left= */ NULL, /* ret_tries_done= */ NULL); + if (r < 0) + return r; + + const char *a = strrstr_no_case(stripped, "-commit_"); + if (!a) + return -EBADMSG; + + const char *c = endswith_no_case(stripped, ".conf"); + if (!c) + return -EBADMSG; + + assert(a < c); + + _cleanup_free_ char *entry_token = strndup(stripped, a - stripped); + if (!entry_token) + return -ENOMEM; + + if (!boot_entry_token_valid(entry_token)) + return -EBADMSG; + + const char *b = a + STRLEN("-commit_"); + size_t n = strspn(b, DIGITS); + if (n <= 0 || !IN_SET(b[n], '+', '.', '@')) + return -EBADMSG; + + _cleanup_free_ char *entry_commit_string = strndup(b, n); + if (!entry_commit_string) + return -ENOMEM; + + uint64_t entry_commit; + r = safe_atou64_full(entry_commit_string, 10, &entry_commit); + if (r < 0) + return r; + if (!entry_commit_valid(entry_commit)) + return -EBADMSG; + + if (ret_entry_token) + *ret_entry_token = TAKE_PTR(entry_token); + if (ret_entry_commit) + *ret_entry_commit = entry_commit; + + return 0; +} + +int boot_entry_parse_commit( + BootEntry *entry, + char **ret_entry_token, + uint64_t *ret_entry_commit) { + + int r; + + assert(entry); + + if (entry->type != BOOT_ENTRY_TYPE1) + return -EADDRNOTAVAIL; + + _cleanup_free_ char *fn = NULL; + r = path_extract_filename(entry->path, &fn); + if (r < 0) + return r; + + return boot_entry_parse_commit_filename(fn, ret_entry_token, ret_entry_commit); +} + +int boot_config_find_oldest_commit( + BootConfig *config, + const char *entry_token, + char ***ret_ids) { + + int r; + + assert(config); + assert(entry_token); + assert(ret_ids); + + uint64_t commit_oldest = UINT64_MAX, commit_2nd_oldest = UINT64_MAX, commit_blocked = UINT64_MAX; + + /* First, determine which commit is the oldest (that isn't the current one), and hence the candidate + * to be removed */ + FOREACH_ARRAY(b, config->entries, config->n_entries) { + _cleanup_free_ char *et = NULL; + uint64_t ec; + + r = boot_entry_parse_commit(b, &et, &ec); + if (r == -EADDRNOTAVAIL) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to parse entry filename of '%s', ignoring: %m", strna(b->id)); + continue; + } + + if (!streq(et, entry_token)) /* Not ours? */ + continue; + + if (ec < commit_oldest) { + commit_2nd_oldest = commit_oldest; + commit_oldest = ec; + } else if (ec > commit_oldest && ec < commit_2nd_oldest) + commit_2nd_oldest = ec; + + if (boot_config_selected_entry(config) == b) { + assert(commit_blocked == UINT64_MAX); + commit_blocked = ec; + } + } + + uint64_t commit_picked; + if (commit_oldest == UINT64_MAX) + return log_debug_errno(SYNTHETIC_ERRNO(ENXIO), "No matching entry found while determining oldest entry."); + if (commit_oldest != commit_blocked) + commit_picked = commit_oldest; + else { + if (commit_2nd_oldest == UINT64_MAX) + return log_debug_errno(SYNTHETIC_ERRNO(EBUSY), "Only matching entry found while determining oldest entry is current one, skipping it."); + + assert(commit_2nd_oldest != commit_blocked); + commit_picked = commit_2nd_oldest; + } + + log_debug("Determined commit %" PRIu64 " to be oldest.", commit_picked); + + /* Second loop: actually remove all entries matching this commit (which can be multiple, since UKIs + * have profiles) */ + _cleanup_(strv_freep) char **l = NULL; + FOREACH_ARRAY(b, config->entries, config->n_entries) { + _cleanup_free_ char *et = NULL; + uint64_t ec; + + r = boot_entry_parse_commit(b, &et, &ec); + if (r == -EADDRNOTAVAIL) + continue; + if (r < 0) { + log_debug_errno(r, "Failed to parse entry filename of '%s', ignoring: %m", strna(b->id)); + continue; + } + + if (!streq(et, entry_token)) /* Not ours? */ + continue; + + if (ec != commit_picked) + continue; + + r = strv_extend(&l, b->id); + if (r < 0) + return r; + } + + /* The list cannot be empty, the first loop above and the 2nd loop must have found the same matching + * entries, and if the first loop didn't find any we'd not come this far. */ + assert(!strv_isempty(l)); + *ret_ids = TAKE_PTR(l); + return 0; +} diff --git a/src/bootctl/bootspec-util.h b/src/bootctl/bootspec-util.h index 51dac12b9f4..0824c8040fb 100644 --- a/src/bootctl/bootspec-util.h +++ b/src/bootctl/bootspec-util.h @@ -4,3 +4,15 @@ #include "bootspec.h" int boot_config_load_and_select(BootConfig *config, const char *root, const char *esp_path, dev_t esp_devid, const char *xbootldr_path, dev_t xbootldr_devid); + +static inline bool entry_commit_valid(uint64_t commit) { + return commit > 0 && commit < UINT64_MAX; +} + +int boot_entry_make_commit_filename(const char *entry_token, uint64_t entry_commit, const char *version, unsigned profile_nr, unsigned tries_left, char **ret); + +int boot_entry_parse_commit_filename(const char *filename, char **ret_entry_token, uint64_t *ret_entry_commit); + +int boot_entry_parse_commit(BootEntry *entry, char **ret_entry_token, uint64_t *ret_entry_commit); + +int boot_config_find_oldest_commit(BootConfig *config, const char *entry_token, char ***ret_ids); diff --git a/src/bootctl/meson.build b/src/bootctl/meson.build index f8349df7168..ff33cde3f61 100644 --- a/src/bootctl/meson.build +++ b/src/bootctl/meson.build @@ -25,4 +25,10 @@ executables += [ 'link_with' : boot_link_with, 'dependencies' : [libopenssl_cflags], }, + test_template + { + 'sources' : files( + 'test-bootspec-util.c', + 'bootspec-util.c', + ), + }, ] diff --git a/src/bootctl/test-bootspec-util.c b/src/bootctl/test-bootspec-util.c new file mode 100644 index 00000000000..1fa891469f4 --- /dev/null +++ b/src/bootctl/test-bootspec-util.c @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "alloc-util.h" +#include "bootspec-util.h" +#include "tests.h" + +static void test_one( + const char *entry_token, + uint64_t entry_commit, + const char *version, + unsigned profile_nr, + unsigned tries_left, + const char *expected) { + + _cleanup_free_ char *fn = NULL; + ASSERT_OK(boot_entry_make_commit_filename(entry_token, entry_commit, version, profile_nr, tries_left, &fn)); + ASSERT_STREQ(fn, expected); + + _cleanup_free_ char *token = NULL; + uint64_t commit = 0; + ASSERT_OK(boot_entry_parse_commit_filename(fn, &token, &commit)); + ASSERT_STREQ(token, entry_token); + ASSERT_EQ(commit, entry_commit); +} + +TEST(boot_entry_commit_filename) { + test_one("foo", 1, NULL, 0, UINT_MAX, "foo-commit_1.conf"); + test_one("foo", 42, "1.0", 0, UINT_MAX, "foo-commit_42.1.0.conf"); + test_one("foo", 42, "1.0", 3, UINT_MAX, "foo-commit_42.1.0@3.conf"); + test_one("foo", 42, "1.0", 3, 5, "foo-commit_42.1.0@3+5.conf"); + test_one("foo", 42, NULL, 3, UINT_MAX, "foo-commit_42@3.conf"); + test_one("foo", 42, NULL, 3, 7, "foo-commit_42@3+7.conf"); + test_one("foo", 42, NULL, 0, 9, "foo-commit_42+9.conf"); + test_one("my-token", 123456, "v2", 0, UINT_MAX, "my-token-commit_123456.v2.conf"); + + /* Invalid inputs for make */ + _cleanup_free_ char *fn = NULL; + ASSERT_ERROR(boot_entry_make_commit_filename("foo/bar", 1, NULL, 0, UINT_MAX, &fn), EINVAL); + ASSERT_ERROR(boot_entry_make_commit_filename("foo", 0, NULL, 0, UINT_MAX, &fn), EINVAL); + ASSERT_ERROR(boot_entry_make_commit_filename("foo", UINT64_MAX, NULL, 0, UINT_MAX, &fn), EINVAL); + + /* Invalid inputs for parse */ + _cleanup_free_ char *token = NULL; + uint64_t commit = 0; + ASSERT_ERROR(boot_entry_parse_commit_filename("foo.conf", &token, &commit), EBADMSG); + ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_.conf", &token, &commit), EBADMSG); + ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_abc.conf", &token, &commit), EBADMSG); + ASSERT_ERROR(boot_entry_parse_commit_filename("foo-commit_0.conf", &token, &commit), EBADMSG); +} + +DEFINE_TEST_MAIN(LOG_INFO);