bootctl: add helpers that format a type1 menu entry filename from a commit nr

This commit is contained in:
Lennart Poettering
2026-04-15 16:09:48 +02:00
parent d292874bf6
commit 4d0f6ac593
4 changed files with 283 additions and 0 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -25,4 +25,10 @@ executables += [
'link_with' : boot_link_with,
'dependencies' : [libopenssl_cflags],
},
test_template + {
'sources' : files(
'test-bootspec-util.c',
'bootspec-util.c',
),
},
]

View File

@@ -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);