diff --git a/crates/bls/src/lib.rs b/crates/bls/src/lib.rs index 33e0b7d..cac52e2 100644 --- a/crates/bls/src/lib.rs +++ b/crates/bls/src/lib.rs @@ -32,6 +32,12 @@ pub struct BlsEntry { pub initrd: Vec, /// The path to an EFI image. pub efi: Option, + /// The path to a unified kernel image. + pub uki: Option, + /// The profile of the unified kernel image to boot. + pub profile: Option, + /// The architecture the entry is for, such as `x64` or `aa64`. + pub architecture: Option, /// The sort key for the entry. pub sort_key: Option, /// The version of the entry. @@ -60,6 +66,9 @@ impl FromStr for BlsEntry { let mut linux: Option = None; let mut initrd: Vec = Vec::new(); let mut efi: Option = None; + let mut uki: Option = None; + let mut profile: Option = None; + let mut architecture: Option = None; let mut sort_key: Option = None; let mut version: Option = None; let mut machine_id: Option = None; @@ -118,6 +127,21 @@ impl FromStr for BlsEntry { efi = Some(value.trim().to_string()); } + // The path to a unified kernel image. + "uki" => { + uki = Some(value.trim().to_string()); + } + + // The profile of the unified kernel image to boot. + "profile" => { + profile = Some(value.trim().to_string()); + } + + // The architecture the entry is for. + "architecture" => { + architecture = Some(value.trim().to_string()); + } + "sort-key" => { sort_key = Some(value.trim().to_string()); } @@ -144,6 +168,9 @@ impl FromStr for BlsEntry { linux, initrd, efi, + uki, + profile, + architecture, sort_key, version, machine_id, @@ -157,22 +184,50 @@ impl FromStr for BlsEntry { impl BlsEntry { /// Checks if this BLS entry is something we can actually boot in Sprout. pub fn is_valid(&self) -> bool { - self.linux.is_some() || self.efi.is_some() + self.linux.is_some() || self.efi.is_some() || self.uki.is_some() + } + + /// Whether the entry only boots a unified kernel image, which carries its own initrd. + fn is_uki_only(&self) -> bool { + self.linux.is_none() && self.efi.is_none() && self.uki.is_some() + } + + /// Whether the entry is for the architecture named `machine`, such as `x64`. + /// An entry without an architecture is for every architecture. + pub fn matches_architecture(&self, machine: &str) -> bool { + self.architecture + .as_deref() + .is_none_or(|architecture| architecture.eq_ignore_ascii_case(machine)) + } + + /// Fetches the load options to pass to the image that is chainloaded. + /// A unified kernel image with a profile gets `@` first, which selects the profile. + pub fn chainload_options(&self) -> Option { + match (self.is_uki_only(), self.profile.as_deref(), self.options()) { + (true, Some(profile), Some(options)) => Some(format!("@{} {}", profile, options)), + (true, Some(profile), None) => Some(format!("@{}", profile)), + (_, _, options) => options, + } } /// Fetches the path to an EFI bootable image to boot, if any. - /// This prioritizes the linux field over efi. + /// This prioritizes the linux field over efi, and efi over uki. /// It also converts / to \\ to match EFI path style. pub fn chainload_path(&self) -> Option { self.linux .clone() .or(self.efi.clone()) + .or(self.uki.clone()) .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) } /// Fetches the paths to the initrds to pass to the kernel, in order. /// It also converts / to \\ to match EFI path style. pub fn initrd_paths(&self) -> Vec { + // A unified kernel image has its initrd built in. + if self.is_uki_only() { + return Vec::new(); + } self.initrd .iter() .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) @@ -1024,6 +1079,54 @@ mod tests { assert!(resolve_default_flags(&[]).is_empty()); } + #[test] + fn parse_architecture_uki_and_profile() { + let entry: BlsEntry = "uki /EFI/Linux/a.efi\narchitecture x64\nprofile 2\n" + .parse() + .unwrap(); + assert_eq!(entry.uki.as_deref(), Some("/EFI/Linux/a.efi")); + assert_eq!(entry.architecture.as_deref(), Some("x64")); + assert_eq!(entry.profile.as_deref(), Some("2")); + assert!(entry.is_valid()); + } + + #[test] + fn uki_is_used_last_for_chainloading() { + let entry: BlsEntry = "uki /a.efi\nefi /b.efi\n".parse().unwrap(); + assert_eq!(entry.chainload_path().as_deref(), Some("b.efi")); + let entry: BlsEntry = "uki /EFI/Linux/a.efi\n".parse().unwrap(); + assert_eq!(entry.chainload_path().as_deref(), Some("EFI\\Linux\\a.efi")); + } + + #[test] + fn uki_entries_have_no_initrds() { + let entry: BlsEntry = "uki /a.efi\ninitrd /initrd\n".parse().unwrap(); + assert!(entry.initrd_paths().is_empty()); + let entry: BlsEntry = "linux /vmlinuz\ninitrd /initrd\n".parse().unwrap(); + assert_eq!(entry.initrd_paths(), ["initrd"]); + } + + #[test] + fn architecture_must_match_ignoring_case() { + let entry: BlsEntry = "linux /v\narchitecture X64\n".parse().unwrap(); + assert!(entry.matches_architecture("x64")); + assert!(!entry.matches_architecture("aa64")); + let entry: BlsEntry = "linux /v\n".parse().unwrap(); + assert!(entry.matches_architecture("aa64")); + } + + #[test] + fn uki_profile_prefixes_the_load_options() { + let entry: BlsEntry = "uki /a.efi\nprofile 2\noptions quiet\n".parse().unwrap(); + assert_eq!(entry.chainload_options().as_deref(), Some("@2 quiet")); + let entry: BlsEntry = "uki /a.efi\nprofile 2\n".parse().unwrap(); + assert_eq!(entry.chainload_options().as_deref(), Some("@2")); + let entry: BlsEntry = "linux /v\nprofile 2\noptions quiet\n".parse().unwrap(); + assert_eq!(entry.chainload_options().as_deref(), Some("quiet")); + let entry: BlsEntry = "linux /v\n".parse().unwrap(); + assert_eq!(entry.chainload_options(), None); + } + #[test] fn boot_counter_parse_without_suffix() { assert_eq!(BootCounter::parse("foo"), ("foo", None));