feat(bls): parse loader.conf

This commit is contained in:
alex authored and Alex Zenla committed 2026-10-03 17:39:38 -07:00
1 parent 1207f1d4e9
commit 5c517473e3
2 files changed
+164

No files matched your search

+79
View File
@@ -7,6 +7,10 @@ use alloc::vec::Vec;
use anyhow::{Error, Result};
use core::{cmp::Ordering, iter::Peekable, str::FromStr};
mod loader_conf;
pub use loader_conf::{LoaderConf, LoaderTimeout};
/// Represents a parsed BLS entry.
/// Fields unrelated to Sprout are not included.
#[derive(Default, Debug, Clone)]
@@ -579,6 +583,81 @@ mod tests {
assert!(!entry.is_bad());
}
#[test]
fn loader_conf_empty_has_no_settings() {
let conf = LoaderConf::parse("");
assert_eq!(conf, LoaderConf::default());
assert!(conf.warnings.is_empty());
}
#[test]
fn loader_conf_reads_default_and_timeout() {
let conf = LoaderConf::parse("default fedora-*\ntimeout 7\n");
assert_eq!(conf.default.as_deref(), Some("fedora-*"));
assert_eq!(conf.timeout, Some(LoaderTimeout::Seconds(7)));
assert!(conf.warnings.is_empty());
}
#[test]
fn loader_conf_skips_comments_blank_lines_and_bom() {
let conf = LoaderConf::parse("\u{feff}# a comment\n\n \n default arch.conf \n");
assert_eq!(conf.default.as_deref(), Some("arch.conf"));
assert!(conf.warnings.is_empty());
}
#[test]
fn loader_conf_last_value_wins() {
let conf = LoaderConf::parse("default a\ndefault b\ntimeout 1\ntimeout 2\n");
assert_eq!(conf.default.as_deref(), Some("b"));
assert_eq!(conf.timeout, Some(LoaderTimeout::Seconds(2)));
}
#[test]
fn loader_conf_timeout_words() {
for (value, expected) in [
("0", LoaderTimeout::Seconds(0)),
("menu-hidden", LoaderTimeout::Hidden),
("menu-disabled", LoaderTimeout::Disabled),
("menu-force", LoaderTimeout::Force),
] {
let conf = LoaderConf::parse(&format!("timeout {value}\n"));
assert_eq!(conf.timeout, Some(expected), "{value}");
}
}
#[test]
fn loader_conf_invalid_timeout_is_ignored_with_a_warning() {
for value in ["soon", "-1", "99999999999999999999999"] {
let conf = LoaderConf::parse(&format!("timeout {value}\n"));
assert_eq!(conf.timeout, None, "{value}");
assert_eq!(conf.warnings.len(), 1, "{value}");
}
}
#[test]
fn loader_conf_keys_without_values_are_ignored_with_a_warning() {
let conf = LoaderConf::parse("default\ntimeout\n");
assert_eq!(conf.default, None);
assert_eq!(conf.timeout, None);
assert_eq!(conf.warnings.len(), 2);
}
#[test]
fn loader_conf_warns_about_unsupported_keys() {
let conf = LoaderConf::parse("editor no\nentry-token machine-id\nconsole-mode max\n");
assert_eq!(conf.warnings.len(), 3);
assert!(
conf.warnings
.iter()
.any(|warning| warning.contains("editor"))
);
assert!(
conf.warnings
.iter()
.any(|warning| warning.contains("entry-token"))
);
}
#[test]
fn default_flags_keep_a_good_default() {
// The second entry is bad and also matched the default pattern.
+85
View File
@@ -0,0 +1,85 @@
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
/// The menu timeout of a `loader.conf` file.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoaderTimeout {
/// Show the menu for this many seconds. Zero hides the menu.
Seconds(u64),
/// Hide the menu unless a key is held.
Hidden,
/// Never show the menu.
Disabled,
/// Always show the menu and wait for the user.
Force,
}
impl LoaderTimeout {
/// Parses a `timeout` value, which is a number of seconds or one of the menu words.
fn parse(value: &str) -> Option<Self> {
match value {
"menu-hidden" => Some(Self::Hidden),
"menu-disabled" => Some(Self::Disabled),
"menu-force" => Some(Self::Force),
_ if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
value.parse().ok().map(Self::Seconds)
}
_ => None,
}
}
}
/// The settings Sprout understands from a `loader.conf` file.
/// Reference: <https://www.freedesktop.org/software/systemd/man/latest/loader.conf.html>
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct LoaderConf {
/// A glob pattern that selects the default entry, or `@saved`.
pub default: Option<String>,
/// How long to show the boot menu.
pub timeout: Option<LoaderTimeout>,
/// Problems found while parsing, such as keys that are not supported.
pub warnings: Vec<String>,
}
impl LoaderConf {
/// Parses the `input` as a `loader.conf` file. Parsing never fails, as a bad setting
/// should not prevent booting. Anything that is ignored is described in `warnings`.
/// When a key appears more than once, the last value is used.
pub fn parse(input: &str) -> Self {
let mut conf = Self::default();
// Some editors write a UTF-8 byte order mark at the start of the file.
let input = input.strip_prefix('\u{feff}').unwrap_or(input);
for line in input.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
let (key, value) = match line.split_once(char::is_whitespace) {
Some((key, value)) => (key, value.trim()),
None => (line, ""),
};
match key {
"default" if !value.is_empty() => conf.default = Some(value.to_string()),
"timeout" => match LoaderTimeout::parse(value) {
Some(timeout) => conf.timeout = Some(timeout),
None => conf
.warnings
.push(format!("ignoring invalid loader.conf timeout '{}'", value)),
},
"default" => conf
.warnings
.push("ignoring loader.conf default without a value".to_string()),
_ => conf
.warnings
.push(format!("ignoring unsupported loader.conf key '{}'", key)),
}
}
conf
}
}