/* * Copyright (C) 2020 Red Hat, Inc. * * SPDX-License-Identifier: Apache-2.0 */ use std::cell::RefCell; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use std::process::Command; use anyhow::{bail, Context, Result}; use bootc_internal_utils::CommandRunExt; use camino::{Utf8Path, Utf8PathBuf}; use cap_std::fs::Dir; use cap_std_ext::cap_std; use chrono::prelude::*; use fn_error_context::context; use openat_ext::OpenatDirExt; use os_release::OsRelease; use rustix::fd::BorrowedFd; use walkdir::WalkDir; use widestring::U16CString; use crate::bootupd::RootContext; use crate::freezethaw::fsfreeze_thaw_cycle; use crate::model::*; use crate::ostreeutil; use crate::util; use crate::{blockdev, filetree, grubconfigs}; use crate::{component::*, packagesystem}; /// Well-known paths to the ESP that may have been mounted external to us. pub(crate) const ESP_MOUNTS: &[&str] = &["boot/efi", "efi", "boot"]; /// New efi dir under usr/lib const EFILIB: &str = "usr/lib/efi"; /// The binary to change EFI boot ordering const EFIBOOTMGR: &str = "efibootmgr"; #[cfg(target_arch = "aarch64")] pub(crate) const SHIM: &str = "shimaa64.efi"; #[cfg(target_arch = "x86_64")] pub(crate) const SHIM: &str = "shimx64.efi"; #[cfg(target_arch = "riscv64")] pub(crate) const SHIM: &str = "shimriscv64.efi"; /// Systemd boot loader info EFI variable names const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"; /// Return `true` if the system is booted via EFI pub(crate) fn is_efi_booted() -> Result { Path::new("/sys/firmware/efi") .try_exists() .map_err(Into::into) } #[derive(Default)] pub(crate) struct Efi { mountpoint: RefCell>, } impl Efi { // Get mounted point for esp pub(crate) fn get_mounted_esp(&self, root: &Path) -> Result> { // First check all potential mount points without holding the borrow let mut found_mount = None; for &mnt in ESP_MOUNTS.iter() { let path = root.join(mnt); if !path.exists() { continue; } let st = rustix::fs::statfs(&path)?; if st.f_type == libc::MSDOS_SUPER_MAGIC { util::ensure_writable_mount(&path)?; found_mount = Some(path); break; } } // Only borrow mutably if we found a mount point if let Some(mnt) = found_mount { log::debug!("Reusing existing mount point {mnt:?}"); *self.mountpoint.borrow_mut() = Some(mnt.clone()); Ok(Some(mnt)) } else { Ok(None) } } // Mount the passed esp_device, return mount point pub(crate) fn mount_esp_device(&self, root: &Path, esp_device: &Path) -> Result { let mut mountpoint = None; for &mnt in ESP_MOUNTS.iter() { let mnt = root.join(mnt); if !mnt.exists() { continue; } std::process::Command::new("mount") .arg(&esp_device) .arg(&mnt) .run() .with_context(|| format!("Failed to mount {:?}", esp_device))?; log::debug!("Mounted at {mnt:?}"); mountpoint = Some(mnt); break; } let mnt = mountpoint.ok_or_else(|| anyhow::anyhow!("No mount point found"))?; *self.mountpoint.borrow_mut() = Some(mnt.clone()); Ok(mnt) } // Firstly check if esp is already mounted, then mount the passed esp device pub(crate) fn ensure_mounted_esp(&self, root: &Path, esp_device: &Path) -> Result { if let Some(mountpoint) = self.mountpoint.borrow().as_deref() { return Ok(mountpoint.to_owned()); } let destdir = if let Some(destdir) = self.get_mounted_esp(Path::new(root))? { destdir } else { self.mount_esp_device(root, esp_device)? }; Ok(destdir) } fn unmount(&self) -> Result<()> { if let Some(mount) = self.mountpoint.borrow_mut().take() { Command::new("umount") .arg(&mount) .run() .with_context(|| format!("Failed to unmount {mount:?}"))?; log::trace!("Unmounted"); } Ok(()) } #[context("Updating EFI firmware variables")] fn update_firmware(&self, device: &str, espdir: &openat::Dir, vendordir: &str) -> Result<()> { if !is_efi_booted()? { log::debug!("Not booted via EFI, skipping firmware update"); return Ok(()); } let sysroot = Dir::open_ambient_dir("/", cap_std::ambient_authority())?; let product_name = get_product_name(&sysroot)?; log::debug!("Get product name: '{product_name}'"); assert!(product_name.len() > 0); // clear all the boot entries that match the target name clear_efi_target(&product_name)?; create_efi_boot_entry(device, espdir, vendordir, &product_name) } } #[context("Get product name")] fn get_product_name(sysroot: &Dir) -> Result { let release_path = "etc/system-release"; if sysroot.exists(release_path) { let content = sysroot.read_to_string(release_path)?; let re = regex::Regex::new(r" *release.*").unwrap(); let name = re.replace_all(&content, "").trim().to_string(); return Ok(name); } // Read /etc/os-release let release: OsRelease = OsRelease::new()?; Ok(release.name) } /// Convert a nul-terminated UTF-16 byte array to a String. fn string_from_utf16_bytes(slice: &[u8]) -> String { // For some reason, systemd appends 3 nul bytes after the string. // Drop the last byte if there's an odd number. let size = slice.len() / 2; let v: Vec = (0..size) .map(|i| u16::from_ne_bytes([slice[2 * i], slice[2 * i + 1]])) .collect(); U16CString::from_vec(v).unwrap().to_string_lossy() } /// Read a nul-terminated UTF-16 string from an EFI variable. fn read_efi_var_utf16_string(name: &str) -> Option { let efivars = Path::new("/sys/firmware/efi/efivars"); if !efivars.exists() { log::trace!("No efivars mount at {:?}", efivars); return None; } let path = efivars.join(name); if !path.exists() { log::trace!("No EFI variable {name}"); return None; } match std::fs::read(&path) { Ok(buf) => { // Skip the first 4 bytes, those are the EFI variable attributes. if buf.len() < 4 { log::warn!("Read less than 4 bytes from {:?}", path); return None; } Some(string_from_utf16_bytes(&buf[4..])) } Err(reason) => { log::warn!("Failed reading {:?}: {reason}", path); None } } } /// Read the LoaderInfo EFI variable if it exists. fn get_loader_info() -> Option { read_efi_var_utf16_string(LOADER_INFO_VAR_STR) } /// Read the StubInfo EFI variable if it exists. fn get_stub_info() -> Option { read_efi_var_utf16_string(STUB_INFO_VAR_STR) } /// Whether to skip adoption if a systemd bootloader is found. fn skip_systemd_bootloaders() -> bool { if let Some(loader_info) = get_loader_info() { if loader_info.starts_with("systemd") { log::trace!("Skipping adoption for {:?}", loader_info); return true; } } if let Some(stub_info) = get_stub_info() { log::trace!("Skipping adoption for {:?}", stub_info); return true; } false } impl Component for Efi { fn name(&self) -> &'static str { "EFI" } fn query_adopt(&self, devices: &Option>) -> Result> { if devices.is_none() { log::trace!("No ESP detected"); return Ok(None); }; // Don't adopt if the system is booted with systemd-boot or // systemd-stub since those will be managed with bootctl. if skip_systemd_bootloaders() { return Ok(None); } crate::component::query_adopt_state() } // Backup "/boot/efi/EFI/{vendor}/grub.cfg" to "/boot/efi/EFI/{vendor}/grub.cfg.bak" // Replace "/boot/efi/EFI/{vendor}/grub.cfg" with new static "grub.cfg" fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> { let sysroot = openat::Dir::open(sysroot_path).with_context(|| format!("Opening {sysroot_path}"))?; let Some(vendor) = self.get_efi_vendor(&sysroot)? else { anyhow::bail!("Failed to find efi vendor"); }; // destdir is /boot/efi/EFI let efidir = destdir .sub_dir(&vendor) .with_context(|| format!("Opening EFI/{}", vendor))?; if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP)? { println!("Creating a backup of the current GRUB config on EFI"); efidir .copy_file(grubconfigs::GRUBCONFIG, grubconfigs::GRUBCONFIG_BACKUP) .context("Failed to backup GRUB config")?; } grubconfigs::install(&sysroot, Some(&vendor), true)?; // Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk. fsfreeze_thaw_cycle(efidir.open_file(".")?)?; Ok(()) } /// Given an adoptable system and an update, perform the update. fn adopt_update( &self, rootcxt: &RootContext, updatemeta: &ContentMetadata, with_static_config: bool, ) -> Result> { let esp_devices = blockdev::find_colocated_esps(&rootcxt.devices)?; let Some(meta) = self.query_adopt(&esp_devices)? else { return Ok(None); }; let updated = rootcxt .sysroot .sub_dir(&component_updatedirname(self)) .context("opening update dir")?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; let esp_devices = esp_devices.unwrap_or_default(); for esp in esp_devices { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?; let efidir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; validate_esp_fstype(&efidir)?; // For adoption, we should only touch files that we know about. let diff = updatef.relative_diff_to(&efidir)?; log::trace!("applying adoption diff: {}", &diff); filetree::apply_diff(&updated, &efidir, &diff, None) .context("applying filesystem changes")?; // Backup current config and install static config if with_static_config { // Install the static config if the OSTree bootloader is not set. if let Some(bootloader) = crate::ostreeutil::get_ostree_bootloader()? { println!( "ostree repo 'sysroot.bootloader' config option is currently set to: '{bootloader}'", ); } else { println!("ostree repo 'sysroot.bootloader' config option is not set yet"); self.migrate_static_grub_config(rootcxt.path.as_str(), &efidir)?; }; } // Do the sync before unmount fsfreeze_thaw_cycle(efidir.open_file(".")?)?; drop(efidir); self.unmount().context("unmount after adopt")?; } Ok(Some(InstalledContent { meta: updatemeta.clone(), filetree: Some(updatef), adopted_from: Some(meta.version), })) } fn install( &self, src_root: &openat::Dir, dest_root: &str, device: &str, update_firmware: bool, ) -> Result { let Some(meta) = get_component_update(src_root, self)? else { anyhow::bail!("No update metadata for component {} found", self.name()); }; log::debug!("Found metadata {}", meta.version); let srcdir_name = component_updatedirname(self); let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?; // Let's attempt to use an already mounted ESP at the target // dest_root if one is already mounted there in a known ESP location. let destpath = if let Some(destdir) = self.get_mounted_esp(Path::new(dest_root))? { destdir } else { // Using `blockdev` to find the partition instead of partlabel because // we know the target install toplevel device already. if device.is_empty() { anyhow::bail!("Device value not provided"); } let esp_device = blockdev::get_esp_partition(device)? .ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; self.mount_esp_device(Path::new(dest_root), Path::new(&esp_device))? }; let destd = &openat::Dir::open(&destpath) .with_context(|| format!("opening dest dir {}", destpath.display()))?; validate_esp_fstype(destd)?; // TODO - add some sort of API that allows directly setting the working // directory to a file descriptor. std::process::Command::new("cp") .args(["-rp", "--reflink=auto"]) .arg(&srcdir_name) .arg(destpath) .current_dir(format!("/proc/self/fd/{}", src_root.as_raw_fd())) .run()?; if update_firmware { if let Some(vendordir) = self.get_efi_vendor(&src_root)? { self.update_firmware(device, destd, &vendordir)? } } Ok(InstalledContent { meta, filetree: Some(ft), adopted_from: None, }) } fn run_update( &self, rootcxt: &RootContext, current: &InstalledContent, ) -> Result { let currentf = current .filetree .as_ref() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; let sysroot_dir = &rootcxt.sysroot; let updatemeta = self.query_update(sysroot_dir)?.expect("update available"); let updated = sysroot_dir .sub_dir(&component_updatedirname(self)) .context("opening update dir")?; let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?; let diff = currentf.diff(&updatef)?; let Some(esp_devices) = blockdev::find_colocated_esps(&rootcxt.devices)? else { anyhow::bail!("Failed to find all esp devices"); }; for esp in esp_devices { let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?; let destdir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?; validate_esp_fstype(&destdir)?; log::trace!("applying diff: {}", &diff); filetree::apply_diff(&updated, &destdir, &diff, None) .context("applying filesystem changes")?; // Do the sync before unmount fsfreeze_thaw_cycle(destdir.open_file(".")?)?; drop(destdir); self.unmount().context("unmount after update")?; } let adopted_from = None; Ok(InstalledContent { meta: updatemeta, filetree: Some(updatef), adopted_from, }) } fn generate_update_metadata(&self, sysroot: &str) -> Result { let sysroot_path = Utf8Path::new(sysroot); // copy EFI files to updates dir from usr/lib/efi let efilib_path = sysroot_path.join(EFILIB); let meta = if efilib_path.exists() { let mut packages = Vec::new(); let sysroot_dir = Dir::open_ambient_dir(sysroot_path, cap_std::ambient_authority())?; let efi_components = get_efi_component_from_usr(&sysroot_path, EFILIB)?; if efi_components.len() == 0 { bail!("Failed to find EFI components from {efilib_path}"); } for efi in efi_components { Command::new("cp") .args(["-rp", "--reflink=auto"]) .arg(&efi.path) .arg(crate::model::BOOTUPD_UPDATES_DIR) .current_dir(format!("/proc/self/fd/{}", sysroot_dir.as_raw_fd())) .run()?; packages.push(format!("{}-{}", efi.name, efi.version)); } // change to now to workaround https://github.com/coreos/bootupd/issues/933 let timestamp = std::time::SystemTime::now(); ContentMetadata { timestamp: chrono::DateTime::::from(timestamp), version: packages.join(","), } } else { let ostreebootdir = sysroot_path.join(ostreeutil::BOOT_PREFIX); // move EFI files to updates dir from /usr/lib/ostree-boot if ostreebootdir.exists() { let cruft = ["loader", "grub2"]; for p in cruft.iter() { let p = ostreebootdir.join(p); if p.exists() { std::fs::remove_dir_all(&p)?; } } let efisrc = ostreebootdir.join("efi/EFI"); if !efisrc.exists() { bail!("Failed to find {:?}", &efisrc); } let dest_efidir = component_updatedir(sysroot, self); let dest_efidir = Utf8PathBuf::from_path_buf(dest_efidir).expect("Path is invalid UTF-8"); // Fork off mv() because on overlayfs one can't rename() a lower level // directory today, and this will handle the copy fallback. Command::new("mv").args([&efisrc, &dest_efidir]).run()?; let efidir = openat::Dir::open(dest_efidir.as_std_path()) .with_context(|| format!("Opening {}", dest_efidir))?; let files = crate::util::filenames(&efidir)?.into_iter().map(|mut f| { f.insert_str(0, "/boot/efi/EFI/"); f }); packagesystem::query_files(sysroot, files)? } else { anyhow::bail!("Failed to find {ostreebootdir}"); } }; write_update_metadata(sysroot, self, &meta)?; Ok(meta) } fn query_update(&self, sysroot: &openat::Dir) -> Result> { get_component_update(sysroot, self) } fn validate(&self, current: &InstalledContent) -> Result { let devices = crate::blockdev::get_devices("/").context("get parent devices")?; let esp_devices = blockdev::find_colocated_esps(&devices)?; if !is_efi_booted()? && esp_devices.is_none() { return Ok(ValidationResult::Skip); } let currentf = current .filetree .as_ref() .ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?; let mut errs = Vec::new(); let esp_devices = esp_devices.unwrap_or_default(); for esp in esp_devices.iter() { let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp))?; let efidir = openat::Dir::open(&destpath.join("EFI")) .with_context(|| format!("opening EFI dir {}", destpath.display()))?; let diff = currentf.relative_diff_to(&efidir)?; for f in diff.changes.iter() { errs.push(format!("Changed: {}", f)); } for f in diff.removals.iter() { errs.push(format!("Removed: {}", f)); } assert_eq!(diff.additions.len(), 0); drop(efidir); self.unmount().context("unmount after validate")?; } if !errs.is_empty() { Ok(ValidationResult::Errors(errs)) } else { Ok(ValidationResult::Valid) } } fn get_efi_vendor(&self, sysroot: &openat::Dir) -> Result> { let updated = sysroot .sub_dir(&component_updatedirname(self)) .context("opening update dir")?; let shim_files = find_file_recursive(updated.recover_path()?, SHIM)?; // Does not support multiple shim for efi if shim_files.len() > 1 { anyhow::bail!("Found multiple {SHIM} in the image"); } if let Some(p) = shim_files.first() { let p = p .parent() .unwrap() .file_name() .ok_or_else(|| anyhow::anyhow!("No file name found"))?; Ok(Some(p.to_string_lossy().into_owned())) } else { anyhow::bail!("Failed to find {SHIM} in the image") } } } impl Drop for Efi { fn drop(&mut self) { log::debug!("Unmounting"); let _ = self.unmount(); } } fn validate_esp_fstype(dir: &openat::Dir) -> Result<()> { let dir = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) }; let stat = rustix::fs::fstatfs(&dir)?; if stat.f_type != libc::MSDOS_SUPER_MAGIC { bail!( "EFI mount is not a msdos filesystem, but is {:?}", stat.f_type ); }; Ok(()) } #[derive(Debug, PartialEq)] struct BootEntry { id: String, name: String, } /// Parse boot entries from efibootmgr output fn parse_boot_entries(output: &str) -> Vec { let mut entries = Vec::new(); for line in output.lines().filter_map(|line| line.strip_prefix("Boot")) { // Need to consider if output only has "Boot0000* UiApp", without additional info if line.starts_with('0') { let parts = if let Some((parts, _)) = line.split_once('\t') { parts } else { line }; if let Some((id, name)) = parts.split_once(' ') { let id = id.trim_end_matches('*').to_string(); let name = name.trim().to_string(); entries.push(BootEntry { id, name }); } } } entries } #[context("Clearing EFI boot entries that match target {target}")] pub(crate) fn clear_efi_target(target: &str) -> Result<()> { let target = target.to_lowercase(); let output = Command::new(EFIBOOTMGR).output()?; if !output.status.success() { anyhow::bail!("Failed to invoke {EFIBOOTMGR}") } let output = String::from_utf8(output.stdout)?; let boot_entries = parse_boot_entries(&output); for entry in boot_entries { if entry.name.to_lowercase() == target { log::debug!("Deleting matched target {:?}", entry); let mut cmd = Command::new(EFIBOOTMGR); cmd.args(["-b", entry.id.as_str(), "-B"]); println!("Executing: {cmd:?}"); cmd.run_with_cmd_context()?; } } anyhow::Ok(()) } #[context("Adding new EFI boot entry")] pub(crate) fn create_efi_boot_entry( device: &str, espdir: &openat::Dir, vendordir: &str, target: &str, ) -> Result<()> { let fsinfo = crate::filesystem::inspect_filesystem(espdir, ".")?; let source = fsinfo.source; let devname = source .rsplit_once('/') .ok_or_else(|| anyhow::anyhow!("Failed to parse {source}"))? .1; let partition_path = format!("/sys/class/block/{devname}/partition"); let partition_number = std::fs::read_to_string(&partition_path) .with_context(|| format!("Failed to read {partition_path}"))?; let shim = format!("{vendordir}/{SHIM}"); if espdir.exists(&shim)? { anyhow::bail!("Failed to find {SHIM}"); } let loader = format!("\\EFI\\{}\\{SHIM}", vendordir); log::debug!("Creating new EFI boot entry using '{target}'"); let mut cmd = Command::new(EFIBOOTMGR); cmd.args([ "--create", "--disk", device, "--part", partition_number.trim(), "--loader", loader.as_str(), "--label", target, ]); println!("Executing: {cmd:?}"); cmd.run_with_cmd_context() } #[context("Find target file recursively")] fn find_file_recursive>(dir: P, target_file: &str) -> Result> { let mut result = Vec::new(); for entry in WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) { if entry.file_type().is_file() { if let Some(file_name) = entry.file_name().to_str() { if file_name == target_file { if let Some(path) = entry.path().to_str() { result.push(path.into()); } } } } } Ok(result) } #[derive(Debug, PartialEq, Eq)] pub struct EFIComponent { name: String, version: String, path: Utf8PathBuf, } /// Get EFIComponents from e.g. usr/lib/efi, like "usr/lib/efi///EFI" fn get_efi_component_from_usr<'a>( sysroot: &'a Utf8Path, usr_path: &'a str, ) -> Result> { let efilib_path = sysroot.join(usr_path); let skip_count = Utf8Path::new(usr_path).components().count(); let mut components: Vec = WalkDir::new(&efilib_path) .min_depth(3) // //EFI: so 3 levels down .max_depth(3) .into_iter() .filter_map(|entry| { let entry = entry.ok()?; if !entry.file_type().is_dir() || entry.file_name() != "EFI" { return None; } let abs_path = entry.path(); let rel_path = abs_path.strip_prefix(sysroot).ok()?; let utf8_rel_path = Utf8PathBuf::from_path_buf(rel_path.to_path_buf()).ok()?; let mut components = utf8_rel_path.components(); let name = components.nth(skip_count)?.to_string(); let version = components.next()?.to_string(); Some(EFIComponent { name, version, path: utf8_rel_path, }) }) .collect(); components.sort_by(|a, b| a.name.cmp(&b.name)); Ok(components) } #[cfg(test)] mod tests { use cap_std_ext::dirext::CapStdExtDirExt; use super::*; #[test] fn test_parse_boot_entries() -> Result<()> { let output = r" BootCurrent: 0003 Timeout: 0 seconds BootOrder: 0003,0001,0000,0002 Boot0000* UiApp FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(462caa21-7614-4503-836e-8ab6f4662331) Boot0001* UEFI Misc Device PciRoot(0x0)/Pci(0x3,0x0){auto_created_boot_option} Boot0002* EFI Internal Shell FvVol(7cb8bdc9-f8eb-4f34-aaea-3ee4af6516a1)/FvFile(7c04a583-9e3e-4f1c-ad65-e05268d0b4d1) Boot0003* Fedora HD(2,GPT,94ff4025-5276-4bec-adea-e98da271b64c,0x1000,0x3f800)/\EFI\fedora\shimx64.efi"; let entries = parse_boot_entries(output); assert_eq!( entries, [ BootEntry { id: "0000".to_string(), name: "UiApp".to_string() }, BootEntry { id: "0001".to_string(), name: "UEFI Misc Device".to_string() }, BootEntry { id: "0002".to_string(), name: "EFI Internal Shell".to_string() }, BootEntry { id: "0003".to_string(), name: "Fedora".to_string() } ] ); let output = r" BootCurrent: 0003 Timeout: 0 seconds BootOrder: 0003,0001,0000,0002"; let entries = parse_boot_entries(output); assert_eq!(entries, []); let output = r" BootCurrent: 0003 Timeout: 0 seconds BootOrder: 0003,0001,0000,0002 Boot0000* UiApp Boot0001* UEFI Misc Device Boot0002* EFI Internal Shell Boot0003* test"; let entries = parse_boot_entries(output); assert_eq!( entries, [ BootEntry { id: "0000".to_string(), name: "UiApp".to_string() }, BootEntry { id: "0001".to_string(), name: "UEFI Misc Device".to_string() }, BootEntry { id: "0002".to_string(), name: "EFI Internal Shell".to_string() }, BootEntry { id: "0003".to_string(), name: "test".to_string() } ] ); Ok(()) } #[cfg(test)] fn fixture() -> Result { let tempdir = cap_std_ext::cap_tempfile::tempdir(cap_std::ambient_authority())?; tempdir.create_dir("etc")?; Ok(tempdir) } #[test] fn test_get_product_name() -> Result<()> { let tmpd = fixture()?; { tmpd.atomic_write("etc/system-release", "Fedora release 40 (Forty)")?; let name = get_product_name(&tmpd)?; assert_eq!("Fedora", name); } { tmpd.atomic_write("etc/system-release", "CentOS Stream release 9")?; let name = get_product_name(&tmpd)?; assert_eq!("CentOS Stream", name); } { tmpd.atomic_write( "etc/system-release", "Red Hat Enterprise Linux CoreOS release 4", )?; let name = get_product_name(&tmpd)?; assert_eq!("Red Hat Enterprise Linux CoreOS", name); } { tmpd.atomic_write( "etc/system-release", "Red Hat Enterprise Linux CoreOS release 4 ", )?; let name = get_product_name(&tmpd)?; assert_eq!("Red Hat Enterprise Linux CoreOS", name); } { tmpd.remove_file("etc/system-release")?; let name = get_product_name(&tmpd)?; assert!(name.len() > 0); } Ok(()) } #[test] fn test_get_efi_component_from_usr() -> Result<()> { let tmpdir: &tempfile::TempDir = &tempfile::tempdir()?; let tpath = tmpdir.path(); let efi_path = tpath.join("usr/lib/efi"); std::fs::create_dir_all(efi_path.join("BAR/1.1/EFI"))?; std::fs::create_dir_all(efi_path.join("FOO/1.1/EFI"))?; std::fs::create_dir_all(efi_path.join("FOOBAR/1.1/test"))?; let utf8_tpath = Utf8Path::from_path(tpath).ok_or_else(|| anyhow::anyhow!("Path is not valid UTF-8"))?; let efi_comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?; assert_eq!( efi_comps, vec![ EFIComponent { name: "BAR".to_string(), version: "1.1".to_string(), path: Utf8PathBuf::from("usr/lib/efi/BAR/1.1/EFI"), }, EFIComponent { name: "FOO".to_string(), version: "1.1".to_string(), path: Utf8PathBuf::from("usr/lib/efi/FOO/1.1/EFI"), }, ] ); std::fs::remove_dir_all(efi_path.join("BAR/1.1/EFI"))?; std::fs::remove_dir_all(efi_path.join("FOO/1.1/EFI"))?; let efi_comps = get_efi_component_from_usr(utf8_tpath, EFILIB)?; assert_eq!(efi_comps, []); Ok(()) } }