diff --git a/Cargo.lock b/Cargo.lock index 9ab14d3..963bebe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,11 +71,11 @@ dependencies = [ "anyhow", "bitflags", "edera-sprout-config", + "edera-sprout-eficore", "hex", "log", "sha2", "shlex", - "spin", "toml", "uefi", "uefi-raw", @@ -88,6 +88,18 @@ dependencies = [ "serde", ] +[[package]] +name = "edera-sprout-eficore" +version = "0.0.22" +dependencies = [ + "anyhow", + "bitflags", + "log", + "spin", + "uefi", + "uefi-raw", +] + [[package]] name = "generic-array" version = "0.14.9" diff --git a/Cargo.toml b/Cargo.toml index 7ce9d89..969077a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/config", + "crates/eficore", "crates/sprout", ] resolver = "3" @@ -16,7 +17,6 @@ edition = "2024" bitflags = "2.10.0" log = "0.4.28" spin = "0.10.0" -uefi = "0.36.0" uefi-raw = "0.12.0" [workspace.dependencies.anyhow] @@ -46,6 +46,11 @@ version = "0.9.8" default-features = false features = ["serde", "parse"] +[workspace.dependencies.uefi] +version = "0.36.0" +default-features = false +features = ["alloc", "global_allocator", "panic_handler"] + # Common build profiles # NOTE: We have to compile everything for opt-level = 2 due to optimization passes # which don't handle the UEFI target properly. diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 3bb3862..41d2965 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -11,6 +11,16 @@ We currently only support `x86_64-unknown-uefi` and `aarch64-unknown-uefi` targe To test your changes in QEMU, please run `./hack/dev/boot.sh`, you can specify `x86_64` or `aarch64` as an argument to boot.sh to boot the specified architecture. +## Crate Structure + +Sprout is split into multiple crates: + +- `edera-sprout-config` at `crates/config`: Serialization structures for the Sprout configuration file. +- `edera-sprout-eficore` at `crates/eficore`: Core library for Sprout EFI code. +- `edera-sprout` as `crates/sprout`: Sprout's main crate that contains bootloader logic. + +It is intended that overtime Sprout will be split into even more crates. + ## Hack Scripts You can use the `./hack` scripts to run common development tasks: diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 2381ebe..bd75968 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -13,3 +13,4 @@ default-features = false [lib] name = "edera_sprout_config" +path = "src/lib.rs" diff --git a/crates/eficore/Cargo.toml b/crates/eficore/Cargo.toml new file mode 100644 index 0000000..6a6a89e --- /dev/null +++ b/crates/eficore/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "edera-sprout-eficore" +description = "Sprout EFI Core" +license.workspace = true +version.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +bitflags.workspace = true +log.workspace = true +spin.workspace = true +uefi.workspace = true +uefi-raw.workspace = true + +[lib] +name = "eficore" +path = "src/lib.rs" diff --git a/crates/sprout/src/integrations/bootloader_interface.rs b/crates/eficore/src/bootloader_interface.rs similarity index 97% rename from crates/sprout/src/integrations/bootloader_interface.rs rename to crates/eficore/src/bootloader_interface.rs index b3cf6a8..1edfa5a 100644 --- a/crates/sprout/src/integrations/bootloader_interface.rs +++ b/crates/eficore/src/bootloader_interface.rs @@ -1,7 +1,6 @@ -use crate::integrations::bootloader_interface::bitflags::LoaderFeatures; +use crate::bootloader_interface::bitflags::LoaderFeatures; use crate::platform::timer::PlatformTimer; -use crate::utils::device_path_subpath; -use crate::utils::variables::{VariableClass, VariableController}; +use crate::variables::{VariableClass, VariableController}; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; @@ -103,7 +102,8 @@ impl BootloaderInterface { /// Tell the system the relative path to the partition root of the current bootloader. pub fn set_loader_path(path: &DevicePath) -> Result<()> { - let subpath = device_path_subpath(path).context("unable to get loader path subpath")?; + let subpath = + crate::path::device_path_subpath(path).context("unable to get loader path subpath")?; Self::VENDOR.set_cstr16( "LoaderImageIdentifier", &subpath, diff --git a/crates/sprout/src/integrations/bootloader_interface/bitflags.rs b/crates/eficore/src/bootloader_interface/bitflags.rs similarity index 100% rename from crates/sprout/src/integrations/bootloader_interface/bitflags.rs rename to crates/eficore/src/bootloader_interface/bitflags.rs diff --git a/crates/sprout/src/utils/framebuffer.rs b/crates/eficore/src/framebuffer.rs similarity index 100% rename from crates/sprout/src/utils/framebuffer.rs rename to crates/eficore/src/framebuffer.rs diff --git a/crates/eficore/src/handle.rs b/crates/eficore/src/handle.rs new file mode 100644 index 0000000..ed0ca1e --- /dev/null +++ b/crates/eficore/src/handle.rs @@ -0,0 +1,26 @@ +use anyhow::{Context, Result}; +use uefi::boot::SearchType; +use uefi::{Guid, Handle}; +use uefi_raw::Status; + +/// Find a handle that provides the specified `protocol`. +pub fn find_handle(protocol: &Guid) -> Result> { + // Locate the requested protocol handle. + match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) { + // If a handle is found, the protocol is available. + Ok(handles) => Ok(if handles.is_empty() { + None + } else { + Some(handles[0]) + }), + // If an error occurs, check if it is because the protocol is not available. + // If so, return false. Otherwise, return the error. + Err(error) => { + if error.status() == Status::NOT_FOUND { + Ok(None) + } else { + Err(error).context("unable to determine if the protocol is available") + } + } + } +} diff --git a/crates/eficore/src/lib.rs b/crates/eficore/src/lib.rs new file mode 100644 index 0000000..36ae779 --- /dev/null +++ b/crates/eficore/src/lib.rs @@ -0,0 +1,39 @@ +//! Sprout EFI Core. +//! This crate provides tools for working with the EFI environment. +#![no_std] +extern crate alloc; + +/// EFI handle helpers. +pub mod handle; + +/// Logging support for EFI applications. +pub mod logger; + +/// Disk partitioning support infrastructure. +pub mod partition; + +/// Path handling for UEFI. +pub mod path; + +/// platform: Integration or support code for specific hardware platforms. +pub mod platform; + +/// Secure Boot support. +pub mod secure; + +/// Support for the shim loader application that enables Secure Boot. +pub mod shim; + +/// String utilities. +pub mod strings; + +/// Implements support for the bootloader interface specification. +pub mod bootloader_interface; +/// Support code for the EFI framebuffer. +pub mod framebuffer; +/// Support code for the media loader protocol. +pub mod media_loader; +/// setup: Code that initializes the UEFI environment for Sprout. +pub mod setup; +/// Support code for EFI variables. +pub mod variables; diff --git a/crates/sprout/src/logger.rs b/crates/eficore/src/logger.rs similarity index 100% rename from crates/sprout/src/logger.rs rename to crates/eficore/src/logger.rs diff --git a/crates/sprout/src/utils/media_loader.rs b/crates/eficore/src/media_loader.rs similarity index 100% rename from crates/sprout/src/utils/media_loader.rs rename to crates/eficore/src/media_loader.rs diff --git a/crates/sprout/src/utils/media_loader/constants.rs b/crates/eficore/src/media_loader/constants.rs similarity index 100% rename from crates/sprout/src/utils/media_loader/constants.rs rename to crates/eficore/src/media_loader/constants.rs diff --git a/crates/eficore/src/partition.rs b/crates/eficore/src/partition.rs new file mode 100644 index 0000000..08e97b4 --- /dev/null +++ b/crates/eficore/src/partition.rs @@ -0,0 +1,55 @@ +use anyhow::{Context, Result}; +use uefi::Guid; +use uefi::proto::device_path::DevicePath; +use uefi::proto::media::partition::PartitionInfo; +use uefi_raw::Status; + +/// Represents the type of partition GUID that can be retrieved. +#[derive(PartialEq, Eq)] +pub enum PartitionGuidForm { + /// The partition GUID is the unique partition GUID. + Partition, + /// The partition GUID is the partition type GUID. + PartitionType, +} + +/// Retrieve the partition / partition type GUID of the device root `path`. +/// This only works on GPT partitions. If the root is not a GPT partition, None is returned. +/// If the GUID is all zeros, this will return None. +pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result> { + // Clone the path so we can pass it to the UEFI stack. + let path = path.to_boxed(); + let result = uefi::boot::locate_device_path::(&mut &*path); + let handle = match result { + Ok(handle) => Ok(Some(handle)), + Err(error) => { + // If the error is NOT_FOUND or UNSUPPORTED, we can return None. + // These are non-fatal errors. + if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED { + Ok(None) + } else { + Err(error) + } + } + } + .context("unable to locate device path")?; + + // If we have the handle, we can try to open the partition info protocol. + if let Some(handle) = handle { + // Open the partition info protocol. + let partition_info = uefi::boot::open_protocol_exclusive::(handle) + .context("unable to open partition info protocol")?; + // Find the unique partition GUID. + // If this is not a GPT partition, this will produce None. + Ok(partition_info + .gpt_partition_entry() + .map(|entry| match form { + // Match the form of the partition GUID. + PartitionGuidForm::Partition => entry.unique_partition_guid, + PartitionGuidForm::PartitionType => entry.partition_type_guid.0, + }) + .filter(|guid| !guid.is_zero())) + } else { + Ok(None) + } +} diff --git a/crates/eficore/src/path.rs b/crates/eficore/src/path.rs new file mode 100644 index 0000000..02b82b9 --- /dev/null +++ b/crates/eficore/src/path.rs @@ -0,0 +1,174 @@ +use alloc::borrow::ToOwned; +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use anyhow::{Context, Result}; +use core::ops::Deref; +use uefi::fs::{FileSystem, Path}; +use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; +use uefi::proto::device_path::{DevicePath, PoolDevicePath}; +use uefi::proto::media::fs::SimpleFileSystem; +use uefi::{CString16, Handle}; + +/// Represents the components of a resolved path. +pub struct ResolvedPath { + /// The root path of the resolved path. This is the device itself. + /// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/" + pub root_path: Box, + /// The subpath of the resolved path. This is the path to the file. + /// For example, "\EFI\BOOT\BOOTX64.efi" + pub sub_path: Box, + /// The full path of the resolved path. This is the safest path to use. + /// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" + pub full_path: Box, + /// The handle of the filesystem containing the path. + /// This can be used to acquire a [SimpleFileSystem] protocol to read the file. + pub filesystem_handle: Handle, +} + +impl ResolvedPath { + /// Read the file specified by this path into a buffer and return it. + pub fn read_file(&self) -> Result> { + let fs = uefi::boot::open_protocol_exclusive::(self.filesystem_handle) + .context("unable to open filesystem protocol")?; + let mut fs = FileSystem::new(fs); + let path = self + .sub_path + .to_string(DisplayOnly(false), AllowShortcuts(false))?; + let content = fs.read(Path::new(&path)); + content.context("unable to read file contents") + } +} + +/// Checks if a [CString16] contains a char `c`. +/// We need to call to_string() because CString16 doesn't support `contains` with a char. +fn cstring16_contains_char(string: &CString16, c: char) -> bool { + string.to_string().contains(c) +} + +/// Parses the input `path` as a [DevicePath]. +/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. +pub fn text_to_device_path(path: &str) -> Result { + let path = CString16::try_from(path).context("unable to convert path to CString16")?; + let device_path_from_text = uefi::boot::open_protocol_exclusive::( + uefi::boot::get_handle_for_protocol::() + .context("no device path from text protocol")?, + ) + .context("unable to open device path from text protocol")?; + + device_path_from_text + .convert_text_to_device_path(&path) + .context("unable to convert text to device path") +} + +/// Grabs the root part of the `path`. +/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" +/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)" +pub fn device_path_root(path: &DevicePath) -> Result { + let mut path = path + .node_iter() + .filter_map(|item| { + let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); + if item + .as_ref() + .map(|item| cstring16_contains_char(item, '(')) + .unwrap_or(false) + { + Some(item.unwrap_or_default()) + } else { + None + } + }) + .map(|item| item.to_string()) + .collect::>() + .join("/"); + path.push('/'); + Ok(path) +} + +/// Grabs the part of the `path` after the root. +/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" +/// it will give "\EFI\BOOT\BOOTX64.efi" +pub fn device_path_subpath(path: &DevicePath) -> Result { + let path = path + .node_iter() + .filter_map(|item| { + let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); + if item + .as_ref() + .map(|item| cstring16_contains_char(item, '(')) + .unwrap_or(false) + { + None + } else { + Some(item.unwrap_or_default()) + } + }) + .map(|item| item.to_string()) + .collect::>() + .join("\\"); + Ok(path) +} + +/// Resolve a path specified by `input` to its various components. +/// Uses `default_root_path` as the base root if one is not specified in the path. +/// Returns [ResolvedPath] which contains the resolved components. +pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result { + let mut path = text_to_device_path(input).context("unable to convert text to path")?; + let path_has_device = path + .node_iter() + .next() + .map(|it| { + it.to_string(DisplayOnly(false), AllowShortcuts(false)) + .unwrap_or_default() + }) + .map(|it| it.to_string().contains('(')) + .unwrap_or(false); + if !path_has_device { + let mut input = input.to_string(); + if !input.starts_with('\\') { + input.insert(0, '\\'); + } + + let default_root_path = default_root_path.context("unable to get default root path")?; + + input.insert_str( + 0, + device_path_root(default_root_path) + .context("unable to get loaded image device root")? + .as_str(), + ); + path = text_to_device_path(input.as_str()).context("unable to convert text to path")?; + } + + let path = path.to_boxed(); + let root = device_path_root(path.as_ref()).context("unable to convert root to path")?; + let root_path = text_to_device_path(root.as_str()) + .context("unable to convert root to path")? + .to_boxed(); + let root_path = root_path.as_ref(); + + // locate_device_path modifies the path, so we need to clone it. + let root_path_modifiable = root_path.to_owned(); + let handle = uefi::boot::locate_device_path::(&mut &*root_path_modifiable) + .context("unable to locate filesystem device path")?; + let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?; + Ok(ResolvedPath { + root_path: root_path.to_boxed(), + sub_path: text_to_device_path(subpath.as_str())?.to_boxed(), + full_path: path, + filesystem_handle: handle, + }) +} + +/// Read the contents of a file at the location specified with the `input` path. +/// Internally, this uses [resolve_path] to resolve the path to its various components. +/// [resolve_path] is passed the `default_root_path` which should specify a base root. +/// +/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved +/// filesystem handle, so care must be taken to call this function outside a scope with +/// the filesystem handle protocol acquired. +pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result> { + let resolved = resolve_path(default_root_path, input)?; + resolved.read_file() +} diff --git a/crates/eficore/src/platform.rs b/crates/eficore/src/platform.rs new file mode 100644 index 0000000..3a29cc3 --- /dev/null +++ b/crates/eficore/src/platform.rs @@ -0,0 +1,4 @@ +/// Timer support. +pub mod timer; +/// TPM support. +pub mod tpm; diff --git a/crates/sprout/src/platform/timer.rs b/crates/eficore/src/platform/timer.rs similarity index 100% rename from crates/sprout/src/platform/timer.rs rename to crates/eficore/src/platform/timer.rs diff --git a/crates/sprout/src/platform/timer/aarch64.rs b/crates/eficore/src/platform/timer/aarch64.rs similarity index 100% rename from crates/sprout/src/platform/timer/aarch64.rs rename to crates/eficore/src/platform/timer/aarch64.rs diff --git a/crates/sprout/src/platform/timer/x86_64.rs b/crates/eficore/src/platform/timer/x86_64.rs similarity index 100% rename from crates/sprout/src/platform/timer/x86_64.rs rename to crates/eficore/src/platform/timer/x86_64.rs diff --git a/crates/sprout/src/platform/tpm.rs b/crates/eficore/src/platform/tpm.rs similarity index 96% rename from crates/sprout/src/platform/tpm.rs rename to crates/eficore/src/platform/tpm.rs index 14f81ff..88ee913 100644 --- a/crates/sprout/src/platform/tpm.rs +++ b/crates/eficore/src/platform/tpm.rs @@ -1,4 +1,3 @@ -use crate::utils; use anyhow::{Context, Result}; use uefi::ResultExt; use uefi::boot::ScopedProtocol; @@ -43,8 +42,8 @@ impl PlatformTpm { /// Returns None if TPM is not available. fn protocol() -> Result> { // Attempt to acquire the TCG2 protocol handle. If it's not available, return None. - let Some(handle) = - utils::find_handle(&Tcg2Protocol::GUID).context("unable to determine tpm presence")? + let Some(handle) = crate::handle::find_handle(&Tcg2Protocol::GUID) + .context("unable to determine tpm presence")? else { return Ok(None); }; diff --git a/crates/sprout/src/secure.rs b/crates/eficore/src/secure.rs similarity index 89% rename from crates/sprout/src/secure.rs rename to crates/eficore/src/secure.rs index a28e227..a33a600 100644 --- a/crates/sprout/src/secure.rs +++ b/crates/eficore/src/secure.rs @@ -1,4 +1,4 @@ -use crate::utils::variables::VariableController; +use crate::variables::VariableController; use anyhow::Result; /// Secure boot services. diff --git a/crates/sprout/src/setup.rs b/crates/eficore/src/setup.rs similarity index 100% rename from crates/sprout/src/setup.rs rename to crates/eficore/src/setup.rs diff --git a/crates/sprout/src/integrations/shim.rs b/crates/eficore/src/shim.rs similarity index 97% rename from crates/sprout/src/integrations/shim.rs rename to crates/eficore/src/shim.rs index 21e62b6..56777d2 100644 --- a/crates/sprout/src/integrations/shim.rs +++ b/crates/eficore/src/shim.rs @@ -1,8 +1,7 @@ -use crate::integrations::shim::hook::SecurityHook; +use crate::path::ResolvedPath; use crate::secure::SecureBoot; -use crate::utils; -use crate::utils::ResolvedPath; -use crate::utils::variables::{VariableClass, VariableController}; +use crate::shim::hook::SecurityHook; +use crate::variables::{VariableClass, VariableController}; use alloc::boxed::Box; use alloc::string::ToString; use alloc::vec::Vec; @@ -90,7 +89,7 @@ impl<'a> ShimInput<'a> { let path = path .to_string(DisplayOnly(false), AllowShortcuts(false)) .context("unable to convert device path to string")?; - let path = utils::resolve_path(None, &path.to_string()) + let path = crate::path::resolve_path(None, &path.to_string()) .context("unable to resolve path")?; // Read the file path. let data = path.read_file()?; @@ -163,14 +162,14 @@ impl ShimSupport { /// Determines whether the shim is loaded. pub fn loaded() -> Result { - Ok(utils::find_handle(&Self::SHIM_LOCK_GUID) + Ok(crate::handle::find_handle(&Self::SHIM_LOCK_GUID) .context("unable to find shim lock protocol")? .is_some()) } /// Determines whether the shim loader is available. pub fn loader_available() -> Result { - Ok(utils::find_handle(&Self::SHIM_IMAGE_LOADER_GUID) + Ok(crate::handle::find_handle(&Self::SHIM_IMAGE_LOADER_GUID) .context("unable to find shim image loader protocol")? .is_some()) } @@ -178,7 +177,7 @@ impl ShimSupport { /// Use the shim to validate the `input`, returning [ShimVerificationOutput] when complete. pub fn verify(input: ShimInput) -> Result { // Acquire the handle to the shim lock protocol. - let handle = utils::find_handle(&Self::SHIM_LOCK_GUID) + let handle = crate::handle::find_handle(&Self::SHIM_LOCK_GUID) .context("unable to find shim lock protocol")? .ok_or_else(|| anyhow!("unable to find shim lock protocol"))?; // Acquire the protocol exclusively to the shim lock. diff --git a/crates/sprout/src/integrations/shim/hook.rs b/crates/eficore/src/shim/hook.rs similarity index 96% rename from crates/sprout/src/integrations/shim/hook.rs rename to crates/eficore/src/shim/hook.rs index d8a0876..1778b60 100644 --- a/crates/sprout/src/integrations/shim/hook.rs +++ b/crates/eficore/src/shim/hook.rs @@ -1,5 +1,4 @@ -use crate::integrations::shim::{ShimInput, ShimSupport, ShimVerificationOutput}; -use crate::utils; +use crate::shim::{ShimInput, ShimSupport, ShimVerificationOutput}; use anyhow::{Context, Result}; use core::slice; use log::warn; @@ -181,14 +180,14 @@ impl SecurityHook { /// Install the security hook if needed. pub fn install() -> Result { // Find the security arch protocol. If we can't find it, we will return false. - let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) + let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID) .context("unable to check security arch existence")? else { return Ok(false); }; // Find the security arch2 protocol. If we can't find it, we will return false. - let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) + let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID) .context("unable to check security arch2 existence")? else { return Ok(false); @@ -228,14 +227,14 @@ impl SecurityHook { /// Uninstalls the global security hook, if installed. pub fn uninstall() -> Result<()> { // Find the security arch protocol. If we can't find it, we will do nothing. - let Some(hook_arch) = utils::find_handle(&SECURITY_ARCH_GUID) + let Some(hook_arch) = crate::handle::find_handle(&SECURITY_ARCH_GUID) .context("unable to check security arch existence")? else { return Ok(()); }; // Find the security arch2 protocol. If we can't find it, we will do nothing. - let Some(hook_arch2) = utils::find_handle(&SECURITY_ARCH2_GUID) + let Some(hook_arch2) = crate::handle::find_handle(&SECURITY_ARCH2_GUID) .context("unable to check security arch2 existence")? else { return Ok(()); diff --git a/crates/eficore/src/strings.rs b/crates/eficore/src/strings.rs new file mode 100644 index 0000000..f8d5603 --- /dev/null +++ b/crates/eficore/src/strings.rs @@ -0,0 +1,22 @@ +use alloc::vec::Vec; +use anyhow::{Context, Result, bail}; +use uefi::CString16; + +/// Convert a byte slice into a CString16. +pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result { + // Validate the input bytes are the right length. + if !bytes.len().is_multiple_of(2) { + bail!("utf16 bytes must be a multiple of 2"); + } + + // Convert the bytes to UTF-16 data. + let data = bytes + // Chunk everything into two bytes. + .chunks_exact(2) + // Reinterpret the bytes as u16 little-endian. + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + // Collect the result into a vector. + .collect::>(); + + CString16::try_from(data).context("unable to convert utf16 bytes to CString16") +} diff --git a/crates/sprout/src/utils/variables.rs b/crates/eficore/src/variables.rs similarity index 98% rename from crates/sprout/src/utils/variables.rs rename to crates/eficore/src/variables.rs index c5b7c4c..5fe860c 100644 --- a/crates/sprout/src/utils/variables.rs +++ b/crates/eficore/src/variables.rs @@ -1,4 +1,4 @@ -use crate::utils; +use crate::strings; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; @@ -59,7 +59,7 @@ impl VariableController { match uefi::runtime::get_variable_boxed(&name, &self.vendor) { Ok((data, _)) => { // Try to decode UTF-16 bytes to a CString16. - match utils::utf16_bytes_to_cstring16(&data) { + match strings::utf16_bytes_to_cstring16(&data) { Ok(value) => { // We have a value, so return the UTF-8 value. Ok(Some(value.to_string())) diff --git a/crates/sprout/Cargo.toml b/crates/sprout/Cargo.toml index 9a212f1..35696de 100644 --- a/crates/sprout/Cargo.toml +++ b/crates/sprout/Cargo.toml @@ -11,19 +11,14 @@ edition.workspace = true anyhow.workspace = true bitflags.workspace = true edera-sprout-config.path = "../config" +edera-sprout-eficore.path = "../eficore" hex.workspace = true sha2.workspace = true shlex.workspace = true -spin.workspace = true toml.workspace = true log.workspace = true - -[dependencies.uefi] -workspace = true -features = ["alloc", "global_allocator", "panic_handler"] - -[dependencies.uefi-raw] -workspace = true +uefi.workspace = true +uefi-raw.workspace = true [[bin]] name = "sprout" diff --git a/crates/sprout/src/actions/chainload.rs b/crates/sprout/src/actions/chainload.rs index 66483f6..6c1176d 100644 --- a/crates/sprout/src/actions/chainload.rs +++ b/crates/sprout/src/actions/chainload.rs @@ -1,13 +1,13 @@ use crate::context::SproutContext; -use crate::integrations::bootloader_interface::BootloaderInterface; -use crate::integrations::shim::{ShimInput, ShimSupport}; use crate::utils; -use crate::utils::media_loader::MediaLoaderHandle; -use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use alloc::boxed::Box; use alloc::rc::Rc; use anyhow::{Context, Result, bail}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; +use eficore::bootloader_interface::BootloaderInterface; +use eficore::media_loader::MediaLoaderHandle; +use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; +use eficore::shim::{ShimInput, ShimSupport}; use log::error; use uefi::CString16; use uefi::proto::loaded_image::LoadedImage; @@ -18,7 +18,7 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat let sprout_image = uefi::boot::image_handle(); // Resolve the path to the image to chainload. - let resolved = utils::resolve_path( + let resolved = eficore::path::resolve_path( Some(context.root().loaded_image_path()?), &context.stamp(&configuration.path), ) @@ -68,9 +68,11 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat // If an initrd is provided, register it with the EFI stack. let mut initrd_handle = None; if let Some(linux_initrd) = initrd { - let content = - utils::read_file_contents(Some(context.root().loaded_image_path()?), &linux_initrd) - .context("unable to read linux 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")?; diff --git a/crates/sprout/src/actions/edera.rs b/crates/sprout/src/actions/edera.rs index 467d610..147ef60 100644 --- a/crates/sprout/src/actions/edera.rs +++ b/crates/sprout/src/actions/edera.rs @@ -1,15 +1,7 @@ use crate::{ actions, context::SproutContext, - utils::{ - self, - media_loader::{ - MediaLoaderHandle, - constants::xen::{ - XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID, - }, - }, - }, + utils::{self}, }; use alloc::rc::Rc; use alloc::string::{String, ToString}; @@ -17,6 +9,12 @@ use alloc::{format, vec}; use anyhow::{Context, Result}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::edera::EderaConfiguration; +use eficore::media_loader::{ + MediaLoaderHandle, + constants::xen::{ + XEN_EFI_CONFIG_MEDIA_GUID, XEN_EFI_KERNEL_MEDIA_GUID, XEN_EFI_RAMDISK_MEDIA_GUID, + }, +}; use log::error; use uefi::Guid; @@ -79,8 +77,9 @@ fn register_media_loader_file( // Stamp the path to the file. let path = context.stamp(path); // Read the file contents. - let content = utils::read_file_contents(Some(context.root().loaded_image_path()?), &path) - .context(format!("unable to read {} file", what))?; + let content = + eficore::path::read_file_contents(Some(context.root().loaded_image_path()?), &path) + .context(format!("unable to read {} file", what))?; // Register the media loader. let handle = MediaLoaderHandle::register(guid, content.into_boxed_slice()) .context(format!("unable to register {} media loader", what))?; diff --git a/crates/sprout/src/config/loader.rs b/crates/sprout/src/config/loader.rs index ec8c83b..da45b78 100644 --- a/crates/sprout/src/config/loader.rs +++ b/crates/sprout/src/config/loader.rs @@ -1,10 +1,9 @@ use crate::options::SproutOptions; -use crate::platform::tpm::PlatformTpm; -use crate::utils; use alloc::vec::Vec; use anyhow::{Context, Result, bail}; use core::ops::Deref; use edera_sprout_config::{RootConfiguration, latest_version}; +use eficore::platform::tpm::PlatformTpm; use log::info; use toml::Value; use uefi::proto::device_path::LoadedImageDevicePath; @@ -21,7 +20,7 @@ fn load_raw_config(options: &SproutOptions) -> Result> { info!("configuration file: {}", options.config); // Read the contents of the sprout config file. - let content = utils::read_file_contents(Some(&path), &options.config) + let content = eficore::path::read_file_contents(Some(&path), &options.config) .context("unable to read sprout config file")?; // Measure the sprout.toml into the TPM, if needed and possible. diff --git a/crates/sprout/src/context.rs b/crates/sprout/src/context.rs index f8a8b31..ea33c64 100644 --- a/crates/sprout/src/context.rs +++ b/crates/sprout/src/context.rs @@ -1,5 +1,4 @@ use crate::options::SproutOptions; -use crate::platform::timer::PlatformTimer; use alloc::boxed::Box; use alloc::collections::{BTreeMap, BTreeSet}; use alloc::format; @@ -10,6 +9,7 @@ use anyhow::anyhow; use anyhow::{Result, bail}; use core::cmp::Reverse; use edera_sprout_config::actions::ActionDeclaration; +use eficore::platform::timer::PlatformTimer; use uefi::proto::device_path::DevicePath; /// The maximum number of iterations that can be performed in [SproutContext::finalize]. diff --git a/crates/sprout/src/drivers.rs b/crates/sprout/src/drivers.rs index 270886e..accc005 100644 --- a/crates/sprout/src/drivers.rs +++ b/crates/sprout/src/drivers.rs @@ -1,12 +1,11 @@ use crate::context::SproutContext; -use crate::integrations::shim::{ShimInput, ShimSupport}; -use crate::utils; use alloc::collections::BTreeMap; use alloc::format; use alloc::rc::Rc; use alloc::string::String; use anyhow::{Context, Result}; use edera_sprout_config::drivers::DriverDeclaration; +use eficore::shim::{ShimInput, ShimSupport}; use log::info; use uefi::boot::SearchType; @@ -16,7 +15,7 @@ fn load_driver(context: Rc, driver: &DriverDeclaration) -> Result let sprout_image = uefi::boot::image_handle(); // Resolve the path to the driver image. - let resolved = utils::resolve_path( + let resolved = eficore::path::resolve_path( Some(context.root().loaded_image_path()?), &context.stamp(&driver.path), ) diff --git a/crates/sprout/src/extractors/filesystem_device_match.rs b/crates/sprout/src/extractors/filesystem_device_match.rs index 33911d6..87909c4 100644 --- a/crates/sprout/src/extractors/filesystem_device_match.rs +++ b/crates/sprout/src/extractors/filesystem_device_match.rs @@ -1,11 +1,11 @@ use crate::context::SproutContext; -use crate::utils; use alloc::rc::Rc; use alloc::string::String; use anyhow::{Context, Result, anyhow, bail}; use core::ops::Deref; use core::str::FromStr; use edera_sprout_config::extractors::filesystem_device_match::FilesystemDeviceMatchExtractor; +use eficore::partition::PartitionGuidForm; use uefi::fs::{FileSystem, Path}; use uefi::proto::device_path::DevicePath; use uefi::proto::media::file::{File, FileSystemVolumeLabel}; @@ -48,8 +48,9 @@ pub fn extract( .to_boxed(); // Fetch the partition uuid for this filesystem. - let partition_uuid = utils::partition_guid(&root, utils::PartitionGuidForm::Partition) - .context("unable to fetch the partition uuid of the filesystem")?; + let partition_uuid = + eficore::partition::partition_guid(&root, PartitionGuidForm::Partition) + .context("unable to fetch the partition uuid of the filesystem")?; // Compare the partition uuid to the parsed uuid. // If it does not match, continue to the next filesystem. @@ -73,7 +74,7 @@ pub fn extract( // Fetch the partition type uuid for this filesystem. let partition_type_uuid = - utils::partition_guid(&root, utils::PartitionGuidForm::PartitionType) + eficore::partition::partition_guid(&root, PartitionGuidForm::PartitionType) .context("unable to fetch the partition uuid of the filesystem")?; // Compare the partition type uuid to the parsed uuid. // If it does not match, continue to the next filesystem. @@ -133,7 +134,7 @@ pub fn extract( .context("unable to open filesystem device path")?; let path = path.deref(); // Acquire the device path root as a string. - return utils::device_path_root(path).context("unable to get device path root"); + return eficore::path::device_path_root(path).context("unable to get device path root"); } // If there is a fallback value, use it at this point. diff --git a/crates/sprout/src/generators/bls.rs b/crates/sprout/src/generators/bls.rs index 91d3d2e..ebdf088 100644 --- a/crates/sprout/src/generators/bls.rs +++ b/crates/sprout/src/generators/bls.rs @@ -1,7 +1,6 @@ use crate::context::SproutContext; use crate::entries::BootableEntry; use crate::generators::bls::entry::BlsEntry; -use crate::utils; use crate::utils::vercmp; use alloc::format; use alloc::rc::Rc; @@ -89,8 +88,9 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result Result<()> { // Grab the partition GUID of the ESP that sprout was loaded from. let loaded_image_partition_guid = - utils::partition_guid(&loaded_image_path, PartitionGuidForm::Partition) + eficore::partition::partition_guid(&loaded_image_path, PartitionGuidForm::Partition) .context("unable to retrieve loaded image partition guid")?; // Set the partition GUID of the ESP that sprout was loaded from in the bootloader interface. diff --git a/crates/sprout/src/menu.rs b/crates/sprout/src/menu.rs index c80ae47..b293ba2 100644 --- a/crates/sprout/src/menu.rs +++ b/crates/sprout/src/menu.rs @@ -1,9 +1,9 @@ use crate::entries::BootableEntry; -use crate::integrations::bootloader_interface::BootloaderInterface; -use crate::platform::timer::PlatformTimer; use alloc::vec; use anyhow::{Context, Result, bail}; use core::time::Duration; +use eficore::bootloader_interface::BootloaderInterface; +use eficore::platform::timer::PlatformTimer; use log::{info, warn}; use uefi::ResultExt; use uefi::boot::TimerTrigger; diff --git a/crates/sprout/src/platform.rs b/crates/sprout/src/platform.rs deleted file mode 100644 index 7020413..0000000 --- a/crates/sprout/src/platform.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// timer: Platform timer support. -pub mod timer; -/// tpm: Platform TPM support. -pub mod tpm; diff --git a/crates/sprout/src/utils.rs b/crates/sprout/src/utils.rs index 62b8fc9..699f79d 100644 --- a/crates/sprout/src/utils.rs +++ b/crates/sprout/src/utils.rs @@ -1,199 +1,10 @@ -use alloc::borrow::ToOwned; -use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use anyhow::{Context, Result, bail}; -use core::ops::Deref; use sha2::{Digest, Sha256}; -use uefi::boot::SearchType; -use uefi::fs::{FileSystem, Path}; -use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly}; -use uefi::proto::device_path::{DevicePath, PoolDevicePath}; -use uefi::proto::media::fs::SimpleFileSystem; -use uefi::proto::media::partition::PartitionInfo; -use uefi::{CString16, Guid, Handle}; -use uefi_raw::Status; - -/// Support code for the EFI framebuffer. -pub mod framebuffer; - -/// Support code for the media loader protocol. -pub mod media_loader; - -/// Support code for EFI variables. -pub mod variables; /// Implements a version comparison algorithm according to the BLS specification. pub mod vercmp; -/// Parses the input `path` as a [DevicePath]. -/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol. -pub fn text_to_device_path(path: &str) -> Result { - let path = CString16::try_from(path).context("unable to convert path to CString16")?; - let device_path_from_text = uefi::boot::open_protocol_exclusive::( - uefi::boot::get_handle_for_protocol::() - .context("no device path from text protocol")?, - ) - .context("unable to open device path from text protocol")?; - - device_path_from_text - .convert_text_to_device_path(&path) - .context("unable to convert text to device path") -} - -/// Checks if a [CString16] contains a char `c`. -/// We need to call to_string() because CString16 doesn't support `contains` with a char. -fn cstring16_contains_char(string: &CString16, c: char) -> bool { - string.to_string().contains(c) -} - -/// Grabs the root part of the `path`. -/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" -/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)" -pub fn device_path_root(path: &DevicePath) -> Result { - let mut path = path - .node_iter() - .filter_map(|item| { - let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); - if item - .as_ref() - .map(|item| cstring16_contains_char(item, '(')) - .unwrap_or(false) - { - Some(item.unwrap_or_default()) - } else { - None - } - }) - .map(|item| item.to_string()) - .collect::>() - .join("/"); - path.push('/'); - Ok(path) -} - -/// Grabs the part of the `path` after the root. -/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" -/// it will give "\EFI\BOOT\BOOTX64.efi" -pub fn device_path_subpath(path: &DevicePath) -> Result { - let path = path - .node_iter() - .filter_map(|item| { - let item = item.to_string(DisplayOnly(false), AllowShortcuts(false)); - if item - .as_ref() - .map(|item| cstring16_contains_char(item, '(')) - .unwrap_or(false) - { - None - } else { - Some(item.unwrap_or_default()) - } - }) - .map(|item| item.to_string()) - .collect::>() - .join("\\"); - Ok(path) -} - -/// Represents the components of a resolved path. -pub struct ResolvedPath { - /// The root path of the resolved path. This is the device itself. - /// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/" - pub root_path: Box, - /// The subpath of the resolved path. This is the path to the file. - /// For example, "\EFI\BOOT\BOOTX64.efi" - pub sub_path: Box, - /// The full path of the resolved path. This is the safest path to use. - /// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi" - pub full_path: Box, - /// The handle of the filesystem containing the path. - /// This can be used to acquire a [SimpleFileSystem] protocol to read the file. - pub filesystem_handle: Handle, -} - -impl ResolvedPath { - /// Read the file specified by this path into a buffer and return it. - pub fn read_file(&self) -> Result> { - let fs = uefi::boot::open_protocol_exclusive::(self.filesystem_handle) - .context("unable to open filesystem protocol")?; - let mut fs = FileSystem::new(fs); - let path = self - .sub_path - .to_string(DisplayOnly(false), AllowShortcuts(false))?; - let content = fs.read(Path::new(&path)); - content.context("unable to read file contents") - } -} - -/// Resolve a path specified by `input` to its various components. -/// Uses `default_root_path` as the base root if one is not specified in the path. -/// Returns [ResolvedPath] which contains the resolved components. -pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result { - let mut path = text_to_device_path(input).context("unable to convert text to path")?; - let path_has_device = path - .node_iter() - .next() - .map(|it| { - it.to_string(DisplayOnly(false), AllowShortcuts(false)) - .unwrap_or_default() - }) - .map(|it| it.to_string().contains('(')) - .unwrap_or(false); - if !path_has_device { - let mut input = input.to_string(); - if !input.starts_with('\\') { - input.insert(0, '\\'); - } - - let default_root_path = default_root_path.context("unable to get default root path")?; - - input.insert_str( - 0, - device_path_root(default_root_path) - .context("unable to get loaded image device root")? - .as_str(), - ); - path = text_to_device_path(input.as_str()).context("unable to convert text to path")?; - } - - let path = path.to_boxed(); - let root = device_path_root(path.as_ref()).context("unable to convert root to path")?; - let root_path = text_to_device_path(root.as_str()) - .context("unable to convert root to path")? - .to_boxed(); - let root_path = root_path.as_ref(); - - // locate_device_path modifies the path, so we need to clone it. - let root_path_modifiable = root_path.to_owned(); - let handle = uefi::boot::locate_device_path::(&mut &*root_path_modifiable) - .context("unable to locate filesystem device path")?; - let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?; - Ok(ResolvedPath { - root_path: root_path.to_boxed(), - sub_path: text_to_device_path(subpath.as_str())?.to_boxed(), - full_path: path, - filesystem_handle: handle, - }) -} - -/// Read the contents of a file at the location specified with the `input` path. -/// Internally, this uses [resolve_path] to resolve the path to its various components. -/// [resolve_path] is passed the `default_root_path` which should specify a base root. -/// -/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved -/// filesystem handle, so care must be taken to call this function outside a scope with -/// the filesystem handle protocol acquired. -pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result> { - let resolved = resolve_path(default_root_path, input)?; - resolved.read_file() -} - -/// 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()) -} - /// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings. pub fn combine_options>(options: impl Iterator) -> String { options @@ -209,93 +20,7 @@ pub fn unique_hash(input: &str) -> String { hex::encode(Sha256::digest(input.as_bytes())) } -/// Represents the type of partition GUID that can be retrieved. -#[derive(PartialEq, Eq)] -pub enum PartitionGuidForm { - /// The partition GUID is the unique partition GUID. - Partition, - /// The partition GUID is the partition type GUID. - PartitionType, -} - -/// Retrieve the partition / partition type GUID of the device root `path`. -/// This only works on GPT partitions. If the root is not a GPT partition, None is returned. -/// If the GUID is all zeros, this will return None. -pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result> { - // Clone the path so we can pass it to the UEFI stack. - let path = path.to_boxed(); - let result = uefi::boot::locate_device_path::(&mut &*path); - let handle = match result { - Ok(handle) => Ok(Some(handle)), - Err(error) => { - // If the error is NOT_FOUND or UNSUPPORTED, we can return None. - // These are non-fatal errors. - if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED { - Ok(None) - } else { - Err(error) - } - } - } - .context("unable to locate device path")?; - - // If we have the handle, we can try to open the partition info protocol. - if let Some(handle) = handle { - // Open the partition info protocol. - let partition_info = uefi::boot::open_protocol_exclusive::(handle) - .context("unable to open partition info protocol")?; - // Find the unique partition GUID. - // If this is not a GPT partition, this will produce None. - Ok(partition_info - .gpt_partition_entry() - .map(|entry| match form { - // Match the form of the partition GUID. - PartitionGuidForm::Partition => entry.unique_partition_guid, - PartitionGuidForm::PartitionType => entry.partition_type_guid.0, - }) - .filter(|guid| !guid.is_zero())) - } else { - Ok(None) - } -} - -/// Find a handle that provides the specified `protocol`. -pub fn find_handle(protocol: &Guid) -> Result> { - // Locate the requested protocol handle. - match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) { - // If a handle is found, the protocol is available. - Ok(handles) => Ok(if handles.is_empty() { - None - } else { - Some(handles[0]) - }), - // If an error occurs, check if it is because the protocol is not available. - // If so, return false. Otherwise, return the error. - Err(error) => { - if error.status() == Status::NOT_FOUND { - Ok(None) - } else { - Err(error).context("unable to determine if the protocol is available") - } - } - } -} - -/// Convert a byte slice into a CString16. -pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result { - // Validate the input bytes are the right length. - if !bytes.len().is_multiple_of(2) { - bail!("utf16 bytes must be a multiple of 2"); - } - - // Convert the bytes to UTF-16 data. - let data = bytes - // Chunk everything into two bytes. - .chunks_exact(2) - // Reinterpret the bytes as u16 little-endian. - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) - // Collect the result into a vector. - .collect::>(); - - CString16::try_from(data).context("unable to convert utf16 bytes to CString16") +/// 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()) }