/* 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,
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;
+}
--- /dev/null
+/* 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);