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 } ;
86use std:: collections:: HashMap ;
97use 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]
1216pub ( 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
3042impl 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
3648impl 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-
73104pub ( 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