diff --git a/crates/boot/src/actions/chainload.rs b/crates/boot/src/actions/chainload.rs index 2998ea5..8b4d14b 100644 --- a/crates/boot/src/actions/chainload.rs +++ b/crates/boot/src/actions/chainload.rs @@ -2,9 +2,10 @@ use crate::context::SproutContext; use crate::phases::before_handoff; use alloc::boxed::Box; use alloc::rc::Rc; +use alloc::vec::Vec; use anyhow::{Context, Result, bail}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; -use edera_sprout_parsing::{combine_options, empty_is_none}; +use edera_sprout_parsing::combine_options; use eficore::bootloader_interface::BootloaderInterface; use eficore::loader::source::ImageSource; use eficore::loader::{ImageLoadRequest, ImageLoader}; @@ -62,25 +63,37 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat .set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32); } - // Stamp the initrd path, if provided. - let initrd = configuration - .linux_initrd - .as_ref() - .map(|item| context.stamp(item)); - // The initrd can be None or empty, so we need to collapse that into a single Option. - let initrd = empty_is_none(initrd); - // If an initrd is provided, register it with the EFI stack. let mut initrd_handle = None; - if let Some(linux_initrd) = initrd { - let content = eficore::path::read_file_contents( - Some(context.root().loaded_image_path()?), - &linux_initrd, - ) - .context("unable to read linux initrd")?; - let handle = - MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) - .context("unable to register linux initrd")?; + + // Only proceed if the configuration actually contains initrd paths. + if !configuration.initrd.is_empty() { + // Initialize a master buffer to hold the combined bytes of all initrd files. + let mut content = Vec::new(); + + // Iterate through each path provided in the configuration. + for item in &configuration.initrd { + // "Stamp" the path to replace variables (like disk labels) with real values. + let stamped_path = context.stamp(item); + + // Read the file contents from the EFI partition. + let mut segment = eficore::path::read_file_contents( + Some(context.root().loaded_image_path()?), + &stamped_path, + ).context("unable to read linux initrd segment")?; + + // Append this segment's bytes to the end of our master buffer. + // The kernel will see these as a continuous stream of CPIO archives. + content.append(&mut segment); + } + + // Register the full, concatenated buffer with the EFI stack using the Linux-specific GUID. + // This allows the Linux EFI stub to discover the initrd in memory. + let handle = MediaLoaderHandle::register( + LINUX_EFI_INITRD_MEDIA_GUID, + content.into_boxed_slice() + ).context("unable to register initrd")?; + initrd_handle = Some(handle); } diff --git a/crates/boot/src/actions/edera.rs b/crates/boot/src/actions/edera.rs index 363b8a4..9b1f8f6 100644 --- a/crates/boot/src/actions/edera.rs +++ b/crates/boot/src/actions/edera.rs @@ -5,7 +5,7 @@ use alloc::{format, vec}; use anyhow::{Context, Result}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::edera::EderaConfiguration; -use edera_sprout_parsing::{build_xen_config, combine_options, empty_is_none}; +use edera_sprout_parsing::{build_xen_config, combine_options}; use eficore::media_loader::{ MediaLoaderHandle, constants::xen::{ @@ -74,12 +74,16 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> // Create a vector of media loaders to drop them only after this function completes. let mut media_loaders = vec![config, kernel]; - // Register the initrd if it is provided. - if let Some(initrd) = empty_is_none(configuration.initrd.as_ref()) { - let initrd = - register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) - .context("unable to register initrd media loader")?; - media_loaders.push(initrd); + // Register each initrd segment, filtering out any empty strings or placeholders. + for initrd_path in configuration.initrd.iter().filter(|s| !s.is_empty()) { + let handle = register_media_loader_file( + &context, + XEN_EFI_RAMDISK_MEDIA_GUID, + "initrd", + initrd_path + ).context("unable to register initrd segment media loader")?; + + media_loaders.push(handle); } // Chainload to the Xen EFI stub. @@ -88,7 +92,7 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> &ChainloadConfiguration { path: configuration.xen.clone(), options: vec![], - linux_initrd: None, + initrd: vec![], }, ) .context("unable to chainload to xen"); diff --git a/crates/boot/src/autoconfigure/bls.rs b/crates/boot/src/autoconfigure/bls.rs index d4ab931..98f63f4 100644 --- a/crates/boot/src/autoconfigure/bls.rs +++ b/crates/boot/src/autoconfigure/bls.rs @@ -86,7 +86,7 @@ pub fn scan( let chainload = ChainloadConfiguration { path: format!("{}\\$chainload", root), options: vec!["$options".to_string()], - linux_initrd: Some(format!("{}\\$initrd", root)), + initrd: alloc::vec![format!("{}\\$initrd", root)], }; // Insert the chainload action into the configuration. diff --git a/crates/boot/src/autoconfigure/linux.rs b/crates/boot/src/autoconfigure/linux.rs index d7ec70c..0eab818 100644 --- a/crates/boot/src/autoconfigure/linux.rs +++ b/crates/boot/src/autoconfigure/linux.rs @@ -231,7 +231,7 @@ pub fn scan( let chainload = ChainloadConfiguration { path: "$kernel".to_string(), options: vec!["$linux-options".to_string()], - linux_initrd: Some("$initrd".to_string()), + initrd: alloc::vec!["$initrd".to_string()], }; // Insert the chainload action into the configuration. diff --git a/crates/config/src/actions/chainload.rs b/crates/config/src/actions/chainload.rs index 55c7f00..e6c0c17 100644 --- a/crates/config/src/actions/chainload.rs +++ b/crates/config/src/actions/chainload.rs @@ -1,6 +1,32 @@ use alloc::string::String; use alloc::vec::Vec; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize}; + +// Naming the return type vastly improves readability +type DeResult<'de, D, T> = core::result::Result>::Error>; + +// Type of vector for a list of strings +type StringList = Vec; + +// Helper function to allow "linux-initrd" to be either a single string +// or a list of strings in the TOML file. +fn string_or_vec<'de, D>(deserializer: D) -> DeResult<'de, D, StringList> +where + D: Deserializer<'de>, { + + #[derive(Deserialize)] + #[serde(untagged)] + enum StringOrVec { + String(String), + Vec(Vec), + } + + match StringOrVec::deserialize(deserializer)? { + StringOrVec::String(s) => Ok(alloc::vec![s]), + // Wrap the vector in Ok() to match the return type + StringOrVec::Vec(v) => Ok(v), + } + } /// The configuration of the chainload action. #[derive(Serialize, Deserialize, Debug, Default, Clone)] @@ -8,14 +34,14 @@ pub struct ChainloadConfiguration { /// The path to the image to chainload. /// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable. pub path: String, + /// The options to pass to the image. /// The options are concatenated by a space and then passed to the EFI application. #[serde(default)] pub options: Vec, - /// An optional path to a Linux initrd. - /// This uses the `LINUX_EFI_INITRD_MEDIA_GUID` mechanism to load the initrd into the EFI stack. - /// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is - /// generally better and safer as it can support additional load options in the future. - #[serde(default, rename = "linux-initrd")] - pub linux_initrd: Option, + + /// The path(s) to the initrd to use for the image. + /// Supports both a single string or an array of strings. + #[serde(default, rename = "linux-initrd", deserialize_with = "string_or_vec")] + pub initrd: Vec, } diff --git a/crates/config/src/actions/edera.rs b/crates/config/src/actions/edera.rs index 6577610..879fa12 100644 --- a/crates/config/src/actions/edera.rs +++ b/crates/config/src/actions/edera.rs @@ -2,9 +2,11 @@ use alloc::string::String; use alloc::vec::Vec; use serde::{Deserialize, Serialize}; -/// The configuration of the edera action which boots the Edera hypervisor. -/// Edera is based on Xen but modified significantly with a Rust stack. -/// Sprout is a component of the Edera stack and provides the boot functionality of Xen. +type StringList = Vec; + +// The configuration of the edera action which boots the Edera hypervisor. +// Edera is based on Xen but modified significantly with a Rust stack. +// Sprout is a component of the Edera stack and provides the boot functionality of Xen. #[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct EderaConfiguration { /// The path to the Xen hypervisor EFI image. @@ -13,7 +15,7 @@ pub struct EderaConfiguration { pub kernel: String, /// The path to the initrd to load for dom0. #[serde(default)] - pub initrd: Option, + pub initrd: StringList, /// The options to pass to the kernel. #[serde(default, rename = "kernel-options")] pub kernel_options: Vec, diff --git a/crates/parsing/src/lib.rs b/crates/parsing/src/lib.rs index ba0bb38..9606b85 100644 --- a/crates/parsing/src/lib.rs +++ b/crates/parsing/src/lib.rs @@ -84,8 +84,8 @@ pub fn build_matrix(input: &BTreeMap>) -> Vec>(options: impl Iterator) -> String { options - .flat_map(|item| empty_is_none(Some(item))) .map(|item| item.as_ref().to_string()) + .filter(|item| !item.is_empty()) .collect::>() .join(" ") } @@ -96,11 +96,6 @@ pub fn unique_hash(input: &str) -> String { hex::encode(Sha256::digest(input.as_bytes())) } -/// Filter a string-like Option `input` such that an empty string is [None]. -pub fn empty_is_none>(input: Option) -> Option { - input.filter(|input| !input.as_ref().is_empty()) -} - /// Build a Xen EFI stub configuration file from pre-stamped `xen_options` and `kernel_options`. /// The returned string is in the Xen ini-like config file format. pub fn build_xen_config(xen_options: &str, kernel_options: &str) -> String { @@ -287,21 +282,6 @@ mod tests { assert_eq!(result, ""); } - #[test] - fn empty_is_none_returns_none_for_empty_string() { - assert!(empty_is_none(Some("")).is_none()); - } - - #[test] - fn empty_is_none_returns_some_for_nonempty_string() { - assert_eq!(empty_is_none(Some("x")), Some("x")); - } - - #[test] - fn empty_is_none_passthrough_on_none() { - assert!(empty_is_none(None::<&str>).is_none()); - } - #[test] fn unique_hash_is_deterministic() { assert_eq!(unique_hash("hello"), unique_hash("hello"));