2
2
//!
3
3
//! This module parses the config files for the spec.
4
4
5
- use anyhow:: Result ;
6
- use serde:: de:: Error ;
7
- use serde:: { Deserialize , Deserializer } ;
5
+ use anyhow:: { anyhow, Result } ;
8
6
use std:: collections:: HashMap ;
9
7
use std:: fmt:: Display ;
10
8
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]
12
16
pub ( crate ) struct BLSConfig {
17
+ /// The title of the boot entry, to be displayed in the boot menu.
13
18
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.
16
23
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.
21
34
pub ( crate ) extra : HashMap < String , String > ,
22
35
}
23
36
24
- impl PartialEq for BLSConfig {
25
- fn eq ( & self , other : & Self ) -> bool {
26
- self . version == other. version
27
- }
28
- }
29
-
30
37
impl PartialOrd for BLSConfig {
31
38
fn partial_cmp ( & self , other : & Self ) -> Option < std:: cmp:: Ordering > {
32
- self . version . partial_cmp ( & other. version )
39
+ Some ( self . cmp ( other) )
33
40
}
34
41
}
35
42
36
43
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.
37
48
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 ( )
39
68
}
40
69
}
41
70
@@ -47,8 +76,18 @@ impl Display for BLSConfig {
47
76
48
77
writeln ! ( f, "version {}" , self . version) ?;
49
78
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
+ }
52
91
53
92
for ( key, value) in & self . extra {
54
93
writeln ! ( f, "{} {}" , key, value) ?;
@@ -58,20 +97,15 @@ impl Display for BLSConfig {
58
97
}
59
98
}
60
99
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
-
73
100
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 ( ) ;
75
109
76
110
for line in input. lines ( ) {
77
111
let line = line. trim ( ) ;
@@ -80,14 +114,35 @@ pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
80
114
}
81
115
82
116
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
+ }
84
130
}
85
131
}
86
132
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
+ } )
91
146
}
92
147
93
148
#[ cfg( test) ]
@@ -112,16 +167,37 @@ mod tests {
112
167
config. title,
113
168
Some ( "Fedora 42.20250623.3.1 (CoreOS)" . to_string( ) )
114
169
) ;
115
- assert_eq ! ( config. version, 2 ) ;
170
+ assert_eq ! ( config. version, "2" ) ;
116
171
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 ( ) ) ) ;
119
174
assert_eq ! ( config. extra. get( "custom1" ) , Some ( & "value1" . to_string( ) ) ) ;
120
175
assert_eq ! ( config. extra. get( "custom2" ) , Some ( & "value2" . to_string( ) ) ) ;
121
176
122
177
Ok ( ( ) )
123
178
}
124
179
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
+
125
201
#[ test]
126
202
fn test_parse_missing_version ( ) {
127
203
let input = r#"
@@ -136,13 +212,12 @@ mod tests {
136
212
}
137
213
138
214
#[ test]
139
- fn test_parse_invalid_version_format ( ) {
215
+ fn test_parse_missing_linux ( ) {
140
216
let input = r#"
141
217
title Fedora
142
- version not_an_int
143
- linux /vmlinuz
218
+ version 1
144
219
initrd /initramfs.img
145
- options root=UUID=abc composefs=some-uuid
220
+ options root=UUID=xyz ro quiet
146
221
"# ;
147
222
148
223
let parsed = parse_bls_config ( input) ;
@@ -156,6 +231,7 @@ mod tests {
156
231
version 10
157
232
linux /boot/vmlinuz
158
233
initrd /boot/initrd.img
234
+ initrd /boot/initrd-extra.img
159
235
options root=UUID=abc composefs=some-uuid
160
236
foo bar
161
237
"# ;
@@ -168,6 +244,10 @@ mod tests {
168
244
assert_eq ! ( output_lines. next( ) . unwrap( ) , "version 10" ) ;
169
245
assert_eq ! ( output_lines. next( ) . unwrap( ) , "linux /boot/vmlinuz" ) ;
170
246
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
+ ) ;
171
251
assert_eq ! (
172
252
output_lines. next( ) . unwrap( ) ,
173
253
"options root=UUID=abc composefs=some-uuid"
@@ -178,11 +258,38 @@ mod tests {
178
258
}
179
259
180
260
#[ 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 < ( ) > {
182
288
let config1 = parse_bls_config (
183
289
r#"
184
290
title Entry 1
185
291
version 3
292
+ sort-key a
186
293
linux /vmlinuz-3
187
294
initrd /initrd-3
188
295
options opt1
@@ -193,6 +300,7 @@ mod tests {
193
300
r#"
194
301
title Entry 2
195
302
version 5
303
+ sort-key b
196
304
linux /vmlinuz-5
197
305
initrd /initrd-5
198
306
options opt2
@@ -202,4 +310,88 @@ mod tests {
202
310
assert ! ( config1 < config2) ;
203
311
Ok ( ( ) )
204
312
}
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
+ }
205
397
}
0 commit comments