Skip to content

Commit 2feb86f

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 <walters@verbum.org>
1 parent 3d8d75c commit 2feb86f

1 file changed

Lines changed: 236 additions & 40 deletions

File tree

crates/lib/src/parsers/bls_config.rs

Lines changed: 236 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,34 @@
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)]
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+
pub(crate) version: String,
21+
/// The path to the linux kernel to boot.
1622
pub(crate) linux: String,
17-
pub(crate) initrd: String,
18-
pub(crate) options: String,
19-
20-
#[serde(flatten)]
23+
/// The paths to the initrd images.
24+
pub(crate) initrd: Vec<String>,
25+
/// Kernel command line options.
26+
pub(crate) options: Option<String>,
27+
/// The machine ID of the OS.
28+
pub(crate) machine_id: Option<String>,
29+
/// The sort key for the boot menu.
30+
pub(crate) sort_key: Option<String>,
31+
32+
/// Any extra fields not defined in the spec.
2133
pub(crate) extra: HashMap<String, String>,
2234
}
2335

@@ -29,13 +41,34 @@ impl PartialEq for BLSConfig {
2941

3042
impl PartialOrd for BLSConfig {
3143
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
32-
self.version.partial_cmp(&other.version)
44+
Some(self.cmp(other))
3345
}
3446
}
3547

3648
impl Ord for BLSConfig {
49+
/// This implements the sorting logic from the Boot Loader Specification.
50+
///
51+
/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
52+
/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
3753
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
38-
self.version.cmp(&other.version)
54+
// If both configs have a sort key, compare them.
55+
if let (Some(key1), Some(key2)) = (&self.sort_key, &other.sort_key) {
56+
let ord = key1.cmp(key2);
57+
if ord != std::cmp::Ordering::Equal {
58+
return ord;
59+
}
60+
}
61+
62+
// If both configs have a machine ID, compare them.
63+
if let (Some(id1), Some(id2)) = (&self.machine_id, &other.machine_id) {
64+
let ord = id1.cmp(id2);
65+
if ord != std::cmp::Ordering::Equal {
66+
return ord;
67+
}
68+
}
69+
70+
// Finally, sort by version in descending order.
71+
self.version.cmp(&other.version).reverse()
3972
}
4073
}
4174

@@ -47,8 +80,18 @@ impl Display for BLSConfig {
4780

4881
writeln!(f, "version {}", self.version)?;
4982
writeln!(f, "linux {}", self.linux)?;
50-
writeln!(f, "initrd {}", self.initrd)?;
51-
writeln!(f, "options {}", self.options)?;
83+
for initrd in self.initrd.iter() {
84+
writeln!(f, "initrd {}", initrd)?;
85+
}
86+
if let Some(options) = self.options.as_deref() {
87+
writeln!(f, "options {}", options)?;
88+
}
89+
if let Some(machine_id) = self.machine_id.as_deref() {
90+
writeln!(f, "machine-id {}", machine_id)?;
91+
}
92+
if let Some(sort_key) = self.sort_key.as_deref() {
93+
writeln!(f, "sort-key {}", sort_key)?;
94+
}
5295

5396
for (key, value) in &self.extra {
5497
writeln!(f, "{} {}", key, value)?;
@@ -58,20 +101,15 @@ impl Display for BLSConfig {
58101
}
59102
}
60103

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-
73104
pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
74-
let mut map = HashMap::new();
105+
let mut title = None;
106+
let mut version = None;
107+
let mut linux = None;
108+
let mut initrd = Vec::new();
109+
let mut options = None;
110+
let mut machine_id = None;
111+
let mut sort_key = None;
112+
let mut extra = HashMap::new();
75113

76114
for line in input.lines() {
77115
let line = line.trim();
@@ -80,14 +118,35 @@ pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
80118
}
81119

82120
if let Some((key, value)) = line.split_once(' ') {
83-
map.insert(key.to_string(), value.trim().to_string());
121+
let value = value.trim().to_string();
122+
match key {
123+
"title" => title = Some(value),
124+
"version" => version = Some(value),
125+
"linux" => linux = Some(value),
126+
"initrd" => initrd.push(value),
127+
"options" => options = Some(value),
128+
"machine-id" => machine_id = Some(value),
129+
"sort-key" => sort_key = Some(value),
130+
_ => {
131+
extra.insert(key.to_string(), value);
132+
}
133+
}
84134
}
85135
}
86136

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

93152
#[cfg(test)]
@@ -112,16 +171,37 @@ mod tests {
112171
config.title,
113172
Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
114173
);
115-
assert_eq!(config.version, 2);
174+
assert_eq!(config.version, "2");
116175
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");
176+
assert_eq!(config.initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]);
177+
assert_eq!(config.options, Some("root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6".to_string()));
119178
assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
120179
assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
121180

122181
Ok(())
123182
}
124183

184+
#[test]
185+
fn test_parse_multiple_initrd() -> Result<()> {
186+
let input = r#"
187+
title Fedora 42.20250623.3.1 (CoreOS)
188+
version 2
189+
linux /boot/vmlinuz
190+
initrd /boot/initramfs-1.img
191+
initrd /boot/initramfs-2.img
192+
options root=UUID=abc123 rw
193+
"#;
194+
195+
let config = parse_bls_config(input)?;
196+
197+
assert_eq!(
198+
config.initrd,
199+
vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
200+
);
201+
202+
Ok(())
203+
}
204+
125205
#[test]
126206
fn test_parse_missing_version() {
127207
let input = r#"
@@ -136,13 +216,12 @@ mod tests {
136216
}
137217

138218
#[test]
139-
fn test_parse_invalid_version_format() {
219+
fn test_parse_missing_linux() {
140220
let input = r#"
141221
title Fedora
142-
version not_an_int
143-
linux /vmlinuz
222+
version 1
144223
initrd /initramfs.img
145-
options root=UUID=abc composefs=some-uuid
224+
options root=UUID=xyz ro quiet
146225
"#;
147226

148227
let parsed = parse_bls_config(input);
@@ -156,6 +235,7 @@ mod tests {
156235
version 10
157236
linux /boot/vmlinuz
158237
initrd /boot/initrd.img
238+
initrd /boot/initrd-extra.img
159239
options root=UUID=abc composefs=some-uuid
160240
foo bar
161241
"#;
@@ -168,6 +248,10 @@ mod tests {
168248
assert_eq!(output_lines.next().unwrap(), "version 10");
169249
assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
170250
assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
251+
assert_eq!(
252+
output_lines.next().unwrap(),
253+
"initrd /boot/initrd-extra.img"
254+
);
171255
assert_eq!(
172256
output_lines.next().unwrap(),
173257
"options root=UUID=abc composefs=some-uuid"
@@ -178,11 +262,94 @@ mod tests {
178262
}
179263

180264
#[test]
181-
fn test_ordering() -> Result<()> {
265+
fn test_ordering_by_version() -> Result<()> {
266+
let config1 = parse_bls_config(
267+
r#"
268+
title Entry 1
269+
version 3
270+
linux /vmlinuz-3
271+
initrd /initrd-3
272+
options opt1
273+
"#,
274+
)?;
275+
276+
let config2 = parse_bls_config(
277+
r#"
278+
title Entry 2
279+
version 5
280+
linux /vmlinuz-5
281+
initrd /initrd-5
282+
options opt2
283+
"#,
284+
)?;
285+
286+
assert!(config1 > config2);
287+
Ok(())
288+
}
289+
290+
#[test]
291+
fn test_ordering_by_sort_key() -> Result<()> {
292+
let config1 = parse_bls_config(
293+
r#"
294+
title Entry 1
295+
version 3
296+
sort-key a
297+
linux /vmlinuz-3
298+
initrd /initrd-3
299+
options opt1
300+
"#,
301+
)?;
302+
303+
let config2 = parse_bls_config(
304+
r#"
305+
title Entry 2
306+
version 5
307+
sort-key b
308+
linux /vmlinuz-5
309+
initrd /initrd-5
310+
options opt2
311+
"#,
312+
)?;
313+
314+
assert!(config1 < config2);
315+
Ok(())
316+
}
317+
318+
#[test]
319+
fn test_ordering_by_sort_key_and_version() -> Result<()> {
320+
let config1 = parse_bls_config(
321+
r#"
322+
title Entry 1
323+
version 3
324+
sort-key a
325+
linux /vmlinuz-3
326+
initrd /initrd-3
327+
options opt1
328+
"#,
329+
)?;
330+
331+
let config2 = parse_bls_config(
332+
r#"
333+
title Entry 2
334+
version 5
335+
sort-key a
336+
linux /vmlinuz-5
337+
initrd /initrd-5
338+
options opt2
339+
"#,
340+
)?;
341+
342+
assert!(config1 > config2);
343+
Ok(())
344+
}
345+
346+
#[test]
347+
fn test_ordering_by_machine_id() -> Result<()> {
182348
let config1 = parse_bls_config(
183349
r#"
184350
title Entry 1
185351
version 3
352+
machine-id a
186353
linux /vmlinuz-3
187354
initrd /initrd-3
188355
options opt1
@@ -193,6 +360,7 @@ mod tests {
193360
r#"
194361
title Entry 2
195362
version 5
363+
machine-id b
196364
linux /vmlinuz-5
197365
initrd /initrd-5
198366
options opt2
@@ -202,4 +370,32 @@ mod tests {
202370
assert!(config1 < config2);
203371
Ok(())
204372
}
373+
374+
#[test]
375+
fn test_ordering_by_machine_id_and_version() -> Result<()> {
376+
let config1 = parse_bls_config(
377+
r#"
378+
title Entry 1
379+
version 3
380+
machine-id a
381+
linux /vmlinuz-3
382+
initrd /initrd-3
383+
options opt1
384+
"#,
385+
)?;
386+
387+
let config2 = parse_bls_config(
388+
r#"
389+
title Entry 2
390+
version 5
391+
machine-id a
392+
linux /vmlinuz-5
393+
initrd /initrd-5
394+
options opt2
395+
"#,
396+
)?;
397+
398+
assert!(config1 > config2);
399+
Ok(())
400+
}
205401
}

0 commit comments

Comments
 (0)