From 8711c540749af4b4a7eab4cb076254cb116ac0c8 Mon Sep 17 00:00:00 2001 From: Alex Zenla Date: Fri, 14 Nov 2025 20:45:44 -0800 Subject: [PATCH] feat(boot): utilize jaarg for options parsing --- Cargo.lock | 7 ++ Cargo.toml | 5 + crates/boot/Cargo.toml | 1 + crates/boot/src/main.rs | 1 - crates/boot/src/options.rs | 122 ++++++++++++------------ crates/boot/src/options/parser.rs | 153 ------------------------------ 6 files changed, 73 insertions(+), 216 deletions(-) delete mode 100644 crates/boot/src/options/parser.rs diff --git a/Cargo.lock b/Cargo.lock index 8f2e2ab..bea654c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "edera-sprout-config", "edera-sprout-eficore", "hex", + "jaarg", "log", "sha2", "toml", @@ -120,6 +121,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "jaarg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b216e5405f7e759ee0d16007f9d5c3346f9803a2e86cf01fc8df8baac43d0fa" + [[package]] name = "libc" version = "0.2.177" diff --git a/Cargo.toml b/Cargo.toml index 4093232..a410e4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,11 @@ version = "0.4.3" default-features = false features = ["alloc"] +[workspace.dependencies.jaarg] +version = "0.2.1" +default-features = false +features = ["alloc"] + [workspace.dependencies.serde] version = "1.0.228" default-features = false diff --git a/crates/boot/Cargo.toml b/crates/boot/Cargo.toml index 2f0c739..f041ecc 100644 --- a/crates/boot/Cargo.toml +++ b/crates/boot/Cargo.toml @@ -12,6 +12,7 @@ anyhow.workspace = true edera-sprout-config.path = "../config" edera-sprout-eficore.path = "../eficore" hex.workspace = true +jaarg.workspace = true sha2.workspace = true toml.workspace = true log.workspace = true diff --git a/crates/boot/src/main.rs b/crates/boot/src/main.rs index af1365b..45ef91b 100644 --- a/crates/boot/src/main.rs +++ b/crates/boot/src/main.rs @@ -6,7 +6,6 @@ extern crate alloc; use crate::context::{RootContext, SproutContext}; use crate::entries::BootableEntry; use crate::options::SproutOptions; -use crate::options::parser::OptionsRepresentable; use crate::phases::phase; use alloc::collections::BTreeMap; use alloc::format; diff --git a/crates/boot/src/options.rs b/crates/boot/src/options.rs index 2331ac7..b6426fe 100644 --- a/crates/boot/src/options.rs +++ b/crates/boot/src/options.rs @@ -1,10 +1,13 @@ -use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable}; -use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use anyhow::{Context, Result, bail}; - -/// The Sprout options parser. -pub mod parser; +use core::ptr::null_mut; +use jaarg::alloc::ParseMapResult; +use jaarg::{ + ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts, + StandardErrorUsageWriter, StandardFullHelpWriter, +}; +use log::{error, info}; +use uefi_raw::Status; /// Default configuration file path. const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; @@ -38,65 +41,61 @@ impl Default for SproutOptions { } /// The options parser mechanism for Sprout. -impl OptionsRepresentable for SproutOptions { - /// Produce the [SproutOptions] structure. - type Output = Self; +impl SproutOptions { + /// Produces [SproutOptions] from the arguments provided by the UEFI core. + /// Internally we utilize the `jaarg` argument parser which has excellent no_std support. + pub fn parse() -> Result { + // All the options for the Sprout executable. + const OPTIONS: Opts<&str> = Opts::new(&[ + Opt::help_flag("help", &["--help"]).help_text("Display Sprout Help"), + Opt::flag("autoconfigure", &["--autoconfigure"]) + .help_text("Enable Sprout autoconfiguration"), + Opt::value("config", &["--config"], "PATH") + .help_text("Path to Sprout configuration file"), + Opt::value("boot", &["--boot"], "ENTRY").help_text("Entry to boot, bypassing the menu"), + Opt::flag("force-menu", &["--force-menu"]).help_text("Force showing the boot menu"), + Opt::value("menu-timeout", &["--menu-timeout"], "TIMEOUT") + .help_text("Boot menu timeout, in seconds"), + ]); - /// All the Sprout options that are defined. - fn options() -> &'static [(&'static str, OptionDescription<'static>)] { - &[ - ( - "autoconfigure", - OptionDescription { - description: "Enable Sprout Autoconfiguration", - form: OptionForm::Flag, - }, - ), - ( - "config", - OptionDescription { - description: "Path to Sprout configuration file", - form: OptionForm::Value, - }, - ), - ( - "boot", - OptionDescription { - description: "Entry to boot, bypassing the menu", - form: OptionForm::Value, - }, - ), - ( - "force-menu", - OptionDescription { - description: "Force showing of the boot menu", - form: OptionForm::Flag, - }, - ), - ( - "menu-timeout", - OptionDescription { - description: "Boot menu timeout, in seconds", - form: OptionForm::Value, - }, - ), - ( - "help", - OptionDescription { - description: "Display Sprout Help", - form: OptionForm::Help, - }, - ), - ] - } + // Acquire the arguments as determined by the UEFI core. + let args = eficore::env::args()?; + + // Parse the OPTIONS into a map using jaarg. + let parsed = match OPTIONS.parse_map( + "sprout", + args.iter(), + |program_name| { + let ctx = HelpWriterContext { + options: &OPTIONS, + program_name, + }; + info!("{}", StandardFullHelpWriter::new(ctx)); + }, + |program_name, error| { + let ctx = ErrorUsageWriterContext { + options: &OPTIONS, + program_name, + error, + }; + error!("{}", StandardErrorUsageWriter::new(ctx)); + }, + ) { + ParseMapResult::Map(map) => map, + ParseMapResult::ExitSuccess => unsafe { + uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut()); + }, + + ParseMapResult::ExitFailure => unsafe { + uefi::boot::exit(uefi::boot::image_handle(), Status::ABORTED, 0, null_mut()); + }, + }; - /// Produces [SproutOptions] from the parsed raw `options` map. - fn produce(options: BTreeMap>) -> Result { // Use the default value of sprout options and have the raw options be parsed into it. let mut result = Self::default(); - for (key, value) in options { - match key.as_str() { + for (key, value) in parsed { + match key { "autoconfigure" => { // Enable autoconfiguration. result.autoconfigure = true; @@ -104,12 +103,12 @@ impl OptionsRepresentable for SproutOptions { "config" => { // The configuration file to load. - result.config = value.context("--config option requires a value")?; + result.config = value; } "boot" => { // The entry to boot. - result.boot = Some(value.context("--boot option requires a value")?); + result.boot = Some(value); } "force-menu" => { @@ -119,7 +118,6 @@ impl OptionsRepresentable for SproutOptions { "menu-timeout" => { // The timeout for the boot menu in seconds. - let value = value.context("--menu-timeout option requires a value")?; let value = value .parse::() .context("menu-timeout must be a number")?; diff --git a/crates/boot/src/options/parser.rs b/crates/boot/src/options/parser.rs deleted file mode 100644 index c8252fb..0000000 --- a/crates/boot/src/options/parser.rs +++ /dev/null @@ -1,153 +0,0 @@ -use alloc::collections::BTreeMap; -use alloc::string::{String, ToString}; -use anyhow::{Context, Result, bail}; -use core::ptr::null_mut; -use eficore::env; -use log::info; -use uefi_raw::Status; - -/// The type of option. This disambiguates different behavior -/// of how options are handled. -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub enum OptionForm { - /// A flag, like --verbose. - Flag, - /// A value, in the form --abc 123 or --abc=123. - Value, - /// Help flag, like --help. - Help, -} - -/// The description of an option, used in the option parser -/// to make decisions about how to progress. -#[derive(Debug, Clone)] -pub struct OptionDescription<'a> { - /// The description of the option. - pub description: &'a str, - /// The type of option to parse as. - pub form: OptionForm, -} - -/// Represents a type that can be parsed from command line arguments. -/// This is a super minimal options parser mechanism just for Sprout. -pub trait OptionsRepresentable { - /// The output type that parsing will produce. - type Output; - - /// The configured options for this type. This should describe all the options - /// that are valid to produce the type. The left-hand side is the name of the option, - /// and the right-hand side is the description. - fn options() -> &'static [(&'static str, OptionDescription<'static>)]; - - /// Produces the type by taking the `options` and processing it into the output. - fn produce(options: BTreeMap>) -> Result; - - /// 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 the format: --abc=123 - fn parse_raw() -> Result>> { - // Access the configured options for this type. - let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec()); - - // Collect all the arguments to Sprout. - // Skip the first argument, which is the path to our executable. - let args = env::args()?; - - // 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 option 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 let Some((part_key, part_value)) = option.split_once('=') { - let part_key = part_key.to_string(); - let part_value = part_value.to_string(); - option = part_key; - value = Some(part_value); - } - - // Error on empty option names. - if option.is_empty() { - bail!("invalid empty option"); - } - - // Find the description of the configured option, if any. - let Some(description) = configured.get(option.as_str()) else { - bail!("invalid option: --{option}"); - }; - - // Check if the option requires a value and error if none was provided. - if description.form == OptionForm::Value && 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 None. - value = if let Some(next) = maybe_next - && !next.starts_with("--") - { - iterator.next() - } else { - None - }; - } - - // If the option form does not support a value and there is a value, error. - if description.form != OptionForm::Value && value.is_some() { - bail!("option --{} does not take a value", option); - } - - // Handle the --help flag case. - if description.form == OptionForm::Help { - // Generic configured options output. - info!("Configured Options:"); - for (name, description) in &configured { - info!( - " --{}{}: {}", - name, - if description.form == OptionForm::Value { - " " - } else { - "" - }, - description.description - ); - } - // Exit because the help has been displayed. - unsafe { - uefi::boot::exit(uefi::boot::image_handle(), Status::SUCCESS, 0, null_mut()); - }; - } - - // Insert the option and the value into the map. - options.insert(option, value); - } - Ok(options) - } - - /// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce]. - fn parse() -> Result { - // Parse the program arguments into a raw map. - let options = Self::parse_raw().context("unable to parse options")?; - // Produce the options from the map. - Self::produce(options) - } -}