36 Commits

Author SHA1 Message Date
6f60a279c3 sprout: version 0.0.14 2025-10-28 01:47:15 -04:00
2e66d8c72e chore(docs): update readme with secure boot notes and roadmap items 2025-10-28 01:43:07 -04:00
86e08c2400 fix(doc/extractors/filesystem-device-match): the extractor will error if no criteria is provided 2025-10-28 00:19:38 -04:00
852823e2eb chore(doc/bls/entry): clarify why char::is_whitespace is used despite newline matching 2025-10-28 00:12:16 -04:00
734ab84054 chore(doc/context): clarify context finalization limit error message 2025-10-28 00:10:22 -04:00
c8a3408fdd fix(extractors/filesystem-device-match): clarify when to use fallback for empty criteria 2025-10-28 00:09:11 -04:00
deeda650a7 fix(autoconfigure/linux): remove debug line 2025-10-28 00:06:02 -04:00
268a2cb28b fix(media-loader): improve safety in the event protocol interface install fails 2025-10-27 23:56:12 -04:00
0b6523906d fix(doc): filesystem-device-match will not return a filesystem when criteria is not provided 2025-10-27 23:39:55 -04:00
3acd0ec7d8 chore(doc): document media loader safety 2025-10-27 23:24:35 -04:00
fe593efa8c chore(autoconfigure/docs): clarify why we append / to a device root 2025-10-27 23:15:14 -04:00
3058abab23 fix(menu): check for timeout duration overflow 2025-10-27 23:10:05 -04:00
5df717de6d chore(filesystem-device-match): extract partition uuid fetch to function 2025-10-27 23:05:57 -04:00
011e133455 chore(autoconfigure-linux): clarify variable shadowing for initramfs matching 2025-10-27 23:00:55 -04:00
ccd1a8f498 chore(menu): clarify that we do not need to free the key event 2025-10-27 22:59:00 -04:00
527ce4b1b4 sprout: version 0.0.13 2025-10-27 22:44:21 -04:00
ebd3c07bf5 fix(autoconfigure): reinject values after configuration changes 2025-10-27 22:43:37 -04:00
e8b7b967fa chore(docs): change windows setup guide to use autoconfiguration 2025-10-27 21:36:48 -04:00
2bf4013938 feat(autoconfigure): improved linux support and windows support 2025-10-27 19:47:21 -04:00
6819e55e23 Merge pull request #19 from edera-dev/dependabot/docker/docker-updates-d0b0844295
chore(deps): bump rustlang/rust from `141e9a7` to `7cba2ed` in the docker-updates group
2025-10-27 19:03:00 -04:00
50f7bc11aa sprout: version 0.0.12 2025-10-27 18:41:32 -04:00
2200ba74f6 fix(cargo): force dev profiles to use opt-level = 2 to workaround hardware acceleration 2025-10-27 18:35:18 -04:00
7a3db08e1d fix(cargo): remove transitive dependency on tokio 2025-10-27 18:26:53 -04:00
e7f5be30dd feat(autoconfigure): generate names using a unique hash 2025-10-27 18:21:28 -04:00
3bbe6561ef fix(docs): fedora setup guide should use [options] 2025-10-27 17:57:29 -04:00
3b5e110d1e feat(config): rename [defaults] to [options] 2025-10-27 17:56:38 -04:00
26315fb4c4 fix(options): stamp initrd and combine options safely by ignoring empty strings 2025-10-27 17:44:30 -04:00
a76f9770dc fix(chainload): support an empty initrd path, which will result in no initrd 2025-10-27 16:27:39 -04:00
59edd63a12 fix(doc): list generator is not the matrix generator 2025-10-27 16:17:33 -04:00
8a2e8c8127 fix(sprout): correct rustdoc and clarify safety in some places 2025-10-27 16:16:09 -04:00
6086778dc0 fix(menu): free timer event to avoid leak 2025-10-27 16:03:25 -04:00
e729d6a60b feat(sprout): cleanup default logging 2025-10-27 15:44:29 -04:00
d6e8fe0245 feat(autoconfigure): find vmlinuz and initramfs pairs with linux autoconfigure module 2025-10-27 15:41:29 -04:00
99653b5192 fix(menu): hide the entry name from the menu since it can be long with autoconfigure 2025-10-27 12:25:22 -04:00
dependabot[bot]
3ffda86544 chore(deps): bump rustlang/rust in the docker-updates group
Bumps the docker-updates group with 1 update: rustlang/rust.


Updates `rustlang/rust` from `141e9a7` to `7cba2ed`

---
updated-dependencies:
- dependency-name: rustlang/rust
  dependency-version: nightly-alpine
  dependency-type: direct:production
  dependency-group: docker-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 12:48:29 +00:00
2a76e4f798 chore(code): move bls autoconfigure to separate module 2025-10-27 04:28:25 -04:00
31 changed files with 868 additions and 251 deletions

115
Cargo.lock generated
View File

@@ -14,6 +14,17 @@ version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@@ -32,6 +43,15 @@ version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.24.0" version = "1.24.0"
@@ -44,12 +64,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -59,14 +94,35 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.11" version = "0.0.14"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",
"log", "log",
"serde", "serde",
"sha256",
"toml", "toml",
"uefi", "uefi",
"uefi-raw", "uefi-raw",
@@ -97,12 +153,28 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "generic-array"
version = "0.14.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
dependencies = [
"typenum",
"version_check",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.0" version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "image" name = "image"
version = "0.25.8" version = "0.25.8"
@@ -126,6 +198,12 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.28" version = "0.4.28"
@@ -259,6 +337,29 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "sha2"
version = "0.10.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha256"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f880fc8562bdeb709793f00eb42a2ad0e672c4f883bbe59122b926eca935c8f6"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -314,6 +415,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
[[package]]
name = "typenum"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "ucs2" name = "ucs2"
version = "0.3.3" version = "0.3.3"
@@ -372,6 +479,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06" checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.13" version = "0.7.13"

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.11" version = "0.0.14"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"
@@ -13,7 +13,7 @@ toml = "0.9.8"
log = "0.4.28" log = "0.4.28"
[dependencies.image] [dependencies.image]
version = "0.25.6" version = "0.25.8"
default-features = false default-features = false
features = ["png"] features = ["png"]
optional = true optional = true
@@ -22,6 +22,10 @@ optional = true
version = "1.0.228" version = "1.0.228"
features = ["derive"] features = ["derive"]
[dependencies.sha256]
version = "1.6.0"
default-features = false
[dependencies.uefi] [dependencies.uefi]
version = "0.36.0" version = "0.36.0"
features = ["alloc", "logger"] features = ["alloc", "logger"]
@@ -33,6 +37,11 @@ version = "0.12.0"
default = ["splash"] default = ["splash"]
splash = ["dep:image"] splash = ["dep:image"]
[profile.dev]
# We have to compile for opt-level = 2 due to optimization passes
# which don't handle the UEFI target properly.
opt-level = 2
[profile.release] [profile.release]
lto = "thin" lto = "thin"
strip = "symbols" strip = "symbols"
@@ -46,6 +55,7 @@ debug = 1
inherits = "dev" inherits = "dev"
strip = "debuginfo" strip = "debuginfo"
debug = 0 debug = 0
opt-level = 2
[patch.crates-io.simd-adler32] [patch.crates-io.simd-adler32]
git = "https://github.com/edera-dev/sprout-patched-deps.git" git = "https://github.com/edera-dev/sprout-patched-deps.git"

View File

@@ -2,7 +2,7 @@
ARG RUST_PROFILE=release ARG RUST_PROFILE=release
ARG RUST_TARGET_SUBDIR=release ARG RUST_TARGET_SUBDIR=release
FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:141e9a7f13f77237dd4d462364c3a1b21cb8a6791d8924c409573e77b788af5e AS build FROM --platform=$BUILDPLATFORM rustlang/rust:nightly-alpine@sha256:7cba2edabb6ba0e92cd806cd1e0acae99d50f63e5b9c9ad842766d13c896d68c AS build
RUN apk --no-cache add musl-dev busybox-static RUN apk --no-cache add musl-dev busybox-static
ARG RUST_PROFILE ARG RUST_PROFILE
RUN adduser -S -s /bin/sh build RUN adduser -S -s /bin/sh build

View File

@@ -18,6 +18,9 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions. Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
**IMPORTANT WARNING**: Sprout does not support UEFI Secure Boot yet.
See [this issue](https://github.com/edera-dev/sprout/issues/20) for updates.
## Background ## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control. At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
@@ -55,7 +58,7 @@ The boot menu mechanism is very rudimentary.
### Current ### Current
- [x] Loadable driver support - [x] Loadable driver support
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support - [x] Basic [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support - [x] Chainload support
- [x] Linux boot support via EFI stub - [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload - [x] Windows boot support via chainload
@@ -65,15 +68,18 @@ The boot menu mechanism is very rudimentary.
### Roadmap ### Roadmap
- [ ] Full-featured boot menu - [ ] [Bootloader interface support](https://github.com/edera-dev/sprout/issues/21)
- [ ] Secure Boot support: work in progress - [ ] [BLS specification conformance](https://github.com/edera-dev/sprout/issues/2)
- [ ] UKI support: partial - [ ] [Full-featured boot menu](https://github.com/edera-dev/sprout/issues/1)
- [ ] multiboot2 support - [ ] [Secure Boot support](https://github.com/edera-dev/sprout/issues/20): work in progress
- [ ] Linux boot protocol (boot without EFI stub) - [ ] [UKI support](https://github.com/edera-dev/sprout/issues/6): partial
- [ ] [multiboot2 support](https://github.com/edera-dev/sprout/issues/7)
- [ ] [Linux boot protocol (boot without EFI stub)](https://github.com/edera-dev/sprout/issues/7)
## Concepts ## Concepts
- drivers: loadable EFI modules that can add functionality to the EFI system. - drivers: loadable EFI modules that can add functionality to the EFI system.
- autoconfiguration: code that can automatically generate sprout.toml based on the EFI environment.
- actions: executable code with a configuration that can be run by various other sprout concepts. - actions: executable code with a configuration that can be run by various other sprout concepts.
- generators: code that can generate boot entries based on inputs or runtime code. - generators: code that can generate boot entries based on inputs or runtime code.
- extractors: code that can extract values from the EFI environment. - extractors: code that can extract values from the EFI environment.
@@ -138,7 +144,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# global options. # global options.
[defaults] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them. # filesystems and generating boot entries for them.
autoconfigure = true autoconfigure = true

View File

@@ -40,7 +40,7 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi" path = "\\sprout\\drivers\\ext4.efi"
# global options. # global options.
[defaults] [options]
# enable autoconfiguration by detecting bls enabled # enable autoconfiguration by detecting bls enabled
# filesystems and generating boot entries for them. # filesystems and generating boot entries for them.
autoconfigure = true autoconfigure = true

View File

@@ -33,15 +33,10 @@ Write the following file to `X:\sprout.toml`:
# sprout configuration: version 1 # sprout configuration: version 1
version = 1 version = 1
# add a boot entry for booting Windows # global options.
# which will run the boot-windows action. [options]
[entries.windows] # enable autoconfiguration to detect Windows.
title = "Windows" autoconfigure = true
actions = ["boot-windows"]
# use the chainload action to boot the Windows bootloader.
[actions.boot-windows]
chainload.path = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi"
``` ```
## Step 4: Configure EFI Firmware to boot Sprout ## Step 4: Configure EFI Firmware to boot Sprout

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "kernel" default-entry = "kernel"
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi" has-item = "\\EFI\\BOOT\\kernel.efi"

View File

@@ -1,4 +1,4 @@
version = 1 version = 1
[defaults] [options]
autoconfigure = true autoconfigure = true

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "edera" default-entry = "edera"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "kernel" default-entry = "kernel"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "shell" default-entry = "shell"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -1,7 +1,7 @@
version = 1 version = 1
[defaults] [options]
entry = "xen" default-entry = "xen"
menu-timeout = 0 menu-timeout = 0
[extractors.boot.filesystem-device-match] [extractors.boot.filesystem-device-match]

View File

@@ -3,7 +3,7 @@ use crate::utils;
use crate::utils::media_loader::MediaLoaderHandle; use crate::utils::media_loader::MediaLoaderHandle;
use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use crate::utils::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::{error, info}; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
use uefi::CString16; use uefi::CString16;
@@ -53,18 +53,15 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image) let mut loaded_image_protocol = uefi::boot::open_protocol_exclusive::<LoadedImage>(image)
.context("unable to open loaded image protocol")?; .context("unable to open loaded image protocol")?;
// Stamp and concatenate the options to pass to the image. // Stamp and combine the options to pass to the image.
let options = configuration let options =
.options utils::combine_options(configuration.options.iter().map(|item| context.stamp(item)));
.iter()
.map(|item| context.stamp(item))
.collect::<Vec<_>>()
.join(" ");
// Pass the options to the image, if any are provided. // Pass the options to the image, if any are provided.
// The holder must drop at the end of this function to ensure the options are not leaked, // The holder must drop at the end of this function to ensure the options are not leaked,
// and the holder here ensures it outlives the if block here, as a pointer has to be // and the holder here ensures it outlives the if block here, as a pointer has to be
// passed to the image. This has been hand-validated to be safe. // passed to the image.
// SAFETY: The options outlive the usage of the image, and the image is not used after this.
let mut options_holder: Option<Box<CString16>> = None; let mut options_holder: Option<Box<CString16>> = None;
if !options.is_empty() { if !options.is_empty() {
let options = Box::new( let options = Box::new(
@@ -72,8 +69,6 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.context("unable to convert chainloader options to CString16")?, .context("unable to convert chainloader options to CString16")?,
); );
info!("options: {}", options);
if options.num_bytes() > u32::MAX as usize { if options.num_bytes() > u32::MAX as usize {
bail!("chainloader options too large"); bail!("chainloader options too large");
} }
@@ -88,10 +83,18 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
options_holder = Some(options); options_holder = Some(options);
} }
// 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 = utils::empty_is_none(initrd);
// If an initrd is provided, register it with the EFI stack.
let mut initrd_handle = None; let mut initrd_handle = None;
if let Some(ref linux_initrd) = configuration.linux_initrd { if let Some(linux_initrd) = initrd {
let initrd_path = context.stamp(linux_initrd); let content = utils::read_file_contents(context.root().loaded_image_path()?, &linux_initrd)
let content = utils::read_file_contents(context.root().loaded_image_path()?, &initrd_path)
.context("unable to read linux initrd")?; .context("unable to read linux initrd")?;
let handle = let handle =
MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice())
@@ -103,7 +106,7 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
// This call might return, or it may pass full control to another image that will never return. // This call might return, or it may pass full control to another image that will never return.
// Capture the result to ensure we can return an error if the image fails to start, but only // Capture the result to ensure we can return an error if the image fails to start, but only
// after the optional initrd has been unregistered. // after the optional initrd has been unregistered.
let result = uefi::boot::start_image(image).context("unable to start image"); let result = uefi::boot::start_image(image);
// Unregister the initrd if it was registered. // Unregister the initrd if it was registered.
if let Some(initrd_handle) = initrd_handle if let Some(initrd_handle) = initrd_handle

View File

@@ -40,7 +40,23 @@ pub struct EderaConfiguration {
} }
/// Builds a configuration string for the Xen EFI stub using the specified `configuration`. /// Builds a configuration string for the Xen EFI stub using the specified `configuration`.
fn build_xen_config(configuration: &EderaConfiguration) -> String { fn build_xen_config(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> String {
// Stamp xen options and combine them.
let xen_options = utils::combine_options(
configuration
.xen_options
.iter()
.map(|item| context.stamp(item)),
);
// Stamp kernel options and combine them.
let kernel_options = utils::combine_options(
configuration
.kernel_options
.iter()
.map(|item| context.stamp(item)),
);
// xen config file format is ini-like // xen config file format is ini-like
[ [
// global section // global section
@@ -50,10 +66,10 @@ fn build_xen_config(configuration: &EderaConfiguration) -> String {
// configuration section for sprout // configuration section for sprout
"[sprout]".to_string(), "[sprout]".to_string(),
// xen options // xen options
format!("options={}", configuration.xen_options.join(" ")), format!("options={}", xen_options),
// kernel options, stub replaces the kernel path // kernel options, stub replaces the kernel path
// the kernel is provided via media loader // the kernel is provided via media loader
format!("kernel=stub {}", configuration.kernel_options.join(" ")), format!("kernel=stub {}", kernel_options),
// required or else the last line will be ignored // required or else the last line will be ignored
"".to_string(), "".to_string(),
] ]
@@ -94,7 +110,7 @@ fn register_media_loader_file(
/// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. /// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality.
pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> { pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) -> Result<()> {
// Build the Xen config file content for this configuration. // Build the Xen config file content for this configuration.
let config = build_xen_config(configuration); let config = build_xen_config(context.clone(), configuration);
// Register the media loader for the config. // Register the media loader for the config.
let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config) let config = register_media_loader_text(XEN_EFI_CONFIG_MEDIA_GUID, "config", config)
@@ -113,7 +129,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
let mut media_loaders = vec![config, kernel]; let mut media_loaders = vec![config, kernel];
// Register the initrd if it is provided. // Register the initrd if it is provided.
if let Some(ref initrd) = configuration.initrd { if let Some(initrd) = utils::empty_is_none(configuration.initrd.as_ref()) {
let initrd = let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd) register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?; .context("unable to register initrd media loader")?;
@@ -131,7 +147,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
) )
.context("unable to chainload to xen"); .context("unable to chainload to xen");
// Unregister the media loaders on error. // Unregister the media loaders when an error happens.
for media_loader in media_loaders { for media_loader in media_loaders {
if let Err(error) = media_loader.unregister() { if let Err(error) = media_loader.unregister() {
error!("unable to unregister media loader: {}", error); error!("unable to unregister media loader: {}", error);

View File

@@ -1,101 +1,19 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration; use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use uefi::cstr16; use uefi::fs::FileSystem;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath; use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
/// The name prefix of the BLS chainload action that will be used /// bls: autodetect and configure BLS-enabled filesystems.
/// by the BLS generator to chainload entries. pub mod bls;
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations. /// linux: autodetect and configure Linux kernels.
fn scan_for_bls( /// This autoconfiguration module should not be activated
filesystem: &mut FileSystem, /// on BLS-enabled filesystems as it may make duplicate entries.
root: &DevicePath, pub mod linux;
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration. /// windows: autodetect and configure Windows boot configurations.
let mut root = root pub mod windows;
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing slash to the root to ensure the path is valid.
root.push('/');
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}
/// Generate a [RootConfiguration] based on the environment. /// Generate a [RootConfiguration] based on the environment.
/// Intakes a `config` to use as the basis of the autoconfiguration. /// Intakes a `config` to use as the basis of the autoconfiguration.
@@ -121,8 +39,18 @@ pub fn autoconfigure(config: &mut RootConfiguration) -> Result<()> {
let mut filesystem = FileSystem::new(filesystem); let mut filesystem = FileSystem::new(filesystem);
// Scan the filesystem for BLS supported configurations. // Scan the filesystem for BLS supported configurations.
// If we find any, we will add a BLS generator to the configuration. let bls_found = bls::scan(&mut filesystem, &root, config)
scan_for_bls(&mut filesystem, &root, config).context("unable to scan filesystem")?; .context("unable to scan for bls configurations")?;
// If BLS was not found, scan for Linux configurations.
if !bls_found {
linux::scan(&mut filesystem, &root, config)
.context("unable to scan for linux configurations")?;
}
// Always look for Windows configurations.
windows::scan(&mut filesystem, &root, config)
.context("unable to scan for windows configurations")?;
} }
Ok(()) Ok(())

101
src/autoconfigure/bls.rs Normal file
View File

@@ -0,0 +1,101 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::bls::BlsConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use uefi::cstr16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the BLS chainload action that will be used
/// by the BLS generator to chainload entries.
const BLS_CHAINLOAD_ACTION_PREFIX: &str = "bls-chainload-";
/// Scan the specified `filesystem` for BLS configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// BLS has a loader.conf file that can specify its own auto-entries mechanism.
let bls_loader_conf_path = Path::new(cstr16!("\\loader\\loader.conf"));
// BLS also has an entries directory that can specify explicit entries.
let bls_entries_path = Path::new(cstr16!("\\loader\\entries"));
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Whether we have a loader.conf file.
let has_loader_conf = filesystem
.try_exists(bls_loader_conf_path)
.context("unable to check for BLS loader.conf file")?;
// Whether we have an entries directory.
// We actually iterate the entries to see if there are any.
let has_entries_dir = filesystem
.read_dir(bls_entries_path)
.ok()
.and_then(|mut iterator| iterator.next())
.map(|entry| entry.is_ok())
.unwrap_or(false);
// Detect if a BLS supported configuration is on this filesystem.
// We check both loader.conf and entries directory as only one of them is required.
if !(has_loader_conf || has_entries_dir) {
return Ok(false);
}
// Generate a unique name for the BLS chainload action.
let chainload_action_name = format!("{}{}", BLS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// BLS is now detected, generate a configuration for it.
let generator = BlsConfiguration {
entry: EntryDeclaration {
title: "$title".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
path: format!("{}\\loader", root),
};
// Generate a unique name for the BLS generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-bls-{}", root_unique_hash),
GeneratorDeclaration {
bls: Some(generator),
..Default::default()
},
);
// Generate a chainload configuration for BLS.
// BLS will provide these values to us.
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a BLS supported configuration, so return true.
Ok(true)
}

219
src/autoconfigure/linux.rs Normal file
View File

@@ -0,0 +1,219 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::generators::GeneratorDeclaration;
use crate::generators::list::ListConfiguration;
use crate::utils;
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Linux chainload action that will be used to boot Linux.
const LINUX_CHAINLOAD_ACTION_PREFIX: &str = "linux-chainload-";
/// The locations to scan for kernel pairs.
/// We will check for symlinks and if this directory is a symlink, we will skip it.
const SCAN_LOCATIONS: &[&str] = &["/boot", "/"];
/// Prefixes of kernel files to scan for.
const KERNEL_PREFIXES: &[&str] = &["vmlinuz"];
/// Prefixes of initramfs files to match to.
const INITRAMFS_PREFIXES: &[&str] = &["initramfs", "initrd", "initrd.img"];
/// Pair of kernel and initramfs.
/// This is what scanning a directory is meant to find.
struct KernelPair {
/// The path to a kernel.
kernel: String,
/// The path to an initramfs, if any.
initramfs: Option<String>,
}
/// Scan the specified `filesystem` at `path` for [KernelPair] results.
fn scan_directory(filesystem: &mut FileSystem, path: &str) -> Result<Vec<KernelPair>> {
// All the discovered kernel pairs.
let mut pairs = Vec::new();
// Construct a filesystem path from the path string.
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
let path = Path::new(&path);
let path = path.to_path_buf();
// Check if the path exists and is a directory.
let exists = filesystem
.metadata(&path)
.ok()
.map(|metadata| metadata.is_directory())
.unwrap_or(false);
// If the path does not exist, return an empty list.
if !exists {
return Ok(pairs);
}
// Open a directory iterator on the path to scan.
// Ignore errors here as in some scenarios this might fail due to symlinks.
let Some(directory) = filesystem.read_dir(&path).ok() else {
return Ok(pairs);
};
// For each item in the directory, find a kernel.
for item in directory {
let item = item.context("unable to read directory item")?;
// Skip over any items that are not regular files.
if !item.is_regular_file() {
continue;
}
// Convert the name from a CString16 to a String.
let name = item.file_name().to_string();
// Find a kernel prefix that matches, if any.
let Some(prefix) = KERNEL_PREFIXES
.iter()
.find(|prefix| name == **prefix || name.starts_with(&format!("{}-", prefix)))
else {
// Skip over anything that doesn't match a kernel prefix.
continue;
};
// Acquire the suffix of the name, this will be used to match an initramfs.
let suffix = &name[prefix.len()..];
// Find a matching initramfs, if any.
let mut initramfs_prefix_iter = INITRAMFS_PREFIXES.iter();
let matched_initramfs_path = loop {
let Some(prefix) = initramfs_prefix_iter.next() else {
break None;
};
// Construct an initramfs path.
let initramfs = format!("{}{}", prefix, suffix);
let initramfs = CString16::try_from(initramfs.as_str())
.context("unable to convert initramfs name to CString16")?;
let mut initramfs_path = path.clone();
initramfs_path.push(Path::new(&initramfs));
// Check if the initramfs path exists, if it does, break out of the loop.
if filesystem
.try_exists(&initramfs_path)
.context("unable to check if initramfs path exists")?
{
break Some(initramfs_path);
}
};
// Construct a kernel path from the kernel name.
let mut kernel = path.clone();
kernel.push(Path::new(&item.file_name()));
let kernel = kernel.to_string();
let initramfs = matched_initramfs_path.map(|initramfs_path| initramfs_path.to_string());
// Produce a kernel pair.
let pair = KernelPair { kernel, initramfs };
pairs.push(pair);
}
Ok(pairs)
}
/// Scan the specified `filesystem` for Linux kernels and matching initramfs.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
let mut pairs = Vec::new();
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Scan all locations for kernel pairs, adding them to the list.
for location in SCAN_LOCATIONS {
let scanned = scan_directory(filesystem, location)
.with_context(|| format!("unable to scan directory {}", location))?;
pairs.extend(scanned);
}
// If no kernel pairs were found, return false.
if pairs.is_empty() {
return Ok(false);
}
// Generate a unique name for the linux chainload action.
let chainload_action_name = format!("{}{}", LINUX_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Kernel pairs are detected, generate a list configuration for it.
let generator = ListConfiguration {
entry: EntryDeclaration {
title: "Boot Linux $name".to_string(),
actions: vec![chainload_action_name.clone()],
..Default::default()
},
values: pairs
.into_iter()
.map(|pair| {
BTreeMap::from_iter(vec![
("name".to_string(), pair.kernel.clone()),
("kernel".to_string(), format!("{}{}", root, pair.kernel)),
(
"initrd".to_string(),
pair.initramfs
.map(|initramfs| format!("{}{}", root, initramfs))
.unwrap_or_default(),
),
])
})
.collect(),
};
// Generate a unique name for the Linux generator and insert the generator into the configuration.
config.generators.insert(
format!("autoconfigure-linux-{}", root_unique_hash),
GeneratorDeclaration {
list: Some(generator),
..Default::default()
},
);
// Insert a default value for the linux-options if it doesn't exist.
if !config.values.contains_key("linux-options") {
config
.values
.insert("linux-options".to_string(), "".to_string());
}
// Generate a chainload configuration for the list generator.
// The list will provide these values to us.
// Note that we don't need an extra \\ in the paths here.
// The root already contains a trailing slash.
let chainload = ChainloadConfiguration {
path: "$kernel".to_string(),
options: vec!["$linux-options".to_string()],
linux_initrd: Some("$initrd".to_string()),
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We had a Linux kernel, so return true to indicate something was found.
Ok(true)
}

View File

@@ -0,0 +1,80 @@
use crate::actions::ActionDeclaration;
use crate::actions::chainload::ChainloadConfiguration;
use crate::config::RootConfiguration;
use crate::entries::EntryDeclaration;
use crate::utils;
use anyhow::{Context, Result};
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::proto::device_path::DevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// The name prefix of the Windows chainload action that will be used to boot Windows.
const WINDOWS_CHAINLOAD_ACTION_PREFIX: &str = "windows-chainload-";
/// Windows boot manager path.
const BOOTMGR_FW_PATH: &str = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi";
/// Scan the specified `filesystem` for Windows configurations.
pub fn scan(
filesystem: &mut FileSystem,
root: &DevicePath,
config: &mut RootConfiguration,
) -> Result<bool> {
// Convert the boot manager firmware path to a path.
let bootmgr_fw_path =
CString16::try_from(BOOTMGR_FW_PATH).context("unable to convert path to CString16")?;
let bootmgr_fw_path = Path::new(&bootmgr_fw_path);
// Check if the boot manager firmware path exists, if it doesn't, return false.
if !filesystem
.try_exists(bootmgr_fw_path)
.context("unable to check if bootmgr firmware path exists")?
{
return Ok(false);
}
// Convert the device path root to a string we can use in the configuration.
let mut root = root
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert device root to string")?
.to_string();
// Add a trailing forward-slash to the root to ensure the device root is completed.
root.push('/');
// Generate a unique hash of the root path.
let root_unique_hash = utils::unique_hash(&root);
// Generate a unique name for the Windows chainload action.
let chainload_action_name = format!("{}{}", WINDOWS_CHAINLOAD_ACTION_PREFIX, root_unique_hash,);
// Generate an entry name for Windows.
let entry_name = format!("autoconfigure-windows-{}", root_unique_hash,);
// Create an entry for Windows and insert it into the configuration.
let entry = EntryDeclaration {
title: "Boot Windows".to_string(),
actions: vec![chainload_action_name.clone()],
values: Default::default(),
};
config.entries.insert(entry_name, entry);
// Generate a chainload configuration for Windows.
let chainload = ChainloadConfiguration {
path: format!("{}{}", root, bootmgr_fw_path),
options: vec![],
..Default::default()
};
// Insert the chainload action into the configuration.
config.actions.insert(
chainload_action_name,
ActionDeclaration {
chainload: Some(chainload),
..Default::default()
},
);
// We have a Windows boot entry, so return true to indicate something was found.
Ok(true)
}

View File

@@ -27,7 +27,7 @@ pub struct RootConfiguration {
pub version: u32, pub version: u32,
/// Default options for Sprout. /// Default options for Sprout.
#[serde(default)] #[serde(default)]
pub defaults: DefaultsConfiguration, pub options: OptionsConfiguration,
/// Values to be inserted into the root sprout context. /// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
@@ -65,16 +65,18 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration, pub phases: PhasesConfiguration,
} }
/// Default configuration for Sprout, used when the corresponding options are not specified. /// Options configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct DefaultsConfiguration { pub struct OptionsConfiguration {
/// The entry to boot without showing the boot menu. /// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown. /// If not specified, a boot menu is shown.
pub entry: Option<String>, #[serde(rename = "default-entry", default)]
pub default_entry: Option<String>,
/// The timeout of the boot menu. /// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")] #[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64, pub menu_timeout: u64,
/// Enables autoconfiguration of Sprout based on the environment. /// Enables autoconfiguration of Sprout based on the environment.
#[serde(default)]
pub autoconfigure: bool, pub autoconfigure: bool,
} }

View File

@@ -118,7 +118,10 @@ impl SproutContext {
pub fn all_values(&self) -> BTreeMap<String, String> { pub fn all_values(&self) -> BTreeMap<String, String> {
let mut values = BTreeMap::new(); let mut values = BTreeMap::new();
for key in self.all_keys() { for key in self.all_keys() {
values.insert(key.clone(), self.get(key).cloned().unwrap_or_default()); // Acquire the value from the context. Since retrieving all the keys will give us
// a full view of the context, we can be sure that the key exists.
let value = self.get(&key).cloned().unwrap_or_default();
values.insert(key.clone(), value);
} }
values values
} }
@@ -165,13 +168,13 @@ impl SproutContext {
let mut current_values = self.all_values(); let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check // To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail. // the number of iterations. If it exceeds CONTEXT_FINALIZE_ITERATION_LIMIT, we bail.
let mut iterations: usize = 0; let mut iterations: usize = 0;
loop { loop {
iterations += 1; iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT { if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization"); bail!("maximum number of replacement iterations reached while finalizing context");
} }
let mut did_change = false; let mut did_change = false;

View File

@@ -33,8 +33,6 @@ fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result
// Push the path of the driver from the root. // Push the path of the driver from the root.
full_path.push_str(&context.stamp(&driver.path)); full_path.push_str(&context.stamp(&driver.path));
info!("driver path: {}", full_path);
// Convert the path to a device path. // Convert the path to a device path.
let device_path = utils::text_to_device_path(&full_path)?; let device_path = utils::text_to_device_path(&full_path)?;

View File

@@ -10,16 +10,17 @@ use uefi::proto::device_path::DevicePath;
use uefi::proto::media::file::{File, FileSystemVolumeLabel}; use uefi::proto::media::file::{File, FileSystemVolumeLabel};
use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::fs::SimpleFileSystem;
use uefi::proto::media::partition::PartitionInfo; use uefi::proto::media::partition::PartitionInfo;
use uefi::{CString16, Guid}; use uefi::{CString16, Guid, Handle};
use uefi_raw::Status; use uefi_raw::Status;
/// The filesystem device match extractor. /// The filesystem device match extractor.
/// This extractor finds a filesystem using some search criteria and returns /// This extractor finds a filesystem using some search criteria and returns
/// the device root path that can concatenated with subpaths to access files /// the device root path that can concatenated with subpaths to access files
/// on a particular filesystem. /// on a particular filesystem.
/// The fallback value can be used to provide a value if no match is found.
/// ///
/// This function only requires one of the criteria to match. /// This extractor requires all the criteria to match. If no criteria is provided,
/// The fallback value can be used to provide a value if none is found. /// an error is returned.
#[derive(Serialize, Deserialize, Debug, Default, Clone)] #[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct FilesystemDeviceMatchExtractor { pub struct FilesystemDeviceMatchExtractor {
/// Matches a filesystem that has the specified label. /// Matches a filesystem that has the specified label.
@@ -40,6 +41,48 @@ pub struct FilesystemDeviceMatchExtractor {
pub fallback: Option<String>, pub fallback: Option<String>,
} }
/// Represents the partition UUIDs for a filesystem.
struct PartitionIds {
/// The UUID of the partition.
partition_uuid: Guid,
/// The type UUID of the partition.
type_uuid: Guid,
}
/// Fetches the partition UUIDs for the specified filesystem handle.
fn fetch_partition_uuids(handle: Handle) -> Result<Option<PartitionIds>> {
// Open the partition info protocol for this handle.
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle);
match partition_info {
Ok(partition_info) => {
// GPT partitions have a unique partition GUID.
// MBR does not.
if let Some(gpt) = partition_info.gpt_partition_entry() {
let uuid = gpt.unique_partition_guid;
let type_uuid = gpt.partition_type_guid;
Ok(Some(PartitionIds {
partition_uuid: uuid,
type_uuid: type_uuid.0,
}))
} else {
Ok(None)
}
}
Err(error) => {
// If the filesystem does not have a partition, that is okay.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
Ok(None)
} else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?;
unreachable!()
}
}
}
}
/// Extract a filesystem device path using the specified `context` and `extractor` configuration. /// Extract a filesystem device path using the specified `context` and `extractor` configuration.
pub fn extract( pub fn extract(
context: Rc<SproutContext>, context: Rc<SproutContext>,
@@ -56,56 +99,28 @@ pub fn extract(
// Extract the partition info for this filesystem. // Extract the partition info for this filesystem.
// There is no guarantee that the filesystem has a partition. // There is no guarantee that the filesystem has a partition.
let partition_info = { let partition_info =
// Open the partition info protocol for this handle. fetch_partition_uuids(handle).context("unable to fetch partition info")?;
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle);
match partition_info {
Ok(partition_info) => {
// GPT partitions have a unique partition GUID.
// MBR does not.
if let Some(gpt) = partition_info.gpt_partition_entry() {
let uuid = gpt.unique_partition_guid;
let type_uuid = gpt.partition_type_guid;
Some((uuid, type_uuid.0))
} else {
None
}
}
Err(error) => {
// If the filesystem does not have a partition, that is okay.
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED
{
None
} else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?;
unreachable!()
}
}
}
};
// Check if the partition info matches partition uuid criteria. // Check if the partition info matches partition uuid criteria.
if let Some((partition_uuid, _partition_type_guid)) = partition_info if let Some(ref partition_info) = partition_info
&& let Some(ref has_partition_uuid) = extractor.has_partition_uuid && let Some(ref has_partition_uuid) = extractor.has_partition_uuid
{ {
let parsed_uuid = Guid::from_str(has_partition_uuid) let parsed_uuid = Guid::from_str(has_partition_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?; .map_err(|e| anyhow!("unable to parse has-partition-uuid: {}", e))?;
if partition_uuid != parsed_uuid { if partition_info.partition_uuid != parsed_uuid {
continue; continue;
} }
has_match = true; has_match = true;
} }
// Check if the partition info matches partition type uuid criteria. // Check if the partition info matches partition type uuid criteria.
if let Some((_partition_uuid, partition_type_guid)) = partition_info if let Some(ref partition_info) = partition_info
&& let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid && let Some(ref has_partition_type_uuid) = extractor.has_partition_type_uuid
{ {
let parsed_uuid = Guid::from_str(has_partition_type_uuid) let parsed_uuid = Guid::from_str(has_partition_type_uuid)
.map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?; .map_err(|e| anyhow!("unable to parse has-partition-type-uuid: {}", e))?;
if partition_type_guid != parsed_uuid { if partition_info.type_uuid != parsed_uuid {
continue; continue;
} }
has_match = true; has_match = true;

View File

@@ -1,6 +1,7 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::BootableEntry; use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration; use crate::generators::bls::BlsConfiguration;
use crate::generators::list::ListConfiguration;
use crate::generators::matrix::MatrixConfiguration; use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result; use anyhow::Result;
use anyhow::bail; use anyhow::bail;
@@ -8,6 +9,7 @@ use serde::{Deserialize, Serialize};
use std::rc::Rc; use std::rc::Rc;
pub mod bls; pub mod bls;
pub mod list;
pub mod matrix; pub mod matrix;
/// Declares a generator configuration. /// Declares a generator configuration.
@@ -32,6 +34,9 @@ pub struct GeneratorDeclaration {
/// It will generate a sprout entry for every supported BLS entry. /// It will generate a sprout entry for every supported BLS entry.
#[serde(default)] #[serde(default)]
pub bls: Option<BlsConfiguration>, pub bls: Option<BlsConfiguration>,
/// List generator configuration.
/// Allows you to specify a list of values to generate an entry from.
pub list: Option<ListConfiguration>,
} }
/// Runs the generator specified by the `generator` option. /// Runs the generator specified by the `generator` option.
@@ -45,6 +50,8 @@ pub fn generate(
matrix::generate(context, matrix) matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls { } else if let Some(bls) = &generator.bls {
bls::generate(context, bls) bls::generate(context, bls)
} else if let Some(list) = &generator.list {
list::generate(context, list)
} else { } else {
bail!("unknown generator configuration"); bail!("unknown generator configuration");
} }

View File

@@ -41,7 +41,8 @@ impl FromStr for BlsEntry {
continue; continue;
} }
// Split the line once by whitespace. // Split the line once by whitespace. This technically includes newlines but since
// the lines iterator is used, there should never be a newline here.
let Some((key, value)) = line.split_once(char::is_whitespace) else { let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue; continue;
}; };

52
src/generators/list.rs Normal file
View File

@@ -0,0 +1,52 @@
use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// List generator configuration.
/// The list generator produces multiple entries based
/// on a set of input maps.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ListConfiguration {
/// 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: Vec<BTreeMap<String, String>>,
}
/// Generates a set of entries using the specified `matrix` configuration in the `context`.
pub fn generate(
context: Rc<SproutContext>,
list: &ListConfiguration,
) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// For each combination, create a new context and entry.
for (index, combination) in list.values.iter().enumerate() {
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 = list.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
}

View File

@@ -1,5 +1,6 @@
use crate::context::SproutContext; use crate::context::SproutContext;
use crate::entries::{BootableEntry, EntryDeclaration}; use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::list;
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
@@ -57,30 +58,12 @@ pub fn generate(
) -> Result<Vec<BootableEntry>> { ) -> Result<Vec<BootableEntry>> {
// Produce all the combinations of the input values. // Produce all the combinations of the input values.
let combinations = build_matrix(&matrix.values); let combinations = build_matrix(&matrix.values);
let mut entries = Vec::new(); // Use the list generator to generate entries for each combination.
list::generate(
// For each combination, create a new context and entry. context,
for (index, combination) in combinations.into_iter().enumerate() { &list::ListConfiguration {
let mut context = context.fork(); entry: matrix.entry.clone(),
// Insert the combination into the context. values: combinations,
context.insert(&combination); },
let context = context.freeze(); )
// Stamp the entry title and actions from the template.
let mut entry = matrix.entry.clone();
entry.actions = entry
.actions
.into_iter()
.map(|action| context.stamp(action))
.collect();
// Push the entry into the list with the new context.
entries.push(BootableEntry::new(
index.to_string(),
entry.title.clone(),
context,
entry,
));
}
Ok(entries)
} }

View File

@@ -8,12 +8,11 @@ use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable; use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info; use log::{error, info};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::ops::Deref; use std::ops::Deref;
use std::time::Duration; use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath; use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
/// actions: Code that can be configured and executed by Sprout. /// actions: Code that can be configured and executed by Sprout.
pub mod actions; pub mod actions;
@@ -54,13 +53,8 @@ pub mod options;
/// utils: Utility functions that are used by other parts of Sprout. /// utils: Utility functions that are used by other parts of Sprout.
pub mod utils; pub mod utils;
/// The main entrypoint of sprout. /// Run Sprout, returning an error if one occurs.
/// It is possible this function will not return if actions that are executed fn run() -> Result<()> {
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Parse the options to the sprout executable. // Parse the options to the sprout executable.
let options = SproutOptions::parse().context("unable to parse options")?; let options = SproutOptions::parse().context("unable to parse options")?;
@@ -83,10 +77,6 @@ fn main() -> Result<()> {
>(uefi::boot::image_handle()) >(uefi::boot::image_handle())
.context("unable to get loaded image device path")?; .context("unable to get loaded image device path")?;
let loaded_image_path = current_image_device_path_protocol.deref().to_boxed(); let loaded_image_path = current_image_device_path_protocol.deref().to_boxed();
info!(
"loaded image path: {}",
loaded_image_path.to_string(DisplayOnly(false), AllowShortcuts(false))?
);
RootContext::new(loaded_image_path, options) RootContext::new(loaded_image_path, options)
}; };
@@ -110,7 +100,7 @@ fn main() -> Result<()> {
// If --autoconfigure is specified or the loaded configuration has autoconfigure enabled, // If --autoconfigure is specified or the loaded configuration has autoconfigure enabled,
// trigger the autoconfiguration mechanism. // trigger the autoconfiguration mechanism.
if context.root().options().autoconfigure || config.defaults.autoconfigure { if context.root().options().autoconfigure || config.options.autoconfigure {
autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?; autoconfigure::autoconfigure(&mut config).context("unable to autoconfigure")?;
} }
@@ -128,6 +118,9 @@ fn main() -> Result<()> {
// Extend the root context with the autoconfigured actions. // Extend the root context with the autoconfigured actions.
root.actions_mut().extend(config.actions); root.actions_mut().extend(config.actions);
// Insert any modified root values.
context.insert(&config.values);
} }
// Refreeze the context to ensure that further operations can share the context. // Refreeze the context to ensure that further operations can share the context.
@@ -192,7 +185,7 @@ fn main() -> Result<()> {
entry.restamp_title(); entry.restamp_title();
// Mark this entry as the default entry if it is declared as such. // Mark this entry as the default entry if it is declared as such.
if let Some(ref default_entry) = config.defaults.entry { if let Some(ref default_entry) = config.options.default_entry {
// If the entry matches the default entry, mark it as the default entry. // If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) { if entry.is_match(default_entry) {
entry.mark_default(); entry.mark_default();
@@ -221,7 +214,7 @@ fn main() -> Result<()> {
.root() .root()
.options() .options()
.menu_timeout .menu_timeout
.unwrap_or(config.defaults.menu_timeout); .unwrap_or(config.options.menu_timeout);
let menu_timeout = Duration::from_secs(menu_timeout); let menu_timeout = Duration::from_secs(menu_timeout);
// Use the forced boot entry if possible, otherwise pick the first entry using a boot menu. // Use the forced boot entry if possible, otherwise pick the first entry using a boot menu.
@@ -240,6 +233,28 @@ fn main() -> Result<()> {
.context(format!("unable to execute action '{}'", action))?; .context(format!("unable to execute action '{}'", action))?;
} }
Ok(())
}
/// The main entrypoint of sprout.
/// It is possible this function will not return if actions that are executed
/// exit boot services or do not return control to sprout.
fn main() -> Result<()> {
// Initialize the basic UEFI environment.
setup::init()?;
// Run Sprout, then handle the error.
let result = run();
if let Err(ref error) = result {
// Print an error trace.
error!("sprout encountered an error");
for (index, stack) in error.chain().enumerate() {
error!("[{}]: {}", index, stack);
}
// Sleep for 10 seconds to allow the user to read the error.
uefi::boot::stall(Duration::from_secs(10));
}
// Sprout doesn't necessarily guarantee anything was booted. // Sprout doesn't necessarily guarantee anything was booted.
// If we reach here, we will exit back to whoever called us. // If we reach here, we will exit back to whoever called us.
Ok(()) Ok(())

View File

@@ -40,15 +40,30 @@ fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None) uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")? .context("unable to create timer event")?
}; };
// The timeout is in increments of 100 nanoseconds. // The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100); let timeout_hundred_nanos = timeout.as_nanos() / 100;
// Check if the timeout is too large to fit into an u64.
if timeout_hundred_nanos > u64::MAX as u128 {
bail!("timeout duration overflow");
}
// Set a timer to trigger after the specified duration.
let trigger = TimerTrigger::Relative(timeout_hundred_nanos as u64);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?; uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = [timer_event, key_event]; let mut events = vec![timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events) let event = uefi::boot::wait_for_event(&mut events)
.discard_errdata() .discard_errdata()
.context("unable to wait for event")?; .context("unable to wait for event")?;
// Close the timer event that we acquired.
// We don't need to close the key event because it is owned globally.
if let Some(timer_event) = events.into_iter().next() {
uefi::boot::close_event(timer_event).context("unable to close timer event")?;
}
// The first event is the timer event. // The first event is the timer event.
// If it has triggered, the user did not select a numbered entry. // If it has triggered, the user did not select a numbered entry.
if event == 0 { if event == 0 {
@@ -121,7 +136,7 @@ fn select_with_input<'a>(
// Entry was selected by number. If the number is invalid, we continue. // Entry was selected by number. If the number is invalid, we continue.
MenuOperation::Number(index) => { MenuOperation::Number(index) => {
let Some(entry) = entries.get(index) else { let Some(entry) = entries.get(index) else {
println!("invalid entry number"); info!("invalid entry number");
continue; continue;
}; };
return Ok(entry); return Ok(entry);

View File

@@ -1,4 +1,5 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use log::info;
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior /// The type of option. This disambiguates different behavior
@@ -113,9 +114,9 @@ pub trait OptionsRepresentable {
// Handle the --help flag case. // Handle the --help flag case.
if description.form == OptionForm::Help { if description.form == OptionForm::Help {
// Generic configured options output. // Generic configured options output.
println!("Configured Options:"); info!("Configured Options:");
for (name, description) in &configured { for (name, description) in &configured {
println!( info!(
" --{}{}: {}", " --{}{}: {}",
name, name,
if description.form == OptionForm::Value { if description.form == OptionForm::Value {

View File

@@ -161,3 +161,23 @@ pub fn read_file_contents(default_root_path: &DevicePath, input: &str) -> Result
let content = fs.read(Path::new(&path)); let content = fs.read(Path::new(&path));
content.context("unable to read file contents") content.context("unable to read file contents")
} }
/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
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<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.collect::<Vec<_>>()
.join(" ")
}
/// Produce a unique hash for the input.
/// This uses SHA-256, which is unique enough but relatively short.
pub fn unique_hash(input: &str) -> String {
sha256::digest(input.as_bytes())
}

View File

@@ -51,6 +51,11 @@ impl MediaLoaderHandle {
/// The next call will pass a buffer of the right size, and we should copy /// The next call will pass a buffer of the right size, and we should copy
/// data into that buffer, checking whether it is safe to copy based on /// data into that buffer, checking whether it is safe to copy based on
/// the buffer size. /// the buffer size.
///
/// SAFETY: `this.address` and `this.length` are set by leaking a Box<[u8]>, so we can
/// be sure their pointers are valid when this is called. The caller must call this function
/// while inside UEFI boot services to ensure pointers are valid. Copying to `buffer` is
/// assumed valid because the caller must ensure `buffer` is valid by function contract.
unsafe extern "efiapi" fn load_file( unsafe extern "efiapi" fn load_file(
this: *mut MediaLoaderProtocol, this: *mut MediaLoaderProtocol,
file_path: *const DevicePathProtocol, file_path: *const DevicePathProtocol,
@@ -155,7 +160,7 @@ impl MediaLoaderHandle {
// Install a protocol interface for the device path. // Install a protocol interface for the device path.
// This ensures it can be located by other EFI programs. // This ensures it can be located by other EFI programs.
let mut handle = unsafe { let primary_handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
None, None,
&DevicePathProtocol::GUID, &DevicePathProtocol::GUID,
@@ -178,25 +183,54 @@ impl MediaLoaderHandle {
let protocol = Box::leak(protocol); let protocol = Box::leak(protocol);
// Install a protocol interface for the load file protocol for the media loader protocol. // Install a protocol interface for the load file protocol for the media loader protocol.
handle = unsafe { let secondary_handle = unsafe {
uefi::boot::install_protocol_interface( uefi::boot::install_protocol_interface(
Some(handle), Some(primary_handle),
&LoadFile2Protocol::GUID, &LoadFile2Protocol::GUID,
protocol as *mut _ as *mut c_void, // The UEFI API expects an opaque pointer here.
protocol as *mut MediaLoaderProtocol as *mut c_void,
) )
} };
.context("unable to install media loader load file handle")?;
// Check if the media loader is registered. // If installing the second protocol interface failed, we need to clean up after ourselves.
// If it is not, we can't continue safely because something went wrong. if secondary_handle.is_err() {
if !Self::already_registered(guid)? { // Uninstall the protocol interface for the device path protocol.
bail!("media loader not registered when expected to be registered"); // SAFETY: If we have reached this point, we know that the protocol is registered.
// If this fails, we have no choice but to leak memory. The error will be shown
// to the user, so at least they can see it. In most cases, catching this error
// will exit, so leaking is safe.
unsafe {
uefi::boot::uninstall_protocol_interface(
primary_handle,
&DevicePathProtocol::GUID,
path.as_ffi_ptr() as *mut c_void,
)
.context(
"unable to uninstall media loader device path handle, this will leak memory",
)?;
}
// SAFETY: We know that the protocol is leaked, so we can safely take a reference to it.
let protocol = unsafe { Box::from_raw(protocol) };
// SAFETY: We know that the data is leaked, so we can safely take a reference to it.
let data = unsafe { Box::from_raw(data) };
// SAFETY: We know that the path is leaked, so we can safely take a reference to it.
let path = unsafe { Box::from_raw(path) };
// Drop all the allocations explicitly to clarify the lifetime.
drop(protocol);
drop(data);
drop(path);
} }
// If installing the second protocol interface failed, this will return the error.
// We should have already cleaned up after ourselves, so this is safe.
secondary_handle.context("unable to install media loader load file handle")?;
// Return a handle to the media loader. // Return a handle to the media loader.
Ok(Self { Ok(Self {
guid, guid,
handle, handle: primary_handle,
protocol, protocol,
path, path,
}) })