diff --git a/hack/dev/configs/shell.sprout.toml b/hack/dev/configs/shell.sprout.toml new file mode 100644 index 0000000..99676ff --- /dev/null +++ b/hack/dev/configs/shell.sprout.toml @@ -0,0 +1,11 @@ +version = 1 + +[extractors.boot.filesystem-device-match] +has-item = "\\EFI\\BOOT\\shell.efi" + +[actions.chainload-shell] +chainload.path = "$boot\\EFI\\BOOT\\shell.efi" + +[entries.xen] +title = "Boot Shell" +actions = ["chainload-shell"] diff --git a/src/config/loader.rs b/src/config/loader.rs index 5b4a85c..d02ed18 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -1,31 +1,36 @@ use crate::config::{RootConfiguration, latest_version}; +use crate::options::SproutOptions; use crate::utils; use anyhow::{Context, Result, bail}; +use log::info; use std::ops::Deref; use toml::Value; use uefi::proto::device_path::LoadedImageDevicePath; -/// Loads the raw configuration from the sprout.toml file as data. -fn load_raw_config() -> Result> { +/// Loads the raw configuration from the sprout config file as data. +fn load_raw_config(options: &SproutOptions) -> Result> { // Open the LoadedImageDevicePath protocol to get the path to the current image. let current_image_device_path_protocol = uefi::boot::open_protocol_exclusive::(uefi::boot::image_handle()) .context("unable to get loaded image device path")?; // Acquire the device path as a boxed device path. let path = current_image_device_path_protocol.deref().to_boxed(); - // Read the contents of the sprout.toml file. - let content = utils::read_file_contents(&path, "sprout.toml") - .context("unable to read sprout.toml file")?; - // Return the contents of the sprout.toml file. + + info!("configuration file: {}", options.config); + + // Read the contents of the sprout config file. + let content = utils::read_file_contents(&path, &options.config) + .context("unable to read sprout config file")?; + // Return the contents of the sprout config file. Ok(content) } /// Loads the [RootConfiguration] for Sprout. -pub fn load() -> Result { - // Load the raw configuration from the sprout.toml file. - let content = load_raw_config()?; +pub fn load(options: &SproutOptions) -> Result { + // Load the raw configuration from the sprout config file. + let content = load_raw_config(options)?; // Parse the raw configuration into a toml::Value which can represent any TOML file. - let value: Value = toml::from_slice(&content).context("unable to parse sprout.toml file")?; + let value: Value = toml::from_slice(&content).context("unable to parse sprout config file")?; // Check the version of the configuration without parsing the full configuration. let version = value diff --git a/src/context.rs b/src/context.rs index cdd73c2..8755e83 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,4 +1,5 @@ use crate::actions::ActionDeclaration; +use crate::options::SproutOptions; use anyhow::Result; use anyhow::anyhow; use std::collections::{BTreeMap, BTreeSet}; @@ -13,15 +14,18 @@ pub struct RootContext { actions: BTreeMap, /// The device path of the loaded Sprout image. loaded_image_path: Option>, + /// The global options of Sprout. + options: SproutOptions, } impl RootContext { /// Creates a new root context with the `loaded_image_device_path` which will be stored /// in the context for easy access. - pub fn new(loaded_image_device_path: Box) -> Self { + pub fn new(loaded_image_device_path: Box, options: SproutOptions) -> Self { Self { actions: BTreeMap::new(), loaded_image_path: Some(loaded_image_device_path), + options, } } @@ -41,6 +45,11 @@ impl RootContext { .as_deref() .ok_or_else(|| anyhow!("no loaded image path")) } + + /// Access the global Sprout options. + pub fn options(&self) -> &SproutOptions { + &self.options + } } /// A context of Sprout. This is passed around different parts of Sprout and represents diff --git a/src/main.rs b/src/main.rs index 37b5f17..48b500c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use crate::context::{RootContext, SproutContext}; use crate::phases::phase; -use anyhow::{Context, Result, bail}; +use anyhow::{Context, Result}; use log::info; use std::collections::BTreeMap; use std::ops::Deref; @@ -37,6 +37,9 @@ pub mod phases; /// setup: Code that initializes the UEFI environment for Sprout. pub mod setup; +/// options: Parse the options of the Sprout executable. +pub mod options; + /// utils: Utility functions that are used by other parts of Sprout. pub mod utils; @@ -47,10 +50,13 @@ fn main() -> Result<()> { // Initialize the basic UEFI environment. setup::init()?; + // Parse the options to the sprout executable. + let options = options::parse().context("unable to parse options")?; + // Load the configuration of sprout. // At this point, the configuration has been validated and the specified // version is checked to ensure compatibility. - let config = config::loader::load()?; + let config = config::loader::load(&options)?; // Load the root context. // This is done in a block to ensure the release of the LoadedImageDevicePath protocol. @@ -64,7 +70,7 @@ fn main() -> Result<()> { "loaded image path: {}", loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))? ); - RootContext::new(loaded_image_path) + RootContext::new(loaded_image_path, options) }; // Insert the configuration actions into the root context. @@ -144,9 +150,14 @@ fn main() -> Result<()> { // Execute the late phase. phase(context.clone(), &config.phases.late).context("unable to execute late phase")?; - // Pick the first entry from the list of final entries until a boot menu is implemented. - let Some((context, entry)) = final_entries.first() else { - bail!("no entries found"); + // Use the boot option if possible, otherwise pick the first entry. + let (context, entry) = if let Some(ref boot) = context.root().options().boot { + final_entries + .iter() + .find(|(_context, entry)| &entry.title == boot) + .context(format!("unable to find entry: {boot}"))? + } else { + final_entries.first().context("no entries found")? }; // Execute all the actions for the selected entry. diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 0000000..a9ded1a --- /dev/null +++ b/src/options.rs @@ -0,0 +1,117 @@ +use anyhow::{Context, Result, bail}; +use std::collections::BTreeMap; + +/// Default configuration file path. +const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; + +/// The parsed options of sprout. +#[derive(Debug)] +pub struct SproutOptions { + /// Path to a configuration file to load. + pub config: String, + /// Entry to boot without showing the boot menu. + pub boot: Option, +} + +/// The default Sprout options. +impl Default for SproutOptions { + fn default() -> Self { + Self { + config: DEFAULT_CONFIG_PATH.to_string(), + boot: None, + } + } +} + +/// For minimalism, we don't want a full argument parser. Instead, we use +/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format. +/// We also support --abc=123 = abc: Some("123") format. +fn parse_raw() -> Result>> { + // Collect all the arguments to Sprout. + // Skip the first argument which is the path to our executable. + let args = std::env::args().skip(1).collect::>(); + + // Represent options as key-value pairs. + let mut options = BTreeMap::new(); + + // Iterators makes this way easier. + let mut iterator = args.into_iter().peekable(); + + loop { + // Consume the next option, if any. + let Some(option) = iterator.next() else { + break; + }; + + // If the doesn't start with --, that is invalid. + if !option.starts_with("--") { + bail!("invalid option: {option}"); + } + + // Strip the -- prefix off. + let mut option = option["--".len()..].trim().to_string(); + + // An optional value. + let mut value = None; + + // Check if the option is of the form --abc=123 + if option.contains("=") { + let Some((part_key, part_value)) = option.split_once("=") else { + bail!("invalid option: {option}"); + }; + + let part_key = part_key.to_string(); + let part_value = part_value.to_string(); + option = part_key; + value = Some(part_value); + } + + if value.is_none() { + // Check for the next value. + let maybe_next = iterator.peek(); + + // If the next value isn't another option, set the value to the next value. + // Otherwise, it is an empty string. + value = if let Some(next) = maybe_next + && !next.starts_with("--") + { + iterator.next() + } else { + None + }; + } + + // Error on empty option names. + if option.is_empty() { + bail!("invalid empty option: {option}"); + } + + // Insert the option and the value into the map. + options.insert(option, value); + } + Ok(options) +} + +/// Parse the arguments to Sprout as a [SproutOptions] structure. +pub fn parse() -> Result { + // Use the default value of sprout options and have the raw options be parsed into it. + let mut result = SproutOptions::default(); + let options = parse_raw().context("unable to parse options")?; + + for (key, value) in options { + match key.as_str() { + "config" => { + // The configuration file to load. + result.config = value.context("--config option requires a value")?; + } + + "boot" => { + // The entry to boot. + result.boot = Some(value.context("--boot option requires a value")?); + } + + _ => bail!("unknown option: --{key}"), + } + } + Ok(result) +}