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 , PartialEq ) ]
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+ /// 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-
3037impl 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
3643impl 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-
73100pub ( 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