36 Commits

Author SHA1 Message Date
7dd910a74f sprout: version 0.0.10 2025-10-27 00:53:35 -04:00
187a84fcf8 fix(bls): entries path should be constructed from the subpath, not the device path 2025-10-27 00:50:17 -04:00
e47c813536 fix(bls): swap from intaking \\loader\\entries to just \\loader
This prepares for further BLS integration.
2025-10-27 00:31:07 -04:00
094128de58 fix(log): swap everything to use logging 2025-10-27 00:21:24 -04:00
e8a4fa5053 Revert "fix(log): make all logging debug! instead of info!"
This reverts commit 717e7716ba.
2025-10-27 00:20:42 -04:00
717e7716ba fix(log): make all logging debug! instead of info! 2025-10-27 00:16:00 -04:00
1d32855d22 Merge pull request #16 from edera-dev/azenla/alpine-edge
chore(docs): alpine edge setup guide
2025-10-27 00:15:28 -04:00
93c7a35c62 Merge pull request #17 from edera-dev/azenla/basic-menu
feat(boot): basic boot menu
2025-10-27 00:04:54 -04:00
8b6317f221 chore(doc): document the basic boot menu as a supported feature 2025-10-27 00:01:05 -04:00
4bbac3e4d5 feat(boot): implement basic boot menu 2025-10-26 23:59:50 -04:00
1f48d26385 chore(docs): alpine edge setup guide 2025-10-26 22:29:30 -04:00
9d2e25183b Merge pull request #15 from edera-dev/azenla/defaults-boot
feat(config): support for setting the default entry to boot
2025-10-24 21:30:03 -07:00
734ff117db feat(config): support for setting the default entry to boot 2025-10-24 21:19:38 -07:00
fbebedd66a fix(hack): format should use bash to use glob 2025-10-24 20:09:32 -07:00
b3bf564b65 fix(hack): formatting fixes 2025-10-24 20:08:46 -07:00
340c280c00 fix(hack): check kvm with /dev/kvm instead of cpu flags 2025-10-24 20:07:41 -07:00
7a72b7af5b Merge pull request #14 from edera-dev/azenla/fixes
Repair automated bug analysis issues.
2025-10-24 20:00:49 -07:00
0c2303d789 fix(framebuffer): add proper bounds checking for accessing a pixel 2025-10-24 19:54:28 -07:00
6cd502ef18 fix(actions): if edera action returns successfully, an intended unreachable line could be reached 2025-10-24 19:51:08 -07:00
e243228f15 fix(framebuffer): check width, height and implement proper checking when accessing pixels 2025-10-24 19:44:26 -07:00
2253fa2a1f fix(context): make sure to actually iterate longest first for key replacement 2025-10-24 19:40:40 -07:00
057c48f9f7 fix(bls): parser should skip over empty lines and comments 2025-10-24 19:31:01 -07:00
45d7cd2d3b fix(doc): incorrect comment for startup phase execution 2025-10-24 19:28:38 -07:00
482db0b763 fix(media-loader): eliminate usage of unwrap and swap to result 2025-10-24 19:27:43 -07:00
a15c92a749 fix(context): ensure longer keys are replaced first, fixing key replacement edge case 2025-10-24 19:24:29 -07:00
7d5248e2ee fix(context): skip over empty keys to avoid replacing $ and breaking other values 2025-10-24 19:12:43 -07:00
41fbca6f76 fix(utils): clarify that the to_string().contains() is necessary due to CString16 2025-10-24 19:11:17 -07:00
d39fbae168 fix(splash): check for zero-sized images 2025-10-24 19:08:02 -07:00
0b0b4dc19d chore(perf): replace some string replacement and comparison with characters for performance 2025-10-24 18:59:15 -07:00
86fa00928e fix(bls): convert less safe path concatenation to use path buffer 2025-10-24 18:56:11 -07:00
4c7b1d70ef fix(bls): parsing of entries should split by whitespace, not just spaces 2025-10-24 18:51:34 -07:00
9d2c31f77f fix(options): clarify code that checks for --abc=123 option form 2025-10-24 18:49:14 -07:00
fc710ec391 fix(options): --help should exit with code zero 2025-10-24 18:47:34 -07:00
9f7ca672ea chore(filesystem-device-match): add clarity to statement which is unreachable 2025-10-24 18:46:45 -07:00
2a2aa74c09 fix(context): add context finalization iteration limit
This prevents any possibility of an infinite loop during finalization.
2025-10-24 18:44:54 -07:00
2e3399f33f chore(deps): upgrade to uefi v0.36.0 2025-10-24 18:34:18 -07:00
31 changed files with 627 additions and 137 deletions

50
Cargo.lock generated
View File

@@ -28,9 +28,9 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
[[package]]
name = "bitflags"
version = "2.9.4"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bytemuck"
@@ -46,9 +46,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cfg-if"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crc32fast"
@@ -61,7 +61,7 @@ dependencies = [
[[package]]
name = "edera-sprout"
version = "0.0.9"
version = "0.0.10"
dependencies = [
"anyhow",
"image",
@@ -89,9 +89,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.3"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04bcaeafafdd3cd1cb5d986ff32096ad1136630207c49b9091e3ae541090d938"
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -118,9 +118,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.4"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f"
dependencies = [
"equivalent",
"hashbrown",
@@ -175,9 +175,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.101"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
@@ -204,9 +204,9 @@ dependencies = [
[[package]]
name = "pxfm"
version = "0.1.24"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f9b339b02259ada5c0f4a389b7fb472f933aa17ce176fd2ad98f28bb401fde"
checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84"
dependencies = [
"num-traits",
]
@@ -266,9 +266,9 @@ source = "git+https://github.com/edera-dev/sprout-patched-deps.git?rev=2c4fcc84b
[[package]]
name = "syn"
version = "2.0.106"
version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
@@ -325,9 +325,9 @@ dependencies = [
[[package]]
name = "uefi"
version = "0.35.0"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da7569ceafb898907ff764629bac90ac24ba4203c38c33ef79ee88c74aa35b11"
checksum = "f123e69767fc287c44d70ee19af3b39d1bfb735dbaff5090e95b5b13cd656d16"
dependencies = [
"bitflags",
"cfg-if",
@@ -341,9 +341,9 @@ dependencies = [
[[package]]
name = "uefi-macros"
version = "0.18.1"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3dad47b3af8f99116c0f6d4d669c439487d9aaf1c8d9480d686cda6f3a8aa23"
checksum = "4687412b5ac74d245d5bfb1733ede50c31be19bf8a4b6a967a29b451bab49e67"
dependencies = [
"proc-macro2",
"quote",
@@ -352,9 +352,9 @@ dependencies = [
[[package]]
name = "uefi-raw"
version = "0.11.0"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cad96b8baaf1615d3fdd0f03d04a0b487d857c1b51b19dcbfe05e2e3c447b78"
checksum = "8aff2f4f2b556a36a201d335a1e0a57754967a96857b1f47a52d5a23825cac84"
dependencies = [
"bitflags",
"uguid",
@@ -362,15 +362,15 @@ dependencies = [
[[package]]
name = "uguid"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab14ea9660d240e7865ce9d54ecdbd1cd9fa5802ae6f4512f093c7907e921533"
checksum = "0c8352f8c05e47892e7eaf13b34abd76a7f4aeaf817b716e88789381927f199c"
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
checksum = "462eeb75aeb73aea900253ce739c8e18a67423fadf006037cd3ff27e82748a06"
[[package]]
name = "winnow"

View File

@@ -2,7 +2,7 @@
name = "edera-sprout"
description = "Modern UEFI bootloader"
license = "Apache-2.0"
version = "0.0.9"
version = "0.0.10"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
edition = "2024"
@@ -23,11 +23,11 @@ version = "1.0.228"
features = ["derive"]
[dependencies.uefi]
version = "0.35.0"
version = "0.36.0"
features = ["alloc", "logger"]
[dependencies.uefi-raw]
version = "0.11.0"
version = "0.12.0"
[features]
default = ["splash"]

View File

@@ -39,6 +39,7 @@ simplify installation and usage.
- [Fedora Setup Guide]
- [Generic Linux Setup Guide]
- [Alpine Edge Setup Guide]
- [Windows Setup Guide]
- [Development Guide]
- [Contributing Guide]
@@ -48,8 +49,8 @@ simplify installation and usage.
## Features
NOTE: Currently, Sprout is experimental and is not intended for production use. For example, it doesn't currently
have secure boot support. In fact, as of writing, it doesn't even have a boot menu. Instead, it boots the first entry it sees, or fails.
NOTE: Currently, Sprout is experimental and is not intended for production use.
The boot menu mechanism is very rudimentary.
### Current
@@ -59,11 +60,11 @@ have secure boot support. In fact, as of writing, it doesn't even have a boot me
- [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk
- [x] Boot first configured entry
- [x] Basic boot menu
### Roadmap
- [ ] Boot menu
- [ ] Full-featured boot menu
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] multiboot2 support
@@ -134,10 +135,10 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem
# that contains \loader\entries as a directory
# that contains \loader as a directory
# into the value called "boot"
[extractors.boot.filesystem-device-match]
has-item = "\\loader\\entries"
has-item = "\\loader"
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
@@ -145,7 +146,7 @@ has-item = "\\loader\\entries"
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
path = "$boot\\loader"
entry.title = "$title"
entry.actions = ["bls"]
@@ -159,6 +160,7 @@ chainload.linux-initrd = "$boot\\$initrd"
[Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Alpine Edge Setup Guide]: ./docs/alpine-edge-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md
[Development Guide]: ./DEVELOPMENT.md
[Contributing Guide]: ./CONTRIBUTING.md

113
docs/alpine-edge-setup.md Normal file
View File

@@ -0,0 +1,113 @@
# Setup Sprout on Alpine Edge
## Prerequisites
- Alpine Edge
- EFI System Partition mounted on `/boot/efi` (the default)
- ext4 or FAT32/exFAT formatted `/boot` partition
## Step 1: Base Installation
Download the latest sprout.efi release from the [GitHub releases page](https://github.com/edera-dev/sprout/releases).
For x86_64 systems, download the `sprout-x86_64.efi` file, and for ARM systems, download the `sprout-aarch64.efi` file.
Copy the downloaded `sprout.efi` file to `/boot/efi/EFI/boot/sprout.efi` on your EFI System Partition.
Additionally, you will want to install the `efifs` package, which provides the filesystem support for Sprout.
```bash
# Install the efifs package which provides filesystem support for Sprout.
$ apk install efifs
```
## Step 2: Configure Sprout
Since Alpine uses standard image paths based on the `linux` package installed, it's quite easy to configure Sprout
to boot Alpine.
Write the following file to `/boot/efi/sprout.toml`:
```toml
# sprout configuration: version 1
version = 1
# load an EFI driver for ext2/ext3/ext4.
[drivers.ext2]
path = "\\EFI\\efifs\\ext2.efi"
# extract the full path of the first filesystem
# that contains \boot\vmlinuz-stable as a file
# into the value called "root"
[extractors.root.filesystem-device-match]
has-item = "\\boot\\vmlinuz-stable"
# add a boot entry for booting linux
# which will run the boot-linux action.
[entries.boot-linux-stable]
title = "Boot Linux Stable"
actions = ["boot-linux-stable"]
# use the chainload action to boot linux-stable via the efi stub.
# the options below are passed to the efi stub as the
# kernel command line. the initrd is loaded using the efi stub
# initrd loader mechanism.
[actions.boot-linux-stable]
chainload.path = "$root\\boot\\vmlinuz-stable"
chainload.options = ["root=/dev/sda1", "my-kernel-option"]
chainload.linux-initrd = "$root\\boot\\initramfs-stable"
```
You can replace `vmlinuz-stable` and `initramfs-stable` with the actual
files for the `linux` package you have installed. For example, for `linux-lts` it is `vmlinuz-lts` and `initramfs-lts`.
## Step 3, Option 1: Configure GRUB to load Sprout (recommended)
You can configure GRUB to add a boot entry for Sprout, so you can continue to use GRUB without interruption.
GRUB needs to be configured with the chainloader module to load Sprout.
You will need to find the UUID of your EFI System Partition. You can do this by running the following command:
```bash
$ grep "/boot/efi" /etc/fstab | awk '{print $1}' | awk -F '=' '{print $2}'
SAMPLE-VALUE
```
The GRUB configuration for Sprout is as follows, replace `SAMPLE-VALUE` with the UUID of your EFI System Partition:
```grub
menuentry 'Sprout' $menuentry_id_option 'sprout' {
insmod part_gpt
insmod fat
insmod chain
search --no-floppy --fs-uuid --set=root SAMPLE-VALUE
chainloader /EFI/boot/sprout.efi
}
```
You can append this to `/etc/grub.d/40_custom` and run the following command to update your configuration:
```bash
$ update-grub
```
To update your GRUB configuration.
You may now reboot your system and select Sprout from the GRUB menu.
## Step 3, Option 2: Configure your EFI firmware for Sprout
You can configure your EFI boot menu to show Sprout as an option.
You will need to install the `efibootmgr` package:
```
$ apk add efibootmgr
```
Once `efibootmgr` is installed, find the partition device of your EFI System Partition and run the following:
```bash
$ efibootmgr -d /dev/esp_partition_here -C -L 'Sprout' -l '\EFI\boot\sprout.efi'
```
This will add a new entry to your EFI boot menu called `Sprout` that will boot Sprout with your configuration.
Now if you boot into your UEFI firmware, you should see Sprout as an option to boot.

View File

@@ -40,10 +40,10 @@ version = 1
path = "\\sprout\\drivers\\ext4.efi"
# extract the full path of the first filesystem
# that contains \loader\entries as a directory
# that contains \loader as a directory
# into the value called "boot"
[extractors.boot.filesystem-device-match]
has-item = "\\loader\\entries"
has-item = "\\loader"
# use the sprout bls module to scan a bls
# directory for entries and load them as boot
@@ -51,7 +51,7 @@ has-item = "\\loader\\entries"
# as specified here. the bls action below will
# be passed the extracted values from bls.
[generators.boot.bls]
path = "$boot\\loader\\entries"
path = "$boot\\loader"
entry.title = "$title"
entry.actions = ["bls"]

View File

@@ -36,9 +36,7 @@ DOCKER_TARGET="linux/${TARGET_ARCH}"
FINAL_DIR="target/final/${TARGET_ARCH}"
ASSEMBLE_DIR="target/assemble"
if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
[ -f "/proc/cpuinfo" ] &&
grep -E '^flags.*:.+ vmx .*' /proc/cpuinfo >/dev/null; then
if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] && [ -e "/dev/kvm" ]; then
QEMU_ACCEL="kvm"
fi

View File

@@ -35,13 +35,15 @@ set -- "${@}" -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then
set -- "${@}" -nographic
else
if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then
set -- "${@}" -serial stdio
else
set -- "${@}" \
-device virtio-serial-pci,id=vs0 \
-chardev stdio,id=stdio0 \
-device virtconsole,chardev=stdio0,id=console0
if [ "${GRAPHICAL_ONLY}" != "1" ]; then
if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then
set -- "${@}" -serial stdio
else
set -- "${@}" \
-device virtio-serial-pci,id=vs0 \
-chardev stdio,id=stdio0 \
-device virtconsole,chardev=stdio0,id=console0
fi
fi
if [ "${QEMU_LEGACY_VGA}" = "1" ]; then

View File

@@ -11,7 +11,7 @@ if [ "${TARGET_ARCH}" = "aarch64" ]; then
fi
if [ -z "${SPROUT_CONFIG_NAME}" ]; then
SPROUT_CONFIG_NAME="kernel"
SPROUT_CONFIG_NAME="all"
fi
echo "[build] ${TARGET_ARCH} ${RUST_PROFILE}"

View File

@@ -0,0 +1,30 @@
version = 1
[defaults]
entry = "kernel"
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi"
[actions.chainload-kernel]
chainload.path = "$boot\\EFI\\BOOT\\kernel.efi"
chainload.options = ["console=hvc0"]
chainload.linux-initrd = "$boot\\initramfs"
[entries.kernel]
title = "Boot Linux"
actions = ["chainload-kernel"]
[actions.chainload-shell]
chainload.path = "$boot\\EFI\\BOOT\\shell.efi"
[entries.shell]
title = "Boot Shell"
actions = ["chainload-shell"]
[actions.chainload-xen]
chainload.path = "$boot\\EFI\\BOOT\\xen.efi"
[entries.xen]
title = "Boot Xen"
actions = ["chainload-xen"]

View File

@@ -1,5 +1,9 @@
version = 1
[defaults]
entry = "edera"
menu-timeout = 0
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi"

View File

@@ -1,5 +1,9 @@
version = 1
[defaults]
entry = "kernel"
menu-timeout = 0
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\kernel.efi"

View File

@@ -1,11 +1,15 @@
version = 1
[defaults]
entry = "shell"
menu-timeout = 0
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\shell.efi"
[actions.chainload-shell]
chainload.path = "$boot\\EFI\\BOOT\\shell.efi"
[entries.xen]
[entries.shell]
title = "Boot Shell"
actions = ["chainload-shell"]

View File

@@ -1,5 +1,9 @@
version = 1
[defaults]
entry = "xen"
menu-timeout = 0
[extractors.boot.filesystem-device-match]
has-item = "\\EFI\\BOOT\\xen.efi"

View File

@@ -29,11 +29,10 @@ else
fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig
if [ "${TARGET_KARCH}" = "x86_64" ]
then
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config
./scripts/config -e XEN_PV
./scripts/config -e XEN_PV_DOM0
if [ "${TARGET_KARCH}" = "x86_64" ]; then
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" xen.config
./scripts/config -e XEN_PV
./scripts/config -e XEN_PV_DOM0
fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2yesconfig
@@ -45,5 +44,5 @@ make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" mod2yesconfig
make "-j$(nproc)" CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}"
[ -f "arch/x86/boot/bzImage" ] && cp "arch/x86/boot/bzImage" kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d < "arch/arm64/boot/Image.gz" > kernel.image
[ -f "arch/arm64/boot/Image.gz" ] && gzip -d <"arch/arm64/boot/Image.gz" >kernel.image
exit 0

View File

@@ -1,7 +1,7 @@
#!/bin/sh
#!/bin/bash
set -e
cd "$(dirname "${0}")/.." || exit 1
cargo fmt --all
shfmt -w hack/**/*.sh
cargo fmt --all || true
shfmt -w hack/**/*.sh || true

View File

@@ -1,5 +1,5 @@
use crate::context::SproutContext;
use anyhow::{Result, bail};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
@@ -50,7 +50,10 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
bail!("unknown action '{}'", name.as_ref());
};
// Finalize the context and freeze it.
let context = context.finalize().freeze();
let context = context
.finalize()
.context("unable to finalize context")?
.freeze();
// Execute the action.
if let Some(chainload) = &action.chainload {
@@ -61,6 +64,7 @@ pub fn execute(context: Rc<SproutContext>, name: impl AsRef<str>) -> Result<()>
return Ok(());
} else if let Some(edera) = &action.edera {
edera::edera(context.clone(), edera)?;
return Ok(());
}
#[cfg(feature = "splash")]

View File

@@ -1,5 +1,6 @@
use crate::context::SproutContext;
use anyhow::Result;
use log::info;
use serde::{Deserialize, Serialize};
use std::rc::Rc;
@@ -13,6 +14,6 @@ pub struct PrintConfiguration {
/// Executes the print action with the specified `configuration` inside the provided `context`.
pub fn print(context: Rc<SproutContext>, configuration: &PrintConfiguration) -> Result<()> {
println!("{}", context.stamp(&configuration.text));
info!("{}", context.stamp(&configuration.text));
Ok(())
}

View File

@@ -52,6 +52,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
height: image.height(),
};
// Handle the case where the image is zero-sized.
if input.height == 0 || input.width == 0 {
return input;
}
// Calculate the ratio of the image dimensions.
let input_ratio = input.width as f32 / input.height as f32;
@@ -66,6 +71,11 @@ fn fit_to_frame(image: &DynamicImage, frame: Rect) -> Rect {
height: frame.height,
};
// Handle the case where the output is zero-sized.
if output.height == 0 || output.width == 0 {
return output;
}
if input_ratio < frame_ratio {
output.width = (frame.height as f32 * input_ratio).floor() as u32;
output.height = frame.height;
@@ -110,7 +120,8 @@ fn draw(image: DynamicImage) -> Result<()> {
let image = resize_to_fit(&image, fit);
// Create a framebuffer to draw the image on.
let mut framebuffer = Framebuffer::new(width, height);
let mut framebuffer =
Framebuffer::new(width, height).context("unable to create framebuffer")?;
// Iterate over the pixels in the image and put them on the framebuffer.
for (x, y, pixel) in image.enumerate_pixels() {

View File

@@ -14,6 +14,9 @@ pub mod loader;
/// This must be incremented when the configuration breaks compatibility.
pub const LATEST_VERSION: u32 = 1;
/// The default timeout for the boot menu in seconds.
pub const DEFAULT_MENU_TIMEOUT_SECONDS: u64 = 10;
/// The Sprout configuration format.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct RootConfiguration {
@@ -22,6 +25,9 @@ pub struct RootConfiguration {
/// the configuration is the latest version.
#[serde(default = "latest_version")]
pub version: u32,
/// Default options for Sprout.
#[serde(default)]
pub defaults: DefaultsConfiguration,
/// Values to be inserted into the root sprout context.
#[serde(default)]
pub values: BTreeMap<String, String>,
@@ -59,6 +65,21 @@ pub struct RootConfiguration {
pub phases: PhasesConfiguration,
}
/// Default configuration for Sprout, used when the corresponding options are not specified.
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct DefaultsConfiguration {
/// The entry to boot without showing the boot menu.
/// If not specified, a boot menu is shown.
pub entry: Option<String>,
/// The timeout of the boot menu.
#[serde(rename = "menu-timeout", default = "default_menu_timeout")]
pub menu_timeout: u64,
}
fn latest_version() -> u32 {
LATEST_VERSION
}
fn default_menu_timeout() -> u64 {
DEFAULT_MENU_TIMEOUT_SECONDS
}

View File

@@ -1,11 +1,15 @@
use crate::actions::ActionDeclaration;
use crate::options::SproutOptions;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::{Result, bail};
use std::cmp::Reverse;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use uefi::proto::device_path::DevicePath;
/// The maximum number of iterations that can be performed in [SproutContext::finalize].
const CONTEXT_FINALIZE_ITERATION_LIMIT: usize = 100;
/// Declares a root context for Sprout.
/// This contains data that needs to be shared across Sprout.
#[derive(Default)]
@@ -151,11 +155,20 @@ impl SproutContext {
/// Finalizes a context by producing a context with no parent that contains all the values
/// of all parent contexts merged. This makes it possible to ensure [SproutContext] has no
/// inheritance with other [SproutContext]s. It will still contain a [RootContext] however.
pub fn finalize(&self) -> SproutContext {
pub fn finalize(&self) -> Result<SproutContext> {
// Collect all the values from the context and its parents.
let mut current_values = self.all_values();
// To ensure that there is no possible infinite loop, we need to check
// the number of iterations. If it exceeds 100, we bail.
let mut iterations: usize = 0;
loop {
iterations += 1;
if iterations > CONTEXT_FINALIZE_ITERATION_LIMIT {
bail!("infinite loop detected in context finalization");
}
let mut did_change = false;
let mut values = BTreeMap::new();
for (key, value) in &current_values {
@@ -176,11 +189,11 @@ impl SproutContext {
}
// Produce the final context.
Self {
Ok(Self {
root: self.root.clone(),
parent: None,
values: current_values,
}
})
}
/// Stamps the `text` value with the specified `values` map. The returned value indicates
@@ -188,7 +201,25 @@ impl SproutContext {
fn stamp_values(values: &BTreeMap<String, String>, text: impl AsRef<str>) -> (bool, String) {
let mut result = text.as_ref().to_string();
let mut did_change = false;
for (key, value) in values {
// Sort the keys by length. This is to ensure that we stamp the longest keys first.
// If we did not do this, "$abc" could be stamped by "$a" into an invalid result.
let mut keys = values.keys().collect::<Vec<_>>();
// Sort by key length, reversed. This results in the longest keys appearing first.
keys.sort_by_key(|key| Reverse(key.len()));
for key in keys {
// Empty keys are not supported.
if key.is_empty() {
continue;
}
// We can fetch the value from the map. It is verifiable that the key exists.
let Some(value) = values.get(key) else {
unreachable!("keys iterated over is collected on a map that cannot be modified");
};
let next_result = result.replace(&format!("${key}"), value);
if result != next_result {
did_change = true;

View File

@@ -27,6 +27,7 @@ pub struct BootableEntry {
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
default: bool,
}
impl BootableEntry {
@@ -42,6 +43,7 @@ impl BootableEntry {
title,
context,
declaration,
default: false,
}
}
@@ -65,6 +67,11 @@ impl BootableEntry {
&self.declaration
}
/// Fetch whether the entry is the default entry.
pub fn is_default(&self) -> bool {
self.default
}
/// Swap out the context of the entry.
pub fn swap_context(&mut self, context: Rc<SproutContext>) {
self.context = context;
@@ -75,8 +82,30 @@ impl BootableEntry {
self.title = self.context.stamp(&self.title);
}
/// Mark this entry as the default entry.
pub fn mark_default(&mut self) {
self.default = true;
}
/// Prepend the name of the entry with `prefix`.
pub fn prepend_name_prefix(&mut self, prefix: &str) {
self.name.insert_str(0, prefix);
}
/// Determine if this entry matches `needle` by comparing to the name or title of the entry.
pub fn is_match(&self, needle: &str) -> bool {
self.name == needle || self.title == needle
}
/// Find an entry by `needle` inside the entry iterator `haystack`.
/// This will search for an entry by name, title, or index.
pub fn find<'a>(
needle: &str,
haystack: impl Iterator<Item = &'a BootableEntry>,
) -> Option<&'a BootableEntry> {
haystack
.enumerate()
.find(|(index, entry)| entry.is_match(needle) || index.to_string() == needle)
.map(|(_index, entry)| entry)
}
}

View File

@@ -81,7 +81,7 @@ pub fn extract(
} else {
// We should still handle other errors gracefully.
Err(error).context("unable to open filesystem partition info")?;
None
unreachable!()
}
}
}

View File

@@ -6,16 +6,16 @@ use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::rc::Rc;
use std::str::FromStr;
use uefi::CString16;
use uefi::fs::{FileSystem, Path};
use uefi::cstr16;
use uefi::fs::{FileSystem, PathBuf};
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 default path to the BLS directory.
const BLS_TEMPLATE_PATH: &str = "\\loader";
/// The configuration of the BLS generator.
/// The BLS uses the Bootloader Specification to produce
@@ -24,7 +24,7 @@ const BLS_TEMPLATE_PATH: &str = "\\loader\\entries";
pub struct BlsConfiguration {
/// The entry to use for as a template.
pub entry: EntryDeclaration,
/// The path to the BLS entries directory.
/// The path to the BLS directory.
#[serde(default = "default_bls_path")]
pub path: String,
}
@@ -45,30 +45,31 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Vec<BootableEntry>> {
let mut entries = Vec::new();
// Stamp the path to the BLS entries directory.
// Stamp the path to the BLS 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)
// Resolve the path to the BLS directory.
let bls_resolved = utils::resolve_path(context.root().loaded_image_path()?, &path)
.context("unable to resolve bls path")?;
// Construct a filesystem path to the BLS entries directory.
let mut entries_path = PathBuf::from(
bls_resolved
.sub_path
.to_string(DisplayOnly(false), AllowShortcuts(false))
.context("unable to convert bls path to string")?,
);
entries_path.push(cstr16!("entries"));
// 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 fs =
uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(bls_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)
.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.
@@ -89,10 +90,9 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
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);
// Create a mutable path so we can append the file name to produce the full path.
let mut full_entry_path = entries_path.to_path_buf();
full_entry_path.push(entry.file_name());
// Read the entry file.
let content = fs

View File

@@ -36,8 +36,13 @@ impl FromStr for BlsEntry {
// Trim the line.
let line = line.trim();
// Split the line once by a space.
let Some((key, value)) = line.split_once(" ") else {
// Skip over empty lines and comments.
if line.is_empty() || line.starts_with('#') {
continue;
}
// Split the line once by whitespace.
let Some((key, value)) = line.split_once(char::is_whitespace) else {
continue;
};
@@ -99,7 +104,7 @@ impl BlsEntry {
self.linux
.clone()
.or(self.efi.clone())
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the path to an initrd to pass to the kernel, if any.
@@ -107,7 +112,7 @@ impl BlsEntry {
pub fn initrd_path(&self) -> Option<String> {
self.initrd
.clone()
.map(|path| path.replace("/", "\\").trim_start_matches("\\").to_string())
.map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string())
}
/// Fetches the options to pass to the kernel, if any.

View File

@@ -10,6 +10,7 @@ use anyhow::{Context, Result};
use log::info;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::time::Duration;
use uefi::proto::device_path::LoadedImageDevicePath;
use uefi::proto::device_path::text::{AllowShortcuts, DisplayOnly};
@@ -34,6 +35,9 @@ pub mod extractors;
/// generators: Runtime code that can generate entries with specific values.
pub mod generators;
/// menu: Display a boot menu to select an entry to boot.
pub mod menu;
/// phases: Hooks into specific parts of the boot process.
pub mod phases;
@@ -107,7 +111,7 @@ fn main() -> Result<()> {
context.insert(&extracted);
let context = context.freeze();
// Execute the late phase.
// Execute the startup phase.
phase(context.clone(), &config.phases.startup).context("unable to execute startup phase")?;
let mut entries = Vec::new();
@@ -143,36 +147,55 @@ fn main() -> Result<()> {
// Insert the values from the entry configuration into the
// sprout context to use with the entry itself.
context.insert(&entry.declaration().values);
let context = context.finalize().freeze();
let context = context
.finalize()
.context("unable to finalize context")?
.freeze();
// Provide the new context to the bootable entry.
entry.swap_context(context);
// Restamp the title with any values.
entry.restamp_title();
// Mark this entry as the default entry if it is declared as such.
if let Some(ref default_entry) = config.defaults.entry {
// If the entry matches the default entry, mark it as the default entry.
if entry.is_match(default_entry) {
entry.mark_default();
}
}
}
// TODO(azenla): Implement boot menu here.
// For now, we just print all of the entries.
info!("entries:");
for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title);
info!(" entry {} [{}]: {}", index, entry.name(), title);
// If no entries were the default, pick the first entry as the default entry.
if entries.iter().all(|entry| !entry.is_default())
&& let Some(entry) = entries.first_mut()
{
entry.mark_default();
}
// Execute the late phase.
phase(context.clone(), &config.phases.late).context("unable to execute late phase")?;
// Use the boot option if possible, otherwise pick the first entry.
let entry = if let Some(ref boot) = context.root().options().boot {
entries
.iter()
.enumerate()
.find(|(index, entry)| {
entry.name() == boot || entry.title() == boot || &index.to_string() == boot
})
.context(format!("unable to find entry: {boot}"))?
.1 // select the bootable entry.
// If --boot is specified, boot that entry immediately.
let force_boot_entry = context.root().options().boot.as_ref();
// If --force-menu is specified, show the boot menu regardless of the value of --boot.
let force_boot_menu = context.root().options().force_menu;
// Determine the menu timeout in seconds based on the options or configuration.
// We prefer the options over the configuration to allow for overriding.
let menu_timeout = context
.root()
.options()
.menu_timeout
.unwrap_or(config.defaults.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.
let entry = if !force_boot_menu && let Some(ref force_boot_entry) = force_boot_entry {
BootableEntry::find(force_boot_entry, entries.iter())
.context(format!("unable to find entry: {force_boot_entry}"))?
} else {
entries.first().context("no entries found")?
// Delegate to the menu to select an entry to boot.
menu::select(menu_timeout, &entries).context("unable to select entry via boot menu")?
};
// Execute all the actions for the selected entry.

153
src/menu.rs Normal file
View File

@@ -0,0 +1,153 @@
use crate::entries::BootableEntry;
use anyhow::{Context, Result, bail};
use log::info;
use std::time::Duration;
use uefi::ResultExt;
use uefi::boot::TimerTrigger;
use uefi::proto::console::text::{Input, Key, ScanCode};
use uefi_raw::table::boot::{EventType, Tpl};
/// The characters that can be used to select an entry from keys.
const ENTRY_NUMBER_TABLE: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
/// Represents the operation that can be performed by the boot menu.
#[derive(PartialEq, Eq)]
enum MenuOperation {
/// The user selected a numbered entry.
Number(usize),
/// The user selected the escape key to exit the boot menu.
Exit,
/// The user selected the enter key to display the entries again.
Continue,
/// Timeout occurred.
Timeout,
/// No operation should be performed.
Nop,
}
/// Read a key from the input device with a duration, returning the [MenuOperation] that was
/// performed.
fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
// The event to wait for a key press.
let key_event = input
.wait_for_key_event()
.context("unable to acquire key event")?;
// Timer event for timeout.
// SAFETY: The timer event creation allocated a timer pointer on the UEFI heap.
// This is validated safe as long as we are in boot services.
let timer_event = unsafe {
uefi::boot::create_event_ex(EventType::TIMER, Tpl::CALLBACK, None, None, None)
.context("unable to create timer event")?
};
// The timeout is in increments of 100 nanoseconds.
let trigger = TimerTrigger::Relative(timeout.as_nanos() as u64 / 100);
uefi::boot::set_timer(&timer_event, trigger).context("unable to set timeout timer")?;
let mut events = [timer_event, key_event];
let event = uefi::boot::wait_for_event(&mut events)
.discard_errdata()
.context("unable to wait for event")?;
// The first event is the timer event.
// If it has triggered, the user did not select a numbered entry.
if event == 0 {
return Ok(MenuOperation::Timeout);
}
// If we reach here, there is a key event.
let Some(key) = input.read_key().context("unable to read key")? else {
bail!("no key was pressed");
};
match key {
Key::Printable(c) => {
// If the key is not ascii, we can't process it.
if !c.is_ascii() {
return Ok(MenuOperation::Continue);
}
// Convert the key to a char.
let c: char = c.into();
// Find the key pressed in the entry number table or continue.
Ok(ENTRY_NUMBER_TABLE
.iter()
.position(|&x| x == c)
.map(MenuOperation::Number)
.unwrap_or(MenuOperation::Continue))
}
// The escape key is used to exit the boot menu.
Key::Special(ScanCode::ESCAPE) => Ok(MenuOperation::Exit),
// If the special key is unknown, do nothing.
Key::Special(_) => Ok(MenuOperation::Nop),
}
}
/// Selects an entry from the list of entries using the boot menu.
fn select_with_input<'a>(
input: &mut Input,
timeout: Duration,
entries: &'a [BootableEntry],
) -> Result<&'a BootableEntry> {
loop {
// If the timeout is not zero, let's display the boot menu.
if !timeout.is_zero() {
// Until a pretty menu is available, we just print all the entries.
info!("Boot Menu:");
for (index, entry) in entries.iter().enumerate() {
let title = entry.context().stamp(&entry.declaration().title);
info!(" [{}] {} ({})", index, title, entry.name());
}
}
// Read from input until a valid operation is selected.
let operation = loop {
// If the timeout is zero, we can exit immediately because there is nothing to do.
if timeout.is_zero() {
break MenuOperation::Exit;
}
info!("Select a boot entry using the number keys.");
info!("Press Escape to exit and enter to display the entries again.");
let operation = read(input, &timeout)?;
if operation != MenuOperation::Nop {
break operation;
}
};
match operation {
// Entry was selected by number. If the number is invalid, we continue.
MenuOperation::Number(index) => {
let Some(entry) = entries.get(index) else {
println!("invalid entry number");
continue;
};
return Ok(entry);
}
// When the user exits the boot menu or a timeout occurs, we should
// boot the default entry, if any.
MenuOperation::Exit | MenuOperation::Timeout => {
return entries
.iter()
.find(|item| item.is_default())
.context("no default entry available");
}
// If the operation is to continue or nop, we can just run the loop again.
MenuOperation::Continue | MenuOperation::Nop => {
continue;
}
}
}
}
/// Shows a boot menu to select a bootable entry to boot.
/// The actual work is done internally in [select_with_input] which is called
/// within the context of the standard input device.
pub fn select(timeout: Duration, entries: &[BootableEntry]) -> Result<&BootableEntry> {
// Acquire the standard input device and run the boot menu.
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
}

View File

@@ -15,6 +15,10 @@ pub struct SproutOptions {
pub config: String,
/// Entry to boot without showing the boot menu.
pub boot: Option<String>,
/// Force display of the boot menu.
pub force_menu: bool,
/// The timeout for the boot menu in seconds.
pub menu_timeout: Option<u64>,
}
/// The default Sprout options.
@@ -23,6 +27,8 @@ impl Default for SproutOptions {
Self {
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
force_menu: false,
menu_timeout: None,
}
}
}
@@ -49,6 +55,20 @@ impl OptionsRepresentable for SproutOptions {
form: OptionForm::Value,
},
),
(
"force-menu",
OptionDescription {
description: "Force showing of the boot menu",
form: OptionForm::Flag,
},
),
(
"menu-timeout",
OptionDescription {
description: "Boot menu timeout, in seconds",
form: OptionForm::Value,
},
),
(
"help",
OptionDescription {
@@ -76,6 +96,20 @@ impl OptionsRepresentable for SproutOptions {
result.boot = Some(value.context("--boot option requires a value")?);
}
"force-menu" => {
// Force showing of the boot menu.
result.force_menu = true;
}
"menu-timeout" => {
// The timeout for the boot menu in seconds.
let value = value.context("--menu-timeout option requires a value")?;
let value = value
.parse::<u64>()
.context("menu-timeout must be a number")?;
result.menu_timeout = Some(value);
}
_ => bail!("unknown option: --{key}"),
}
}

View File

@@ -72,11 +72,7 @@ pub trait OptionsRepresentable {
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
if let Some((part_key, part_value)) = option.split_once('=') {
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
@@ -131,7 +127,7 @@ pub trait OptionsRepresentable {
);
}
// Exit because the help has been displayed.
std::process::exit(1);
std::process::exit(0);
}
// Insert the option and the value into the map.

View File

@@ -27,6 +27,12 @@ pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
.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)"
@@ -37,7 +43,7 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.map(|item| cstring16_contains_char(item, '('))
.unwrap_or(false)
{
Some(item.unwrap_or_default())
@@ -62,7 +68,7 @@ pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
if item
.as_ref()
.map(|item| item.to_string().contains("("))
.map(|item| cstring16_contains_char(item, '('))
.unwrap_or(false)
{
None
@@ -104,11 +110,11 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
it.to_string(DisplayOnly(false), AllowShortcuts(false))
.unwrap_or_default()
})
.map(|it| it.to_string().contains("("))
.map(|it| it.to_string().contains('('))
.unwrap_or(false);
if !path_has_device {
let mut input = input.to_string();
if !input.starts_with("\\") {
if !input.starts_with('\\') {
input.insert(0, '\\');
}
input.insert_str(

View File

@@ -13,17 +13,33 @@ pub struct Framebuffer {
impl Framebuffer {
/// Creates a new framebuffer of the specified `width` and `height`.
pub fn new(width: usize, height: usize) -> Self {
Framebuffer {
pub fn new(width: usize, height: usize) -> Result<Self> {
// Verify that the size is valid during multiplication.
let size = width
.checked_mul(height)
.context("framebuffer size overflow")?;
// Initialize the pixel buffer with black pixels, with the verified size.
let pixels = vec![BltPixel::new(0, 0, 0); size];
Ok(Framebuffer {
width,
height,
pixels: vec![BltPixel::new(0, 0, 0); width * height],
}
pixels,
})
}
/// Mutably acquires a pixel of the framebuffer at the specified `x` and `y` coordinate.
pub fn pixel(&mut self, x: usize, y: usize) -> Option<&mut BltPixel> {
self.pixels.get_mut(y * self.width + x)
// Verify that the coordinates are within the bounds of the framebuffer.
if x >= self.width || y >= self.height {
return None;
}
// Calculate the index of the pixel safely, returning None if it overflows.
let index = y.checked_mul(self.width)?.checked_add(x)?;
// Return the pixel at the index. If the index is out of bounds, this will return None.
self.pixels.get_mut(index)
}
/// Blit the framebuffer to the specified `gop` [GraphicsOutput].

View File

@@ -97,7 +97,7 @@ impl MediaLoaderHandle {
}
/// Creates a new device path for the media loader based on a vendor `guid`.
fn device_path(guid: Guid) -> Box<DevicePath> {
fn device_path(guid: Guid) -> Result<Box<DevicePath>> {
// The buffer for the device path.
let mut path = Vec::new();
// Build a device path for the media loader with a vendor-specific guid.
@@ -106,18 +106,18 @@ impl MediaLoaderHandle {
vendor_guid: guid,
vendor_defined_data: &[],
})
.unwrap() // We know that the device path is valid, so we can unwrap.
.context("unable to produce device path")?
.finalize()
.unwrap(); // We know that the device path is valid, so we can unwrap.
.context("unable to produce device path")?;
// Convert the device path to a boxed device path.
// This is safer than dealing with a pooled device path.
path.to_boxed()
Ok(path.to_boxed())
}
/// Checks if the media loader is already registered with the UEFI stack.
fn already_registered(guid: Guid) -> Result<bool> {
// Acquire the device path for the media loader.
let path = Self::device_path(guid);
let path = Self::device_path(guid)?;
let mut existing_path = path.as_ref();
@@ -142,7 +142,7 @@ impl MediaLoaderHandle {
/// to load the data from.
pub fn register(guid: Guid, data: Box<[u8]>) -> Result<MediaLoaderHandle> {
// Acquire the vendor device path for the media loader.
let path = Self::device_path(guid);
let path = Self::device_path(guid)?;
// Check if the media loader is already registered.
// If it is, we can't register it again safely.