document ( by hand :( ) all of the code of sprout

This commit is contained in:
2025-10-20 00:06:46 -07:00
parent 106064d3e7
commit 3453826e9d
8 changed files with 261 additions and 8 deletions

View File

@@ -11,70 +11,109 @@ use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem;
/// BLS entry parser.
mod entry;
/// The default path to the BLS entries directory.
const BLS_TEMPLATE_PATH: &str = "\\loader\\entries";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
/// entries from an input template.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration,
/// The path to the BLS entries directory.
#[serde(default = "default_bls_path")]
pub path: String,
}
fn default_bls_path() -> String {
"\\loader\\entries".to_string()
BLS_TEMPLATE_PATH.to_string()
}
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate(
context: Rc<SproutContext>,
bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
let mut entries = Vec::new();
// Stamp the path to the BLS entries directory.
let path = context.stamp(&bls.path);
// Resolve the path to the BLS entries directory.
let resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
.context("unable to resolve bls path")?;
// Open exclusive access to the BLS filesystem.
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(resolved.filesystem_handle)
.context("unable to open bls filesystem")?;
let mut fs = FileSystem::new(fs);
// Convert the subpath to the BLS entries directory to a string.
let sub_text_path = resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert subpath to string")?;
// Produce a path to the BLS entries directory.
let entries_path = Path::new(&sub_text_path);
// Read the BLS entries directory.
let entries_iter = fs
.read_dir(entries_path)
.context("unable to read bls entries")?;
// For each entry in the BLS entries directory, parse the entry and add it to the list.
for entry in entries_iter {
let entry = entry?;
// Unwrap the entry file info.
let entry = entry.context("unable to read bls item entry")?;
// Skip items that are not regular files.
if !entry.is_regular_file() {
continue;
}
// Get the file name of the filesystem item.
let name = entry.file_name().to_string();
// Ignore files that are not .conf files.
if !name.ends_with(".conf") {
continue;
}
// Produce the full path to the entry file.
let full_entry_path = CString16::try_from(format!("{}\\{}", sub_text_path, name).as_str())
.context("unable to construct full entry path")?;
let full_entry_path = Path::new(&full_entry_path);
// Read the entry file.
let content = fs
.read(full_entry_path)
.context("unable to read bls file")?;
// Parse the entry file as a UTF-8 string.
let content = String::from_utf8(content).context("unable to read bls entry as utf8")?;
// Parse the entry file as a BLS entry.
let entry = BlsEntry::from_str(&content).context("unable to parse bls entry")?;
// Ignore entries that are not valid for Sprout.
if !entry.is_valid() {
continue;
}
// Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork();
context.set("title", entry.title().unwrap_or(name));
context.set("chainload", entry.chainload_path().unwrap_or_default());
context.set("options", entry.options().unwrap_or_default());
context.set("initrd", entry.initrd_path().unwrap_or_default());
// Add the entry to the list with a frozen context.
entries.push((context.freeze(), bls.entry.clone()));
}

View File

@@ -1,59 +1,82 @@
use anyhow::{Error, Result};
use std::str::FromStr;
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)]
pub struct BlsEntry {
/// The title of the entry.
pub title: Option<String>,
/// The options to pass to the entry.
pub options: Option<String>,
/// The path to the linux kernel.
pub linux: Option<String>,
/// The path to the initrd.
pub initrd: Option<String>,
/// The path to an EFI image.
pub efi: Option<String>,
}
/// Parser for a BLS entry.
impl FromStr for BlsEntry {
type Err = Error;
/// Parses the `input` as a BLS entry file.
fn from_str(input: &str) -> Result<Self> {
// All the fields in a BLS entry we understand.
// Set all to None initially.
let mut title: Option<String> = None;
let mut options: Option<String> = None;
let mut linux: Option<String> = None;
let mut initrd: Option<String> = None;
let mut efi: Option<String> = None;
// Iterate over each line in the input and parse it.
for line in input.lines() {
// Trim the line.
let line = line.trim();
// Split the line once by a space.
let Some((key, value)) = line.split_once(" ") else {
continue;
};
// Match the key to a field we understand.
match key {
// The title of the entry.
"title" => {
title = Some(value.trim().to_string());
}
// The options to pass to the entry.
"options" => {
options = Some(value.trim().to_string());
}
// The path to the linux kernel.
"linux" => {
linux = Some(value.trim().to_string());
}
// The path to the initrd.
"initrd" => {
initrd = Some(value.trim().to_string());
}
// The path to an EFI image.
"efi" => {
efi = Some(value.trim().to_string());
}
// Ignore any other key.
_ => {
continue;
}
}
}
Ok(BlsEntry {
// Produce a BLS entry from the parsed fields.
Ok(Self {
title,
options,
linux,
@@ -64,10 +87,14 @@ impl FromStr for BlsEntry {
}
impl BlsEntry {
/// Checks if this BLS entry is something we can actually boot in Sprout.
pub fn is_valid(&self) -> bool {
self.linux.is_some() || self.efi.is_some()
}
/// Fetches the path to an EFI bootable image to boot, if any.
/// This prioritizes the linux field over efi.
/// It also converts / to \\ to match EFI path style.
pub fn chainload_path(&self) -> Option<String> {
self.linux
.clone()
@@ -75,16 +102,20 @@ impl BlsEntry {
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
}
/// Fetches the path to an initrd to pass to the kernel, if any.
/// It also converts / to \\ to match EFI path style.
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
}
/// Fetches the options to pass to the kernel, if any.
pub fn options(&self) -> Option<String> {
self.options.clone()
}
/// Fetches the title of the entry, if any.
pub fn title(&self) -> Option<String> {
self.title.clone()
}

View File

@@ -5,21 +5,37 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// Matrix generator configuration.
/// The matrix generator produces multiple entries based
/// on input values multiplicatively.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct MatrixConfiguration {
/// The template entry to use for each generated entry.
#[serde(default)]
pub entry: EntryDeclaration,
/// The values to use as the input for the matrix.
#[serde(default)]
pub values: BTreeMap<String, Vec<String>>,
}
/// Builds out multiple generations of `input` based on a matrix style.
/// For example, if input is: {"x": ["a", "b"], "y": ["c", "d"]}
/// It will produce:
/// x: a, y: c
/// x: a, y: d
/// x: b, y: c
/// x: b, y: d
fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, String>> {
// Convert the input into a vector of tuples.
let items: Vec<(String, Vec<String>)> = input.clone().into_iter().collect();
// The result is a vector of maps.
let mut result: Vec<BTreeMap<String, String>> = vec![BTreeMap::new()];
for (key, values) in items {
let mut new_result = Vec::new();
// Produce all the combinations of the input values.
for combination in &result {
for value in &values {
let mut new_combination = combination.clone();
@@ -34,18 +50,23 @@ fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<String, S
result.into_iter().filter(|item| !item.is_empty()).collect()
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
matrix: &MatrixConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
// Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for combination in combinations {
let mut context = context.fork();
// Insert the combination into the context.
context.insert(&combination);
let context = context.freeze();
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone();
entry.title = context.stamp(&entry.title);
entry.actions = entry
@@ -53,6 +74,7 @@ pub fn generate(
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push((context, entry));
}