2025-11-03 02:04:21 -05:00
|
|
|
use alloc::collections::BTreeMap;
|
|
|
|
|
use alloc::string::{String, ToString};
|
|
|
|
|
use alloc::vec::Vec;
|
|
|
|
|
use alloc::{format, vec};
|
2025-10-27 15:41:29 -04:00
|
|
|
use anyhow::{Context, Result};
|
2025-11-02 23:28:31 -05:00
|
|
|
use edera_sprout_config::RootConfiguration;
|
|
|
|
|
use edera_sprout_config::actions::ActionDeclaration;
|
|
|
|
|
use edera_sprout_config::actions::chainload::ChainloadConfiguration;
|
|
|
|
|
use edera_sprout_config::entries::EntryDeclaration;
|
|
|
|
|
use edera_sprout_config::generators::GeneratorDeclaration;
|
|
|
|
|
use edera_sprout_config::generators::list::ListConfiguration;
|
2026-03-25 15:56:02 -04:00
|
|
|
use edera_sprout_parsing::{
|
|
|
|
|
LINUX_INITRAMFS_PREFIXES, LINUX_KERNEL_PREFIXES, initramfs_candidates, match_kernel_prefix,
|
|
|
|
|
unique_hash,
|
|
|
|
|
};
|
2026-09-26 20:17:50 -07:00
|
|
|
use log::warn;
|
2025-11-01 02:01:09 -04:00
|
|
|
use uefi::CString16;
|
2025-11-01 02:04:52 -04:00
|
|
|
use uefi::fs::{FileSystem, Path, PathBuf};
|
|
|
|
|
use uefi::proto::device_path::DevicePath;
|
|
|
|
|
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
|
2025-10-27 15:41:29 -04:00
|
|
|
|
|
|
|
|
/// The name prefix of the Linux chainload action that will be used to boot Linux.
|
|
|
|
|
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
|
|
|
|
|
|
|
|
|
|
/// The locations to scan for kernel pairs.
|
2026-09-26 20:23:08 -07:00
|
|
|
/// A location that doesn't exist or can't be read is skipped.
|
|
|
|
|
/// The "\\" location represents the root of the filesystem.
|
2025-11-01 01:58:55 -04:00
|
|
|
const SCAN_LOCATIONS: &[&str] = &["\\boot", "\\"];
|
2025-10-27 15:41:29 -04:00
|
|
|
|
2025-11-02 17:58:06 -05:00
|
|
|
/// This is really silly, but if what we are booting is the Canonical stubble stub,
|
|
|
|
|
/// there is a chance it will assert that the load options are non-empty.
|
|
|
|
|
/// Technically speaking, load options can be empty. However, it assumes load options
|
|
|
|
|
/// have something in it. Canonical's stubble copied code from systemd that does this
|
|
|
|
|
/// and then uses that code improperly by asserting that the pointer is non-null.
|
|
|
|
|
/// To give a good user experience, we place a placeholder value here to ensure it's non-empty.
|
|
|
|
|
/// For stubble, this code ensures the command line pointer becomes null:
|
2025-12-21 14:11:10 -08:00
|
|
|
/// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L61-L64>
|
2025-11-02 17:58:06 -05:00
|
|
|
/// Then this code asserts on it, stopping the boot process:
|
2025-12-21 14:11:10 -08:00
|
|
|
/// <https://github.com/ubuntu/stubble/blob/e56643979addfb98982266018e08921c07424a0c/stub.c#L27>
|
2025-11-02 17:58:06 -05:00
|
|
|
const DEFAULT_LINUX_OPTIONS: &str = "placeholder";
|
|
|
|
|
|
2025-10-27 15:41:29 -04:00
|
|
|
/// Pair of kernel and initramfs.
|
|
|
|
|
/// This is what scanning a directory is meant to find.
|
|
|
|
|
struct KernelPair {
|
|
|
|
|
/// The path to a kernel.
|
|
|
|
|
kernel: String,
|
|
|
|
|
/// The path to an initramfs, if any.
|
|
|
|
|
initramfs: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
|
|
|
|
|
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
|
|
|
|
|
// All the discovered kernel pairs.
|
|
|
|
|
let mut pairs = Vec::new();
|
|
|
|
|
|
2025-11-01 01:58:55 -04:00
|
|
|
// We have to special-case the root directory due to path logic in the uefi crate.
|
|
|
|
|
let is_root = path.is_empty() || path == "\\";
|
|
|
|
|
|
2025-10-27 15:41:29 -04:00
|
|
|
// Construct a filesystem path from the path string.
|
|
|
|
|
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
|
|
|
|
|
let path = Path::new(&path);
|
|
|
|
|
let path = path.to_path_buf();
|
|
|
|
|
|
|
|
|
|
// Check if the path exists and is a directory.
|
|
|
|
|
let exists = filesystem
|
|
|
|
|
.metadata(&path)
|
|
|
|
|
.ok()
|
|
|
|
|
.map(|metadata| metadata.is_directory())
|
|
|
|
|
.unwrap_or(false);
|
|
|
|
|
|
|
|
|
|
// If the path does not exist, return an empty list.
|
|
|
|
|
if !exists {
|
|
|
|
|
return Ok(pairs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open a directory iterator on the path to scan.
|
|
|
|
|
// Ignore errors here as in some scenarios this might fail due to symlinks.
|
|
|
|
|
let Some(directory) = filesystem.read_dir(&path).ok() else {
|
|
|
|
|
return Ok(pairs);
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-01 01:58:55 -04:00
|
|
|
// Create a new path used for joining file names below.
|
|
|
|
|
// All attempts to derive paths for the files in the directory should use this instead.
|
|
|
|
|
// The uefi crate does not handle push correctly for the root directory.
|
|
|
|
|
// It will add a second slash, which will cause our path logic to fail.
|
|
|
|
|
let path_for_join = if is_root {
|
|
|
|
|
PathBuf::new()
|
|
|
|
|
} else {
|
|
|
|
|
path.clone()
|
|
|
|
|
};
|
|
|
|
|
|
2025-10-27 15:41:29 -04:00
|
|
|
// For each item in the directory, find a kernel.
|
|
|
|
|
for item in directory {
|
|
|
|
|
let item = item.context("unable to read directory item")?;
|
|
|
|
|
|
|
|
|
|
// Skip over any items that are not regular files.
|
|
|
|
|
if !item.is_regular_file() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert the name from a CString16 to a String.
|
|
|
|
|
let name = item.file_name().to_string();
|
|
|
|
|
|
2025-11-01 01:58:55 -04:00
|
|
|
// Convert the name to lowercase to make all of this case-insensitive.
|
|
|
|
|
let name_for_match = name.to_lowercase();
|
|
|
|
|
|
2025-10-27 15:41:29 -04:00
|
|
|
// Find a kernel prefix that matches, if any.
|
2025-11-01 01:58:55 -04:00
|
|
|
// This is case-insensitive to ensure we pick up all possibilities.
|
2026-03-25 15:56:02 -04:00
|
|
|
let Some(prefix) = match_kernel_prefix(&name_for_match, LINUX_KERNEL_PREFIXES) else {
|
2025-10-27 15:41:29 -04:00
|
|
|
// Skip over anything that doesn't match a kernel prefix.
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Acquire the suffix of the name, this will be used to match an initramfs.
|
|
|
|
|
let suffix = &name[prefix.len()..];
|
|
|
|
|
|
2026-03-25 15:56:02 -04:00
|
|
|
// Find a matching initramfs by trying each candidate name, if any.
|
|
|
|
|
let mut candidates = initramfs_candidates(suffix, LINUX_INITRAMFS_PREFIXES);
|
2025-10-27 23:00:36 -04:00
|
|
|
let matched_initramfs_path = loop {
|
2026-03-25 15:56:02 -04:00
|
|
|
let Some(candidate) = candidates.next() else {
|
2025-10-27 15:41:29 -04:00
|
|
|
break None;
|
|
|
|
|
};
|
|
|
|
|
// Construct an initramfs path.
|
2026-03-25 15:56:02 -04:00
|
|
|
let initramfs = CString16::try_from(candidate.as_str())
|
2025-10-27 15:41:29 -04:00
|
|
|
.context("unable to convert initramfs name to CString16")?;
|
2025-11-01 01:58:55 -04:00
|
|
|
let mut initramfs_path = path_for_join.clone();
|
2025-10-27 15:41:29 -04:00
|
|
|
initramfs_path.push(Path::new(&initramfs));
|
2025-11-01 01:58:55 -04:00
|
|
|
|
2026-09-26 20:17:07 -07:00
|
|
|
// Check if the initramfs path is a file, if it is, break out of the loop.
|
|
|
|
|
// A path that can't be checked is treated as not existing.
|
2025-10-27 15:41:29 -04:00
|
|
|
if filesystem
|
2026-09-26 20:17:07 -07:00
|
|
|
.metadata(&initramfs_path)
|
|
|
|
|
.is_ok_and(|metadata| metadata.is_regular_file())
|
2025-10-27 15:41:29 -04:00
|
|
|
{
|
|
|
|
|
break Some(initramfs_path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Construct a kernel path from the kernel name.
|
2025-11-01 01:58:55 -04:00
|
|
|
let mut kernel = path_for_join.clone();
|
2025-10-27 15:41:29 -04:00
|
|
|
kernel.push(Path::new(&item.file_name()));
|
|
|
|
|
let kernel = kernel.to_string();
|
2025-10-27 23:00:36 -04:00
|
|
|
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
|
2025-10-27 15:41:29 -04:00
|
|
|
|
|
|
|
|
// Produce a kernel pair.
|
|
|
|
|
let pair = KernelPair { kernel, initramfs };
|
|
|
|
|
pairs.push(pair);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(pairs)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
|
|
|
|
|
pub fn scan(
|
|
|
|
|
filesystem: &mut FileSystem,
|
|
|
|
|
root: &DevicePath,
|
|
|
|
|
config: &mut RootConfiguration,
|
|
|
|
|
) -> Result<bool> {
|
|
|
|
|
let mut pairs = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Convert the device path root to a string we can use in the configuration.
|
|
|
|
|
let mut root = root
|
2026-04-16 14:43:36 -07:00
|
|
|
.to_string16(DisplayOnly(false), AllowShortcuts(false))
|
2025-10-27 15:41:29 -04:00
|
|
|
.context("unable to convert device root to string")?
|
|
|
|
|
.to_string();
|
2025-10-27 23:15:14 -04:00
|
|
|
// Add a trailing forward-slash to the root to ensure the device root is completed.
|
2025-10-27 15:41:29 -04:00
|
|
|
root.push('/');
|
|
|
|
|
|
2025-10-27 18:21:28 -04:00
|
|
|
// Generate a unique hash of the root path.
|
2026-03-25 15:56:02 -04:00
|
|
|
let root_unique_hash = unique_hash(&root);
|
2025-10-27 18:21:28 -04:00
|
|
|
|
2025-10-27 15:41:29 -04:00
|
|
|
// Scan all locations for kernel pairs, adding them to the list.
|
2026-09-26 20:17:50 -07:00
|
|
|
// A location that can't be scanned is skipped, so the other locations are still used.
|
2025-10-27 15:41:29 -04:00
|
|
|
for location in SCAN_LOCATIONS {
|
2026-09-26 20:17:50 -07:00
|
|
|
match scan_directory(filesystem, location) {
|
|
|
|
|
Ok(scanned) => pairs.extend(scanned),
|
|
|
|
|
Err(error) => warn!("unable to scan directory {}: {:#}", location, error),
|
|
|
|
|
}
|
2025-10-27 15:41:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no kernel pairs were found, return false.
|
|
|
|
|
if pairs.is_empty() {
|
|
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate a unique name for the linux chainload action.
|
2025-12-21 14:11:26 -08:00
|
|
|
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash);
|
2025-10-27 15:41:29 -04:00
|
|
|
|
|
|
|
|
// Kernel pairs are detected, generate a list configuration for it.
|
|
|
|
|
let generator = ListConfiguration {
|
|
|
|
|
entry: EntryDeclaration {
|
2025-10-27 19:47:21 -04:00
|
|
|
title: "Boot Linux $name".to_string(),
|
2025-10-27 15:41:29 -04:00
|
|
|
actions: vec![chainload_action_name.clone()],
|
2025-12-21 14:11:26 -08:00
|
|
|
sort_key: Some("$kernel".to_string()),
|
2025-10-27 15:41:29 -04:00
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
values: pairs
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|pair| {
|
|
|
|
|
BTreeMap::from_iter(vec![
|
2025-10-27 19:47:21 -04:00
|
|
|
("name".to_string(), pair.kernel.clone()),
|
|
|
|
|
("kernel".to_string(), format!("{}{}", root, pair.kernel)),
|
|
|
|
|
(
|
|
|
|
|
"initrd".to_string(),
|
|
|
|
|
pair.initramfs
|
|
|
|
|
.map(|initramfs| format!("{}{}", root, initramfs))
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
),
|
2025-10-27 15:41:29 -04:00
|
|
|
])
|
|
|
|
|
})
|
|
|
|
|
.collect(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Generate a unique name for the Linux generator and insert the generator into the configuration.
|
|
|
|
|
config.generators.insert(
|
2025-10-30 15:31:26 -04:00
|
|
|
format!("auto-linux-{}", root_unique_hash),
|
2025-10-27 15:41:29 -04:00
|
|
|
GeneratorDeclaration {
|
|
|
|
|
list: Some(generator),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Insert a default value for the linux-options if it doesn't exist.
|
|
|
|
|
if !config.values.contains_key("linux-options") {
|
2025-11-02 17:58:06 -05:00
|
|
|
config.values.insert(
|
|
|
|
|
"linux-options".to_string(),
|
|
|
|
|
DEFAULT_LINUX_OPTIONS.to_string(),
|
|
|
|
|
);
|
2025-10-27 15:41:29 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Generate a chainload configuration for the list generator.
|
|
|
|
|
// The list will provide these values to us.
|
|
|
|
|
// Note that we don't need an extra \\ in the paths here.
|
|
|
|
|
// The root already contains a trailing slash.
|
|
|
|
|
let chainload = ChainloadConfiguration {
|
2025-10-27 19:47:21 -04:00
|
|
|
path: "$kernel".to_string(),
|
2025-10-27 15:41:29 -04:00
|
|
|
options: vec!["$linux-options".to_string()],
|
2025-10-27 19:47:21 -04:00
|
|
|
linux_initrd: Some("$initrd".to_string()),
|
2026-09-26 19:45:47 -07:00
|
|
|
linux_initrd_chain: vec![],
|
2026-10-03 19:52:21 -07:00
|
|
|
devicetree: None,
|
2025-10-27 15:41:29 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Insert the chainload action into the configuration.
|
|
|
|
|
config.actions.insert(
|
|
|
|
|
chainload_action_name,
|
|
|
|
|
ActionDeclaration {
|
|
|
|
|
chainload: Some(chainload),
|
|
|
|
|
..Default::default()
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// We had a Linux kernel, so return true to indicate something was found.
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|