fix(boot): match and publish BLS entry ids like systemd-boot

Entries now have an id, the file name without the boot counter and with
its .conf or .efi extension in lower case, which is what systemd-boot
publishes in LoaderEntries and LoaderEntrySelected and what bootctl
expects. LoaderEntryDefault, LoaderEntryOneShot and loader.conf match by
id, ignoring case, with *, ? and [a-z], and no longer match titles or
numbers. A type 1 and a type 2 entry with the same name no longer clash.

@saved now works from LoaderEntryDefault as well, stops saving when it
is not asked for, and removes a stale saved entry. LoaderEntryPreferred
and the loader.conf preferred key select an entry that skips bad boot
counter entries. An entry with no tries left that is booted by hand is
counted too, and a read-only entry file is left alone.

Type 1 entries can name an architecture, which hides them on another
architecture, and a uki with an optional profile. Files that start with
auto- are ignored. loader.conf is read lossily, and a failed measurement
no longer stops the boot.
This commit is contained in:
alex authored and Alex Zenla committed 2026-10-03 19:41:39 -07:00
1 parent a26a5f1639
commit 590eeead5e
4 files changed
+181 -73

No files matched your search

+31 -7
View File
@@ -4,7 +4,7 @@ use alloc::format;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use edera_sprout_config::entries::EntryDeclaration;
use edera_sprout_parsing::glob_match;
use edera_sprout_parsing::{fnmatch_ignore_case, glob_match};
/// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)]
@@ -17,6 +17,7 @@ pub struct BootableEntry {
pin_name: bool,
sort_key: Option<String>,
boot_counter: Option<BootCounterTarget>,
id_suffix: Option<String>,
}
impl BootableEntry {
@@ -36,6 +37,7 @@ impl BootableEntry {
pin_name: false,
sort_key: None,
boot_counter: None,
id_suffix: None,
}
}
@@ -44,6 +46,21 @@ impl BootableEntry {
&self.name
}
/// Fetch the id of the entry, which is how the bootloader interface refers to it.
/// A BLS entry has the id systemd-boot gives it: the name with the file extension, such as
/// `fedora.conf`, in lower case. Any other entry has its name as the id.
pub fn id(&self) -> String {
match self.id_suffix {
Some(ref suffix) => format!("{}{}", self.name, suffix).to_lowercase(),
None => self.name.clone(),
}
}
/// Set the file extension that is part of the id of this entry, such as `.conf`.
pub fn set_id_suffix(&mut self, suffix: &str) {
self.id_suffix = Some(suffix.to_string());
}
/// Fetch the title of the entry. This is usually a human-readable key.
pub fn title(&self) -> &str {
&self.title
@@ -133,13 +150,12 @@ impl BootableEntry {
glob_match(needle, &self.name) || glob_match(needle, &self.title)
}
/// Determine if this entry matches `needle` by comparing to the id of the entry, which is
/// the name with or without the `.conf` or `.efi` suffix. This is how loader.conf selects entries.
/// The `needle` is a glob pattern, where `*` matches any sequence of characters.
/// Determine if this entry matches `needle` by comparing to the id or the name of the entry,
/// so that both `fedora.conf` and `fedora` select the entry. This is how loader.conf and the
/// bootloader interface select entries, so the `needle` is a pattern that is matched ignoring
/// case and supports `*`, `?` and `[a-z]`.
pub fn is_match_id(&self, needle: &str) -> bool {
glob_match(needle, &self.name)
|| glob_match(needle, &format!("{}.conf", self.name))
|| glob_match(needle, &format!("{}.efi", self.name))
fnmatch_ignore_case(needle, &self.id()) || fnmatch_ignore_case(needle, &self.name)
}
/// Create a variant of this entry, with the name suffixed by `suffix` and using `context`.
@@ -177,6 +193,14 @@ impl BootableEntry {
.unwrap_or(&self.name)
}
/// Find the first entry that matches `needle` by id inside the entry iterator `haystack`.
pub fn find_id<'a>(
needle: &str,
haystack: impl Iterator<Item = &'a BootableEntry>,
) -> Option<&'a BootableEntry> {
haystack.into_iter().find(|entry| entry.is_match_id(needle))
}
/// Find an entry by `needle` inside the entry iterator `haystack`.
/// This will search for an entry by name, title, or index.
pub fn find<'a>(