bootmgr_rs_core/boot/
config.rs

1// SPDX-FileCopyrightText: 2025 some100 <ootinnyoo@outlook.com>
2// SPDX-License-Identifier: MIT
3
4//! Provides [`BootConfig`], the configuration file for the bootloader.
5//!
6//! This parses space separated key value pairs, the format of which is defined in
7//! the [`BootConfig`] struct.
8//!
9//! The general syntax of the configuration file is not too dissimilar from that of BLS configuration files that
10//! come with systemd-boot.
11//!
12//! Example configuration:
13//!
14//! ```text
15//! # Adjusts the time for the default boot option to be picked
16//! timeout 10
17//!
18//! # Selects the default boot option through its index on the boot list
19//! default 3
20//!
21//! # Change the path where drivers are searched
22//! driver_path /EFI/Drivers
23//!
24//! # Enable or disable the builtin editor provided with the default frontend
25//! editor true
26//!
27//! # Enable or disable PXE boot discovery
28//! pxe true
29//!
30//! # Change the colors of the application
31//! bg magenta
32//! fg light_yellow
33//! highlight_bg gray
34//! highlight_fg black
35//! ```
36//!
37//! Frontends are not strictly obligated to honor the theming, default, and timeout settings.
38//! They exist as a way to signal user settings to the frontend, and the frontend can choose
39//! to implement those settings if needed or possible.
40//!
41//! Note that colors are stored as UEFI [`Color`]. Therefore, a frontend may need to convert
42//! from this color type.
43
44use alloc::{borrow::ToOwned, string::String};
45use uefi::{CStr16, Status, cstr16, proto::console::text::Color};
46
47use crate::{
48    BootResult,
49    system::{
50        fs::{FsError, UefiFileSystem},
51        helper::normalize_path,
52    },
53};
54
55/// The hardcoded configuration path for the [`BootConfig`].
56const CONFIG_PATH: &CStr16 = cstr16!("\\loader\\bootmgr-rs.conf");
57
58/// The configuration file for the bootloader.
59pub struct BootConfig {
60    /// The timeout for the bootloader before the default boot option is selected.
61    pub timeout: i64,
62
63    /// The default boot option as the index of the entry.
64    pub default: Option<usize>,
65
66    /// Whether loading drivers is enabled or not.
67    pub drivers: bool,
68
69    /// The path to the drivers in the same filesystem as the bootloader.
70    pub driver_path: String,
71
72    /// Allows for the editor to be enabled, if there is one.
73    pub editor: bool,
74
75    /// Allows for the basic PXE/TFTP loader to be enabled.
76    pub pxe: bool,
77
78    /// Allows adjusting the background of the UI.
79    pub bg: Color,
80
81    /// Allows adjusting the foreground of the UI.
82    pub fg: Color,
83
84    /// Allows adjusting the background of the highlighter.
85    pub highlight_bg: Color,
86
87    /// Allows adjusting the foreground of the highlighter.
88    pub highlight_fg: Color,
89}
90
91impl BootConfig {
92    /// Creates a new [`BootConfig`].
93    ///
94    /// # Errors
95    ///
96    /// May return an `Error` if the image handle from which this program was loaded from
97    /// does not support [`uefi::proto::media::fs::SimpleFileSystem`]. Otherwise, it will
98    /// return an empty [`BootConfig`].
99    pub(super) fn new() -> BootResult<Self> {
100        let mut fs = UefiFileSystem::from_image_fs()?;
101
102        let mut buf = [0; 4096]; // a config file over 4096 bytes is very unusual and is not supported
103        let bytes = match fs.read_into(CONFIG_PATH, &mut buf) {
104            Ok(bytes) => bytes,
105            Err(FsError::OpenErr(Status::NOT_FOUND)) => return Ok(Self::default()),
106            Err(e) => return Err(e.into()),
107        };
108
109        Ok(Self::get_boot_config(&buf, Some(bytes)))
110    }
111
112    /// Parses the contents of a [`BootConfig`] format string.
113    #[must_use = "Has no effect if the result is unused"]
114    pub fn get_boot_config(content: &[u8], bytes: Option<usize>) -> Self {
115        let mut config = Self::default();
116        let slice = &content[0..bytes.unwrap_or(content.len()).min(content.len())];
117
118        #[cfg(not(test))]
119        if let Some(timeout) = super::bli::get_timeout_var() {
120            config.timeout = timeout;
121        }
122
123        if let Ok(content) = str::from_utf8(slice) {
124            for line in content.lines() {
125                let line = line.trim();
126                if line.is_empty() || line.starts_with('#') {
127                    continue;
128                }
129
130                config.assign_to_field(line);
131            }
132        }
133
134        config
135    }
136
137    /// Assign a field to the [`BootConfig`] given a line containing the key and value.
138    fn assign_to_field(&mut self, line: &str) {
139        if let Some((key, value)) = line.split_once(' ') {
140            let value = value.trim().to_owned();
141            match &*key.to_ascii_lowercase() {
142                "timeout" => {
143                    if let Ok(value) = value.parse() {
144                        self.timeout = value;
145
146                        #[cfg(not(test))]
147                        let _ = super::bli::set_timeout_var(value);
148                    }
149                }
150                "default" => {
151                    if let Ok(value) = value.parse() {
152                        self.default = Some(value);
153                    }
154                }
155                "drivers" => {
156                    if let Ok(value) = value.parse() {
157                        self.drivers = value;
158                    }
159                }
160                "driver_path" => {
161                    let value = normalize_path(&value);
162                    self.driver_path = value;
163                }
164                "editor" => {
165                    if let Ok(value) = value.parse() {
166                        self.editor = value;
167                    }
168                }
169                "pxe" => {
170                    if let Ok(value) = value.parse() {
171                        self.pxe = value;
172                    }
173                }
174                "background" => self.bg = match_str_color_bg(&value),
175                "foreground" => self.fg = match_str_color_fg(&value),
176                "highlight_background" => self.highlight_bg = match_str_color_bg(&value),
177                "highlight_foreground" => self.highlight_fg = match_str_color_fg(&value),
178                _ => (),
179            }
180        }
181    }
182}
183
184impl Default for BootConfig {
185    fn default() -> Self {
186        Self {
187            timeout: 5,
188            default: None,
189            drivers: false,
190            driver_path: "\\EFI\\BOOT\\drivers".to_owned(),
191            editor: false,
192            pxe: false,
193            bg: Color::Black,
194            fg: Color::White,
195            highlight_bg: Color::LightGray,
196            highlight_fg: Color::Black,
197        }
198    }
199}
200
201/// Returns a foreground color given a color's string representation.
202///
203/// Any unrecognized colors will return [`Color::Black`].
204fn match_str_color_fg(color: &str) -> Color {
205    match color {
206        "red" => Color::Red,
207        "green" => Color::Green,
208        "yellow" => Color::Yellow,
209        "blue" => Color::Blue,
210        "magenta" => Color::Magenta,
211        "cyan" => Color::Cyan,
212        "gray" => Color::LightGray,
213        "dark_gray" => Color::DarkGray,
214        "light_red" => Color::LightRed,
215        "light_green" => Color::LightGreen,
216        "light_blue" => Color::LightBlue,
217        "light_magenta" => Color::LightMagenta,
218        "light_cyan" => Color::LightCyan,
219        "white" => Color::White,
220        _ => Color::Black,
221    }
222}
223
224/// Returns a background color given a color's string representation.
225///
226/// The pool of colors is significantly less than foreground, and any unrecognized colors
227/// will also return [`Color::Black`].
228fn match_str_color_bg(color: &str) -> Color {
229    match color {
230        "blue" => Color::Blue,
231        "green" => Color::Green,
232        "cyan" => Color::Cyan,
233        "red" => Color::Red,
234        "magenta" => Color::Magenta,
235        "gray" | "white" => Color::LightGray, // close enough
236        _ => Color::Black,
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use proptest::prelude::*;
244
245    /// # Panics
246    ///
247    /// May panic if the assertions fail.
248    #[test]
249    fn test_full_config() {
250        let config = b"
251            timeout 100
252            default 2
253            driver_path /efi/drivers
254            editor true
255            pxe false
256            background gray
257            foreground white
258            highlight_background black
259            highlight_foreground white
260        ";
261
262        let config = BootConfig::get_boot_config(config, None);
263        assert_eq!(config.timeout, 100);
264        assert_eq!(config.default, Some(2));
265        assert_eq!(config.driver_path, "\\efi\\drivers".to_owned());
266        assert!(config.editor);
267        assert!(!config.pxe);
268        assert!(matches!(config.bg, Color::LightGray));
269        assert!(matches!(config.fg, Color::White));
270        assert!(matches!(config.highlight_bg, Color::Black));
271        assert!(matches!(config.highlight_fg, Color::White));
272    }
273
274    proptest! {
275        #[test]
276        fn doesnt_panic(x in any::<Vec<u8>>(), y in any::<usize>()) {
277            let _ = BootConfig::get_boot_config(&x, Some(y));
278        }
279    }
280}