50 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
160a7737fb sprout: version 0.0.9 2025-10-24 18:17:27 -07:00
68220d0de1 chore(workflows): repin all actions 2025-10-24 18:09:24 -07:00
e9b842a81f chore(workflows): separate release workflow 2025-10-24 18:04:55 -07:00
efb357d62b chore(workflows): add concurrency limiter 2025-10-24 18:01:50 -07:00
30600f0c81 fix(main): repair contextual replacement for entries 2025-10-24 17:16:42 -07:00
e10e98d669 chore(assets): update sprout logo 2025-10-24 16:49:34 -07:00
911b617d92 feat(options): --boot now supports selecting by entry name or index, not just title 2025-10-24 16:32:48 -07:00
d3f9e876fb fix(docs): cleanup rustdoc warnings 2025-10-24 15:55:56 -07:00
e096f8e236 sprout: version 0.0.8 2025-10-24 14:53:17 -07:00
a14686a286 update README with background section 2025-10-24 14:49:11 -07:00
5108b61a15 implement new argument parser with --help support 2025-10-21 19:12:16 -07:00
2aeb0474e6 dev: add support for automatic hypervisor.framework usage on macOS 2025-10-21 05:40:22 -07:00
22c8884f7e booting windows is supported! 2025-10-20 20:35:13 -07:00
3a2b314669 chore(options): move parsing code to a parser module 2025-10-20 19:42:39 -07:00
42 changed files with 1062 additions and 298 deletions

View File

@@ -9,6 +9,10 @@ on:
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs:
zizmor:
name: zizmor
@@ -24,12 +28,12 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: setup uv
uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d # v7
uses: astral-sh/setup-uv@2ddd2b9cb38ad8efd50337e8ab201519a34c9f24 # v7.1.1
- name: zizmor
run: uvx zizmor --pedantic --format sarif . > results.sarif
@@ -37,7 +41,7 @@ jobs:
GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
- name: upload
uses: github/codeql-action/upload-sarif@16140ae1a102900babc80a33c44059580f687047 # v4
uses: github/codeql-action/upload-sarif@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
sarif_file: results.sarif
category: zizmor

View File

@@ -11,6 +11,10 @@ on:
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs:
rustfmt:
name: rustfmt
@@ -22,7 +26,7 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -52,7 +56,7 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -81,7 +85,7 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

View File

@@ -11,6 +11,10 @@ on:
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs:
analyze:
name: analyze (${{ matrix.language }})
@@ -36,18 +40,18 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: initialize codeql
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 #v4
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
config-file: ./.github/codeql/codeql-config.yaml
- name: perform codeql analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 #v4
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -1,13 +1,6 @@
name: publish
on:
workflow_dispatch:
inputs:
release-tag:
description: 'Release Tag'
required: true
type: string
push:
branches:
- main
@@ -25,11 +18,15 @@ on:
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}"
cancel-in-progress: true
jobs:
artifacts:
name: artifacts
permissions:
contents: write # Needed to upload release assets and artifacts.
contents: write # Needed to upload artifacts.
runs-on: ubuntu-latest
steps:
- name: harden runner
@@ -38,7 +35,7 @@ jobs:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
@@ -50,35 +47,13 @@ jobs:
run: ./hack/assemble.sh
- name: 'upload sprout-x86_64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: sprout-x86_64.efi
path: target/assemble/sprout-x86_64.efi
- name: 'upload sprout-aarch64.efi artifact'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: sprout-aarch64.efi
path: target/assemble/sprout-aarch64.efi
- name: 'generate cultivator token'
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}

62
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,62 @@
name: release
on:
workflow_dispatch:
inputs:
release-tag:
description: 'Release Tag'
required: true
type: string
permissions:
contents: read # Needed to checkout the repository.
concurrency:
group: "${{ github.workflow }}-${{ github.event.inputs.release-tag }}"
cancel-in-progress: true
jobs:
release:
name: release
permissions:
contents: write # Needed to upload release assets.
runs-on: ubuntu-latest
steps:
- name: harden runner
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
with:
egress-policy: audit
- name: checkout
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- name: 'install rust toolchain'
run: |
cargo version
- name: 'assemble artifacts'
run: ./hack/assemble.sh
- name: 'generate cultivator token'
uses: actions/create-github-app-token@bf559f85448f9380bcfa2899dbdc01eb5b37be3a # v3.0.0-beta.2
id: generate-token
with:
app-id: "${{ secrets.EDERA_CULTIVATION_APP_ID }}"
private-key: "${{ secrets.EDERA_CULTIVATION_APP_PRIVATE_KEY }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'upload release artifacts'
run: ./hack/ci/upload-release-assets.sh
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}
- name: 'mark release as published'
run: gh release edit "${RELEASE_TAG}" --draft=false --verify-tag
env:
GITHUB_TOKEN: "${{ steps.generate-token.outputs.token }}"
RELEASE_TAG: "${{ github.event.inputs.release-tag }}"
if: ${{ github.event.inputs.release-tag != '' }}

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.7"
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.7"
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

@@ -1,6 +1,6 @@
<div align="center">
![Sprout Logo](assets/logo.png)
![Sprout Logo](assets/logo-small.png)
# Sprout
@@ -18,10 +18,28 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology utilizes a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, utilizes a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
Sprout was designed to take in a machine-readable, writable, and modifiable configuration that treats boot information
like data plus configuration, and can be chained from both UEFI firmware and GRUB alike.
Sprout aims to be flexible, secure, and modern. Written in Rust, it handles data safely and uses unsafe code as little
as possible. It also critically must be easy to install into all common distributions, relying on simple principles to
simplify installation and usage.
## Documentation
- [Fedora Setup Guide]
- [Generic Linux Setup Guide]
- [Alpine Edge Setup Guide]
- [Windows Setup Guide]
- [Development Guide]
- [Contributing Guide]
@@ -31,8 +49,8 @@ Sprout is licensed under Apache 2.0 and is open to modifications and contributio
## 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
@@ -40,15 +58,15 @@ have secure boot support. In fact, as of writing, it doesn't even have a boot me
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support
- [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
- [ ] Windows boot support (untested via chainload)
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
@@ -117,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
@@ -128,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"]
@@ -139,8 +157,10 @@ chainload.options = ["$options"]
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

BIN
assets/logo-large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
assets/logo-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

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,8 +36,11 @@ 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
if [ "$(uname)" = "Darwin" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
[ "$(sysctl -n kern.hv_support 2>&1 || true)" = "1" ]; then
QEMU_ACCEL="hvf"
fi

View File

@@ -19,7 +19,7 @@ elif [ "${TARGET_ARCH}" = "aarch64" ]; then
fi
if [ -n "${QEMU_ACCEL}" ]; then
set -- "${@}" "-accel" "kvm"
set -- "${@}" "-accel" "${QEMU_ACCEL}"
fi
if [ "${QEMU_GDB}" = "1" ]; then
@@ -35,6 +35,7 @@ set -- "${@}" -smp 2 -m 4096
if [ "${NO_GRAPHICAL_BOOT}" = "1" ]; then
set -- "${@}" -nographic
else
if [ "${GRAPHICAL_ONLY}" != "1" ]; then
if [ "${QEMU_LEGACY_SERIAL}" = "1" ]; then
set -- "${@}" -serial stdio
else
@@ -43,6 +44,7 @@ else
-chardev stdio,id=stdio0 \
-device virtconsole,chardev=stdio0,id=console0
fi
fi
if [ "${QEMU_LEGACY_VGA}" = "1" ]; then
set -- "${@}" -vga std

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,8 +29,7 @@ else
fi
make CROSS_COMPILE="${MAYBE_CROSS_COMPILE}" ARCH="${TARGET_KARCH}" defconfig
if [ "${TARGET_KARCH}" = "x86_64" ]
then
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

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

@@ -19,7 +19,7 @@ pub struct DriverDeclaration {
pub path: String,
}
/// Loads the driver specified by the [driver] declaration.
/// Loads the driver specified by the `driver` declaration.
fn load_driver(context: Rc<SproutContext>, driver: &DriverDeclaration) -> Result<()> {
// Acquire the handle and device path of the loaded image.
let sprout_image = uefi::boot::image_handle();

View File

@@ -1,5 +1,7 @@
use crate::context::SproutContext;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::rc::Rc;
/// Declares a boot entry to display in the boot menu.
///
@@ -8,6 +10,7 @@ use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Default, Clone)]
pub struct EntryDeclaration {
/// The title of the entry which will be display in the boot menu.
/// This is the pre-stamped value.
pub title: String,
/// The actions to run when the entry is selected.
#[serde(default)]
@@ -16,3 +19,93 @@ pub struct EntryDeclaration {
#[serde(default)]
pub values: BTreeMap<String, String>,
}
/// Represents an entry that is stamped and ready to be booted.
#[derive(Clone)]
pub struct BootableEntry {
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
default: bool,
}
impl BootableEntry {
/// Create a new bootable entry to represent the full context of an entry.
pub fn new(
name: String,
title: String,
context: Rc<SproutContext>,
declaration: EntryDeclaration,
) -> Self {
Self {
name,
title,
context,
declaration,
default: false,
}
}
/// Fetch the name of the entry. This is usually a machine-identifiable key.
pub fn name(&self) -> &str {
&self.name
}
/// Fetch the title of the entry. This is usually a human-readable key.
pub fn title(&self) -> &str {
&self.title
}
/// Fetch the full context of the entry.
pub fn context(&self) -> Rc<SproutContext> {
Rc::clone(&self.context)
}
/// Fetch the declaration of the entry.
pub fn declaration(&self) -> &EntryDeclaration {
&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;
}
/// Restamp the title with the current context.
pub fn restamp_title(&mut self) {
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

@@ -1,5 +1,5 @@
use crate::context::SproutContext;
use crate::entries::EntryDeclaration;
use crate::entries::BootableEntry;
use crate::generators::bls::BlsConfiguration;
use crate::generators::matrix::MatrixConfiguration;
use anyhow::Result;
@@ -40,7 +40,7 @@ pub struct GeneratorDeclaration {
pub fn generate(
context: Rc<SproutContext>,
generator: &GeneratorDeclaration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
) -> Result<Vec<BootableEntry>> {
if let Some(matrix) = &generator.matrix {
matrix::generate(context, matrix)
} else if let Some(bls) = &generator.bls {

View File

@@ -1,21 +1,21 @@
use crate::context::SproutContext;
use crate::entries::EntryDeclaration;
use crate::entries::{BootableEntry, EntryDeclaration};
use crate::generators::bls::entry::BlsEntry;
use crate::utils;
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,
}
@@ -42,36 +42,34 @@ fn quirk_initrd_remove_tuned(input: String) -> String {
/// Generates entries from the BLS entries directory using the specified `bls` configuration and
/// `context`. The BLS conversion is best-effort and will ignore any unsupported entries.
pub fn generate(
context: Rc<SproutContext>,
bls: &BlsConfiguration,
) -> Result<Vec<(Rc<SproutContext>, EntryDeclaration)>> {
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)
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.
@@ -92,10 +90,9 @@ pub fn generate(
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
@@ -116,7 +113,7 @@ pub fn generate(
// Produce a new sprout context for the entry with the extracted values.
let mut context = context.fork();
let title = entry.title().unwrap_or(name);
let title = entry.title().unwrap_or_else(|| name.clone());
let chainload = entry.chainload_path().unwrap_or_default();
let options = entry.options().unwrap_or_default();
@@ -129,7 +126,12 @@ pub fn generate(
context.set("initrd", initrd);
// Add the entry to the list with a frozen context.
entries.push((context.freeze(), bls.entry.clone()));
entries.push(BootableEntry::new(
name,
bls.entry.title.clone(),
context.freeze(),
bls.entry.clone(),
));
}
Ok(entries)

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

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

View File

@@ -2,11 +2,15 @@
#![feature(uefi_std)]
use crate::context::{RootContext, SproutContext};
use crate::entries::BootableEntry;
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase;
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};
@@ -31,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;
@@ -51,7 +58,7 @@ fn main() -> Result<()> {
setup::init()?;
// Parse the options to the sprout executable.
let options = options::parse().context("unable to parse options")?;
let options = SproutOptions::parse().context("unable to parse options")?;
// Load the configuration of sprout.
// At this point, the configuration has been validated and the specified
@@ -104,66 +111,97 @@ 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 staged_entries = Vec::new();
let mut entries = Vec::new();
// Insert all the static entries from the configuration into the entry list.
for (_name, entry) in config.entries {
for (name, entry) in config.entries {
// Associate the main context with the static entry.
staged_entries.push((context.clone(), entry));
entries.push(BootableEntry::new(
name,
entry.title.clone(),
context.clone(),
entry,
));
}
// Run all the generators declared in the configuration.
for (_name, generator) in config.generators {
for (name, generator) in config.generators {
let context = context.fork().freeze();
// We will prefix all entries with [name]-.
let prefix = format!("{}-", name);
// Add all the entries generated by the generator to the entry list.
// The generator specifies the context associated with the entry.
for entry in generators::generate(context.clone(), &generator)? {
staged_entries.push(entry);
for mut entry in generators::generate(context.clone(), &generator)? {
entry.prepend_name_prefix(&prefix);
entries.push(entry);
}
}
// Build a list of all the final boot entries.
let mut final_entries = Vec::new();
for (context, entry) in staged_entries {
let mut context = context.fork();
for entry in &mut entries {
let mut context = entry.context().fork();
// Insert the values from the entry configuration into the
// sprout context to use with the entry itself.
context.insert(&entry.values);
let context = context.finalize().freeze();
context.insert(&entry.declaration().values);
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();
// Insert the entry configuration into final boot entries with the extended context.
final_entries.push((context, entry));
// 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, (context, entry)) in final_entries.iter().enumerate() {
let title = context.stamp(&entry.title);
info!(" entry {}: {}", index + 1, 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 (context, entry) = if let Some(ref boot) = context.root().options().boot {
final_entries
.iter()
.find(|(_context, entry)| &entry.title == boot)
.context(format!("unable to find entry: {boot}"))?
// 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 {
final_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.
for action in &entry.actions {
let action = context.stamp(action);
actions::execute(context.clone(), &action)
for action in &entry.declaration().actions {
let action = entry.context().stamp(action);
actions::execute(entry.context().clone(), &action)
.context(format!("unable to execute action '{}'", action))?;
}

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

@@ -1,6 +1,10 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// The Sprout options parser.
pub mod parser;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
@@ -11,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.
@@ -19,84 +27,62 @@ impl Default for SproutOptions {
Self {
config: DEFAULT_CONFIG_PATH.to_string(),
boot: None,
force_menu: false,
menu_timeout: None,
}
}
}
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
/// The options parser mechanism for Sprout.
impl OptionsRepresentable for SproutOptions {
/// Produce the [SproutOptions] structure.
type Output = Self;
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
/// All the Sprout options that are defined.
fn options() -> &'static [(&'static str, OptionDescription<'static>)] {
&[
(
"config",
OptionDescription {
description: "Path to Sprout configuration file",
form: OptionForm::Value,
},
),
(
"boot",
OptionDescription {
description: "Entry to boot, bypassing the menu",
form: OptionForm::Value,
},
),
(
"force-menu",
OptionDescription {
description: "Force showing of the boot menu",
form: OptionForm::Flag,
},
),
(
"menu-timeout",
OptionDescription {
description: "Boot menu timeout, in seconds",
form: OptionForm::Value,
},
),
(
"help",
OptionDescription {
description: "Display Sprout Help",
form: OptionForm::Help,
},
),
]
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
if value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option: {option}");
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parse the arguments to Sprout as a [SproutOptions] structure.
pub fn parse() -> Result<SproutOptions> {
/// Produces [SproutOptions] from the parsed raw `options` map.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self> {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = SproutOptions::default();
let options = parse_raw().context("unable to parse options")?;
let mut result = Self::default();
for (key, value) in options {
match key.as_str() {
@@ -110,8 +96,23 @@ pub fn parse() -> Result<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}"),
}
}
Ok(result)
}
}

146
src/options/parser.rs Normal file
View File

@@ -0,0 +1,146 @@
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior
/// of how options are handled.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum OptionForm {
/// A flag, like --verbose.
Flag,
/// A value, in the form --abc 123 or --abc=123.
Value,
/// Help flag, like --help.
Help,
}
/// The description of an option, used in the options parser
/// to make decisions about how to progress.
#[derive(Debug, Clone)]
pub struct OptionDescription<'a> {
/// The description of the option.
pub description: &'a str,
/// The type of option to parse as.
pub form: OptionForm,
}
/// Represents a type that can be parsed from command line arguments.
/// This is a super minimal options parser mechanism just for Sprout.
pub trait OptionsRepresentable {
/// The output type that parsing will produce.
type Output;
/// The configured options for this type. This should describe all the options
/// that are valid to produce the type. The left hand side is the name of the option,
/// and the right hand side is the description.
fn options() -> &'static [(&'static str, OptionDescription<'static>)];
/// Produces the type by taking the `options` and processing it into the output.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self::Output>;
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if let Some((part_key, part_value)) = option.split_once('=') {
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option");
}
// Find the description of the configured option, if any.
let Some(description) = configured.get(option.as_str()) else {
bail!("invalid option: --{option}");
};
// Check if the option requires a value and error if none was provided.
if description.form == OptionForm::Value && value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// If the option form does not support a value and there is a value, error.
if description.form != OptionForm::Value && value.is_some() {
bail!("option --{} does not take a value", option);
}
// Handle the --help flag case.
if description.form == OptionForm::Help {
// Generic configured options output.
println!("Configured Options:");
for (name, description) in &configured {
println!(
" --{}{}: {}",
name,
if description.form == OptionForm::Value {
" <value>"
} else {
""
},
description.description
);
}
// Exit because the help has been displayed.
std::process::exit(0);
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce].
fn parse() -> Result<Self::Output> {
// Parse the program arguments into a raw map.
let options = Self::parse_raw().context("unable to parse options")?;
// Produce the options from the map.
Self::produce(options)
}
}

View File

@@ -12,7 +12,7 @@ pub mod framebuffer;
/// Support code for the media loader protocol.
pub mod media_loader;
/// Parses the input [path] as a [DevicePath].
/// Parses the input `path` as a [DevicePath].
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
@@ -27,7 +27,13 @@ pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
.context("unable to convert text to device path")
}
/// Grabs the root part of the [path].
/// Checks if a [CString16] contains a char `c`.
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
string.to_string().contains(c)
}
/// Grabs the root part of the `path`.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
pub fn device_path_root(path: &DevicePath) -> Result<String> {
@@ -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())
@@ -52,7 +58,7 @@ pub fn device_path_root(path: &DevicePath) -> Result<String> {
Ok(path)
}
/// Grabs the part of the [path] after the root.
/// Grabs the part of the `path` after the root.
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
/// it will give "\EFI\BOOT\BOOTX64.efi"
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
@@ -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
@@ -92,8 +98,8 @@ pub struct ResolvedPath {
pub filesystem_handle: Handle,
}
/// Resolve a path specified by [input] to its various components.
/// Uses [default_root_path] as the base root if one is not specified in the path.
/// Resolve a path specified by `input` to its various components.
/// Uses `default_root_path` as the base root if one is not specified in the path.
/// Returns [ResolvedPath] which contains the resolved components.
pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<ResolvedPath> {
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
@@ -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(
@@ -137,9 +143,9 @@ pub fn resolve_path(default_root_path: &DevicePath, input: &str) -> Result<Resol
})
}
/// Read the contents of a file at the location specified with the [input] path.
/// Read the contents of a file at the location specified with the `input` path.
/// Internally, this uses [resolve_path] to resolve the path to its various components.
/// [resolve_path] is passed the [default_root_path] which should specify a base root.
/// [resolve_path] is passed the `default_root_path` which should specify a base root.
///
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
/// filesystem handle, so care must be taken to call this function outside a scope with

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

@@ -46,8 +46,8 @@ pub struct MediaLoaderHandle {
impl MediaLoaderHandle {
/// The behavior of this function is derived from how Linux calls it.
///
/// Linux calls this function by first passing a NULL [buffer].
/// We must set the size of the buffer it should allocate in [buffer_size].
/// Linux calls this function by first passing a NULL `buffer`.
/// We must set the size of the buffer it should allocate in `buffer_size`.
/// 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
/// the buffer size.
@@ -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();
@@ -137,12 +137,12 @@ impl MediaLoaderHandle {
Ok(false)
}
/// Registers the provided [data] with the UEFI stack as media loader.
/// Registers the provided `data` with the UEFI stack as media loader.
/// This uses a special device path that other EFI programs will look at
/// 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.