use crate::boot_counter::BootCounterTarget; use crate::context::SproutContext; use crate::entries::BootableEntry; use alloc::{ format, rc::Rc, string::{String, ToString}, vec::Vec, }; use anyhow::{Context, Result}; use core::{cmp::Ordering, str::FromStr}; use edera_sprout_bls::{BlsEntry, BootCounter, sort_bls}; use edera_sprout_config::generators::bls::BlsConfiguration; use log::warn; use uefi::{ cstr16, fs::{FileSystem, PathBuf}, proto::device_path::text::{AllowShortcuts, DisplayOnly}, proto::media::fs::SimpleFileSystem, }; /// The number of initrd slots set on each BLS entry. /// Entries set `initrd-0` through `initrd-7`, and slots without an initrd are empty. pub const BLS_INITRD_SLOTS: usize = 8; // TODO(azenla): remove this once variable substitution is implemented. /// This function is used to remove the `tuned_initrd` variable from the initrd paths. /// Fedora uses tuned which adds an initrd that shouldn't be used. fn quirk_initrd_remove_tuned(paths: Vec) -> Vec { paths .into_iter() .filter(|path| path != "$tuned_initrd") .collect() } /// Sorts two entries according to the BLS sort system. /// Reference: fn sort_entries(a: &(BlsEntry, BootableEntry), b: &(BlsEntry, BootableEntry)) -> Ordering { let (a_bls, a_boot) = a; let (b_bls, b_boot) = b; sort_bls(a_bls, a_boot.name(), b_bls, b_boot.name()) } /// Generates entries from the BLS entries directory using the specified `bls` configuration and /// `context`. The BLS conversion is best-effort and will ignore any unsupported entries. pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result> { let mut entries = Vec::new(); // Stamp the path to the BLS directory. let path = context.stamp(&bls.path); // Resolve the path to the BLS directory. let bls_resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &path) .context("unable to resolve bls path")?; // Construct a filesystem path to the BLS entries directory. let mut entries_path = PathBuf::from( bls_resolved .sub_path .to_string16(DisplayOnly(false), AllowShortcuts(false)) .context("unable to convert bls path to string")?, ); entries_path.push(cstr16!("entries")); // Open exclusive access to the BLS filesystem. let fs = uefi::boot::open_protocol_exclusive::(bls_resolved.filesystem_handle) .context("unable to open bls filesystem")?; let mut fs = FileSystem::new(fs); // The entries directory is optional, as a BLS directory may only contain a loader.conf file. // If it does not exist, there are no entries to generate. if !fs .try_exists(&entries_path) .context("unable to check for bls entries directory")? { return Ok(Vec::new()); } // Read the BLS entries directory. let entries_iter = fs .read_dir(&entries_path) .context("unable to read bls entries")?; // For each entry in the BLS entries directory, parse the entry and add it to the list. for entry in entries_iter { // Unwrap the entry file info. let entry = entry.context("unable to read bls item entry")?; // Skip items that are not regular files. if !entry.is_regular_file() { continue; } // Get the file name of the filesystem item. let mut name = entry.file_name().to_string(); // Ignore files that are not .conf files. if !name.to_lowercase().ends_with(".conf") { continue; } // Remove the .conf extension. name.truncate(name.len() - 5); // Skip over files that are named just ".conf" as they are not valid entry files. if name.is_empty() { continue; } // Split the boot counter off the file name. The entry id never includes the counter, so // that it stays the same as tries are consumed. let file_name = entry.file_name().to_string(); let (id, boot_counter) = { let (id, counter) = BootCounter::parse(&name); (id.to_string(), counter) }; let name = id; // Create a mutable path so we can append the file name to produce the full path. let mut full_entry_path = entries_path.to_path_buf(); full_entry_path.push(entry.file_name()); // Read the entry file. // Entries that can't be read or parsed are skipped, as one broken entry // should not prevent booting any of the other entries. let content = match fs.read(full_entry_path) { Ok(content) => content, Err(error) => { warn!("unable to read bls entry {}: {}", name, error); continue; } }; // Parse the entry file as a UTF-8 string. let content = match String::from_utf8(content) { Ok(content) => content, Err(error) => { warn!("unable to read bls entry {} as utf8: {}", name, error); continue; } }; // Parse the entry file as a BLS entry. let mut entry = match BlsEntry::from_str(&content) { Ok(entry) => entry, Err(error) => { warn!("unable to parse bls entry {}: {}", name, error); continue; } }; entry.boot_counter = boot_counter; // Ignore entries that are not valid for Sprout. if !entry.is_valid() { continue; } // Put the initrds through a quirk modifier to support Fedora. let initrds = quirk_initrd_remove_tuned(entry.initrd_paths()); // Skip entries with more initrds than there are slots, as they can't be fully loaded. if initrds.len() > BLS_INITRD_SLOTS { warn!( "bls entry {} has more than {} initrds, skipping", name, BLS_INITRD_SLOTS ); continue; } // Produce a new sprout context for the entry with the extracted values. let mut context = context.fork(); let title_base = entry.title().unwrap_or_else(|| name.clone()); let chainload = entry.chainload_path().unwrap_or_default(); let options = entry.options().unwrap_or_default(); let version = entry.version().unwrap_or_default(); let machine_id = entry.machine_id().unwrap_or_default(); // Combine the title with the version if a version is present, except if it already contains it. // Sometimes BLS will have a version in the title already, and this makes it unique. let title_full = if !version.is_empty() && !title_base.contains(&version) { format!("{} {}", title_base, version) } else { title_base.clone() }; context.set("title-base", title_base); context.set("title", title_full); context.set("chainload", chainload); context.set("options", options); // The initrd value keeps the last initrd, which is what it held before // multiple initrds were supported. context.set("initrd", initrds.last().cloned().unwrap_or_default()); // Set every initrd slot, even the unused ones. An unset slot would let "$initrd-1" // match "$initrd" instead, producing the initrd value followed by "-1". for slot in 0..BLS_INITRD_SLOTS { context.set( format!("initrd-{}", slot), initrds.get(slot).cloned().unwrap_or_default(), ); } context.set("version", version); context.set("machine-id", machine_id); // Keep the id for the boot counter, as the name is moved into the bootable entry. let boot_id = name.clone(); // Produce a new bootable entry. let mut boot = BootableEntry::new( name, bls.entry.title.clone(), context.freeze(), bls.entry.clone(), ); // Pin the entry name to prevent prefixing. // This is needed as the bootloader interface requires the name to be // the same as the entry file name, minus the .conf extension. if bls.pin_names { boot.mark_pin_name(); } // Record where the boot counter lives so a try can be consumed when this entry boots. if let Some(counter) = boot_counter { boot.set_boot_counter(BootCounterTarget { counter, filesystem: bls_resolved.filesystem_handle, directory: entries_path.clone(), id: boot_id, file_name, }); } // Add the BLS entry to the list, along with the bootable entry. entries.push((entry, boot)); } // Sort all the entries according to the BLS sort system. entries.sort_by(sort_entries); // Grab the number of entries that we have, so we can calculate a reverse index. let entry_count = entries.len(); // Set the sort keys of all the bootable entries to a semi-unique prefix + the BLS sort order. // The final comparison happens using version comparison, so this will sort // things properly. for (idx, (_bls, boot)) in entries.iter_mut().enumerate() { boot.set_sort_key(format!("bls-{}-{}", path, entry_count - idx - 1)); } // Collect all the bootable entries and return them. Ok(entries.into_iter().map(|(_, boot)| boot).collect()) }