Skip to content

Commit cbb6eaa

Browse files
committed
bls_config: Rework to be more spec compliant
- Handle multiple initrd entries. - Add support for machine-id and sort-key. To do this we can't just map the keys into JSON because we need to handle multiple values. Switch to a manual parser. Assisted-by: Gemini CLI+gemini-2.5-pro Signed-off-by: Colin Walters <[email protected]>
1 parent 1a6e914 commit cbb6eaa

File tree

1 file changed

+238
-46
lines changed

1 file changed

+238
-46
lines changed

crates/lib/src/parsers/bls_config.rs

Lines changed: 238 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,69 @@
22
//!
33
//! This module parses the config files for the spec.
44
5-
use anyhow::Result;
6-
use serde::de::Error;
7-
use serde::{Deserialize, Deserializer};
5+
use anyhow::{anyhow, Result};
86
use std::collections::HashMap;
97
use std::fmt::Display;
108

11-
#[derive(Debug, Deserialize, Eq)]
9+
/// Represents a single Boot Loader Specification config file.
10+
///
11+
/// The boot loader should present the available boot menu entries to the user in a sorted list.
12+
/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
13+
/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
14+
#[derive(Debug, Eq, PartialEq)]
15+
#[non_exhaustive]
1216
pub(crate) struct BLSConfig {
17+
/// The title of the boot entry, to be displayed in the boot menu.
1318
pub(crate) title: Option<String>,
14-
#[serde(deserialize_with = "deserialize_version")]
15-
pub(crate) version: u32,
19+
/// The version of the boot entry.
20+
/// See <https://uapi-group.org/specifications/specs/version_format_specification/>
21+
pub(crate) version: String,
22+
/// The path to the linux kernel to boot.
1623
pub(crate) linux: String,
17-
pub(crate) initrd: String,
18-
pub(crate) options: String,
19-
20-
#[serde(flatten)]
24+
/// The paths to the initrd images.
25+
pub(crate) initrd: Vec<String>,
26+
/// Kernel command line options.
27+
pub(crate) options: Option<String>,
28+
/// The machine ID of the OS.
29+
pub(crate) machine_id: Option<String>,
30+
/// The sort key for the boot menu.
31+
pub(crate) sort_key: Option<String>,
32+
33+
/// Any extra fields not defined in the spec.
2134
pub(crate) extra: HashMap<String, String>,
2235
}
2336

24-
impl PartialEq for BLSConfig {
25-
fn eq(&self, other: &Self) -> bool {
26-
self.version == other.version
27-
}
28-
}
29-
3037
impl PartialOrd for BLSConfig {
3138
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
32-
self.version.partial_cmp(&other.version)
39+
Some(self.cmp(other))
3340
}
3441
}
3542

3643
impl Ord for BLSConfig {
44+
/// This implements the sorting logic from the Boot Loader Specification.
45+
///
46+
/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
47+
/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
3748
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
38-
self.version.cmp(&other.version)
49+
// If both configs have a sort key, compare them.
50+
if let (Some(key1), Some(key2)) = (&self.sort_key, &other.sort_key) {
51+
let ord = key1.cmp(key2);
52+
if ord != std::cmp::Ordering::Equal {
53+
return ord;
54+
}
55+
}
56+
57+
// If both configs have a machine ID, compare them.
58+
if let (Some(id1), Some(id2)) = (&self.machine_id, &other.machine_id) {
59+
let ord = id1.cmp(id2);
60+
if ord != std::cmp::Ordering::Equal {
61+
return ord;
62+
}
63+
}
64+
65+
// Finally, sort by version in descending order.
66+
// FIXME: This should use <https://uapi-group.org/specifications/specs/version_format_specification/>
67+
self.version.cmp(&other.version).reverse()
3968
}
4069
}
4170

@@ -47,8 +76,18 @@ impl Display for BLSConfig {
4776

4877
writeln!(f, "version {}", self.version)?;
4978
writeln!(f, "linux {}", self.linux)?;
50-
writeln!(f, "initrd {}", self.initrd)?;
51-
writeln!(f, "options {}", self.options)?;
79+
for initrd in self.initrd.iter() {
80+
writeln!(f, "initrd {}", initrd)?;
81+
}
82+
if let Some(options) = self.options.as_deref() {
83+
writeln!(f, "options {}", options)?;
84+
}
85+
if let Some(machine_id) = self.machine_id.as_deref() {
86+
writeln!(f, "machine-id {}", machine_id)?;
87+
}
88+
if let Some(sort_key) = self.sort_key.as_deref() {
89+
writeln!(f, "sort-key {}", sort_key)?;
90+
}
5291

5392
for (key, value) in &self.extra {
5493
writeln!(f, "{} {}", key, value)?;
@@ -58,20 +97,15 @@ impl Display for BLSConfig {
5897
}
5998
}
6099

61-
fn deserialize_version<'de, D>(deserializer: D) -> Result<u32, D::Error>
62-
where
63-
D: Deserializer<'de>,
64-
{
65-
let s: Option<String> = Option::deserialize(deserializer)?;
66-
67-
match s {
68-
Some(s) => Ok(s.parse::<u32>().map_err(D::Error::custom)?),
69-
None => Err(D::Error::custom("Version not found")),
70-
}
71-
}
72-
73100
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
74-
let mut map = HashMap::new();
101+
let mut title = None;
102+
let mut version = None;
103+
let mut linux = None;
104+
let mut initrd = Vec::new();
105+
let mut options = None;
106+
let mut machine_id = None;
107+
let mut sort_key = None;
108+
let mut extra = HashMap::new();
75109

76110
for line in input.lines() {
77111
let line = line.trim();
@@ -80,14 +114,35 @@ pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
80114
}
81115

82116
if let Some((key, value)) = line.split_once(' ') {
83-
map.insert(key.to_string(), value.trim().to_string());
117+
let value = value.trim().to_string();
118+
match key {
119+
"title" => title = Some(value),
120+
"version" => version = Some(value),
121+
"linux" => linux = Some(value),
122+
"initrd" => initrd.push(value),
123+
"options" => options = Some(value),
124+
"machine-id" => machine_id = Some(value),
125+
"sort-key" => sort_key = Some(value),
126+
_ => {
127+
extra.insert(key.to_string(), value);
128+
}
129+
}
84130
}
85131
}
86132

87-
let value = serde_json::to_value(map)?;
88-
let parsed: BLSConfig = serde_json::from_value(value)?;
89-
90-
Ok(parsed)
133+
let linux = linux.ok_or_else(|| anyhow!("Missing 'linux' value"))?;
134+
let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?;
135+
136+
Ok(BLSConfig {
137+
title,
138+
version,
139+
linux,
140+
initrd,
141+
options,
142+
machine_id,
143+
sort_key,
144+
extra,
145+
})
91146
}
92147

93148
#[cfg(test)]
@@ -112,16 +167,37 @@ mod tests {
112167
config.title,
113168
Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
114169
);
115-
assert_eq!(config.version, 2);
170+
assert_eq!(config.version, "2");
116171
assert_eq!(config.linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10");
117-
assert_eq!(config.initrd, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img");
118-
assert_eq!(config.options, "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6");
172+
assert_eq!(config.initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]);
173+
assert_eq!(config.options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string()));
119174
assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
120175
assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
121176

122177
Ok(())
123178
}
124179

180+
#[test]
181+
fn test_parse_multiple_initrd() -> Result<()> {
182+
let input = r#"
183+
title Fedora 42.20250623.3.1 (CoreOS)
184+
version 2
185+
linux /boot/vmlinuz
186+
initrd /boot/initramfs-1.img
187+
initrd /boot/initramfs-2.img
188+
options root=UUID=abc123 rw
189+
"#;
190+
191+
let config = parse_bls_config(input)?;
192+
193+
assert_eq!(
194+
config.initrd,
195+
vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
196+
);
197+
198+
Ok(())
199+
}
200+
125201
#[test]
126202
fn test_parse_missing_version() {
127203
let input = r#"
@@ -136,13 +212,12 @@ mod tests {
136212
}
137213

138214
#[test]
139-
fn test_parse_invalid_version_format() {
215+
fn test_parse_missing_linux() {
140216
let input = r#"
141217
title Fedora
142-
version not_an_int
143-
linux /vmlinuz
218+
version 1
144219
initrd /initramfs.img
145-
options root=UUID=abc composefs=some-uuid
220+
options root=UUID=xyz ro quiet
146221
"#;
147222

148223
let parsed = parse_bls_config(input);
@@ -156,6 +231,7 @@ mod tests {
156231
version 10
157232
linux /boot/vmlinuz
158233
initrd /boot/initrd.img
234+
initrd /boot/initrd-extra.img
159235
options root=UUID=abc composefs=some-uuid
160236
foo bar
161237
"#;
@@ -168,6 +244,10 @@ mod tests {
168244
assert_eq!(output_lines.next().unwrap(), "version 10");
169245
assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
170246
assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
247+
assert_eq!(
248+
output_lines.next().unwrap(),
249+
"initrd /boot/initrd-extra.img"
250+
);
171251
assert_eq!(
172252
output_lines.next().unwrap(),
173253
"options root=UUID=abc composefs=some-uuid"
@@ -178,11 +258,38 @@ mod tests {
178258
}
179259

180260
#[test]
181-
fn test_ordering() -> Result<()> {
261+
fn test_ordering_by_version() -> Result<()> {
262+
let config1 = parse_bls_config(
263+
r#"
264+
title Entry 1
265+
version 3
266+
linux /vmlinuz-3
267+
initrd /initrd-3
268+
options opt1
269+
"#,
270+
)?;
271+
272+
let config2 = parse_bls_config(
273+
r#"
274+
title Entry 2
275+
version 5
276+
linux /vmlinuz-5
277+
initrd /initrd-5
278+
options opt2
279+
"#,
280+
)?;
281+
282+
assert!(config1 > config2);
283+
Ok(())
284+
}
285+
286+
#[test]
287+
fn test_ordering_by_sort_key() -> Result<()> {
182288
let config1 = parse_bls_config(
183289
r#"
184290
title Entry 1
185291
version 3
292+
sort-key a
186293
linux /vmlinuz-3
187294
initrd /initrd-3
188295
options opt1
@@ -193,6 +300,7 @@ mod tests {
193300
r#"
194301
title Entry 2
195302
version 5
303+
sort-key b
196304
linux /vmlinuz-5
197305
initrd /initrd-5
198306
options opt2
@@ -202,4 +310,88 @@ mod tests {
202310
assert!(config1 < config2);
203311
Ok(())
204312
}
313+
314+
#[test]
315+
fn test_ordering_by_sort_key_and_version() -> Result<()> {
316+
let config1 = parse_bls_config(
317+
r#"
318+
title Entry 1
319+
version 3
320+
sort-key a
321+
linux /vmlinuz-3
322+
initrd /initrd-3
323+
options opt1
324+
"#,
325+
)?;
326+
327+
let config2 = parse_bls_config(
328+
r#"
329+
title Entry 2
330+
version 5
331+
sort-key a
332+
linux /vmlinuz-5
333+
initrd /initrd-5
334+
options opt2
335+
"#,
336+
)?;
337+
338+
assert!(config1 > config2);
339+
Ok(())
340+
}
341+
342+
#[test]
343+
fn test_ordering_by_machine_id() -> Result<()> {
344+
let config1 = parse_bls_config(
345+
r#"
346+
title Entry 1
347+
version 3
348+
machine-id a
349+
linux /vmlinuz-3
350+
initrd /initrd-3
351+
options opt1
352+
"#,
353+
)?;
354+
355+
let config2 = parse_bls_config(
356+
r#"
357+
title Entry 2
358+
version 5
359+
machine-id b
360+
linux /vmlinuz-5
361+
initrd /initrd-5
362+
options opt2
363+
"#,
364+
)?;
365+
366+
assert!(config1 < config2);
367+
Ok(())
368+
}
369+
370+
#[test]
371+
fn test_ordering_by_machine_id_and_version() -> Result<()> {
372+
let config1 = parse_bls_config(
373+
r#"
374+
title Entry 1
375+
version 3
376+
machine-id a
377+
linux /vmlinuz-3
378+
initrd /initrd-3
379+
options opt1
380+
"#,
381+
)?;
382+
383+
let config2 = parse_bls_config(
384+
r#"
385+
title Entry 2
386+
version 5
387+
machine-id a
388+
linux /vmlinuz-5
389+
initrd /initrd-5
390+
options opt2
391+
"#,
392+
)?;
393+
394+
assert!(config1 > config2);
395+
Ok(())
396+
}
205397
}

0 commit comments

Comments
 (0)