use crate::boot_counter::BootCounterTarget; use crate::context::SproutContext; use alloc::format; use alloc::rc::Rc; use alloc::string::{String, ToString}; use edera_sprout_bls::profile_entry_id; use edera_sprout_config::entries::EntryDeclaration; use edera_sprout_parsing::{fnmatch_ignore_case, glob_match}; /// Represents an entry that is stamped and ready to be booted. #[derive(Clone)] pub struct BootableEntry { name: String, title: String, context: Rc, declaration: EntryDeclaration, default: bool, pin_name: bool, sort_key: Option, boot_counter: Option, id_suffix: Option, id_profile: Option, extra_profile: bool, } impl BootableEntry { /// Create a new bootable entry to represent the full context of an entry. pub fn new( name: String, title: String, context: Rc, declaration: EntryDeclaration, ) -> Self { Self { name, title, context, declaration, default: false, pin_name: false, sort_key: None, boot_counter: None, id_suffix: None, id_profile: None, extra_profile: false, } } /// Fetch the name of the entry. This is usually a machine-identifiable key. pub fn name(&self) -> &str { &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) => { let file = format!("{}{}", self.sort_name(), suffix); profile_entry_id(&file, self.id_profile.as_deref()) } None => self.name.clone(), } } /// Fetch the name of the entry without the profile of a unified kernel image, which is /// the same for every profile of the same image. pub fn sort_name(&self) -> &str { self.id_profile .as_deref() .and_then(|profile| { self.name .strip_suffix(profile) .and_then(|name| name.strip_suffix('@')) }) .unwrap_or(&self.name) } /// Fetch whether the entry is a profile of a unified kernel image after the first, which /// is never picked as the default entry unless it was asked for. pub fn is_extra_profile(&self) -> bool { self.id_profile.is_some() || self.extra_profile } /// Mark this entry as booting a profile of a unified kernel image after the first. pub fn mark_extra_profile(&mut self) { self.extra_profile = true; } /// Set the profile of a unified kernel image that is part of the id of this entry. pub fn set_id_profile(&mut self, profile: &str) { self.id_profile = Some(profile.to_string()); } /// 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 } /// Fetch the full context of the entry. pub fn context(&self) -> Rc { Rc::clone(&self.context) } /// Fetch the declaration of the entry. pub fn declaration(&self) -> &EntryDeclaration { &self.declaration } /// Fetch whether the entry is the default entry. pub fn is_default(&self) -> bool { self.default } /// Fetch whether the entry is pinned, which prevents prefixing. pub fn is_pin_name(&self) -> bool { self.pin_name } /// Swap out the context of the entry. pub fn swap_context(&mut self, context: Rc) { self.context = context; } /// Restamp the title with the current context. pub fn restamp_title(&mut self) { self.title = self.context.stamp(&self.title); } /// Restamp the declared sort key with the current context. /// A sort key that was set on this entry directly is left as is. pub fn restamp_sort_key(&mut self) { if self.sort_key.is_none() && let Some(ref sort_key) = self.declaration.sort_key { self.sort_key = Some(self.context.stamp(sort_key)); } } /// Mark this entry as the default entry. pub fn mark_default(&mut self) { self.default = true; } // Unmark this entry as the default entry. pub fn unmark_default(&mut self) { self.default = false; } /// Mark this entry as being pinned, which prevents prefixing. pub fn mark_pin_name(&mut self) { self.pin_name = true; } /// Record where the boot counter of this entry is stored. pub fn set_boot_counter(&mut self, target: BootCounterTarget) { self.boot_counter = Some(target); } /// Fetch where the boot counter of this entry is stored, if it has one. pub fn boot_counter(&self) -> Option<&BootCounterTarget> { self.boot_counter.as_ref() } /// Fetch whether the entry has a boot counter with no tries left. pub fn is_bad(&self) -> bool { self.boot_counter .as_ref() .is_some_and(|target| target.counter.is_bad()) } /// Prepend the name of the entry with `prefix`. pub fn prepend_name_prefix(&mut self, prefix: &str) { self.name.insert_str(0, prefix); } /// Determine if this entry matches `needle` by comparing to the name or title of the entry. /// The `needle` is a glob pattern, where `*` matches any sequence of characters. /// A `needle` without any `*` must equal the name or title exactly. pub fn is_match(&self, needle: &str) -> bool { glob_match(needle, &self.name) || glob_match(needle, &self.title) } /// 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 { 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`. /// The actions of the entry are stamped with `context` when the entry is booted. /// Variants of the same entry keep the sort key of this entry, so they stay grouped /// together in the order they were created in. pub fn variant(&self, suffix: &str, context: Rc) -> Self { let mut entry = self.clone(); entry.name.push_str(suffix); // Without any sort key, the name is used to sort, which differs between variants. // Pin the sort key to the name of this entry to keep the variants together. if entry.sort_key.is_none() && entry.declaration.sort_key.is_none() { entry.sort_key = Some(self.name.clone()); } entry.context = context; entry } /// Set the sort key of the entry. This is used to sort entries via version comparison. pub fn set_sort_key(&mut self, sort_key: String) { self.sort_key = Some(sort_key); } /// Retrieve a reference to the sort key of the entry. If one is not specified, we will use the /// name of the entry. pub fn sort_key(&self) -> &str { // Use the sort key specified in the bootable entry, or use the declaration sort key, // or use the name of the entry. self.sort_key .as_deref() .or(self.declaration.sort_key.as_deref()) .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, ) -> 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>( needle: &str, haystack: impl Iterator, ) -> Option<&'a BootableEntry> { haystack .enumerate() .find(|(index, entry)| entry.is_match(needle) || index.to_string() == needle) .map(|(_index, entry)| entry) } }