1#![allow(dead_code)]
6
7use anyhow::{Result, anyhow};
8use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
9use camino::Utf8PathBuf;
10use composefs_boot::bootloader::EFI_EXT;
11use core::fmt;
12use std::collections::HashMap;
13use std::fmt::Display;
14use uapi_version::Version;
15
16use crate::composefs_consts::COMPOSEFS_CMDLINE;
17
18#[derive(Debug, PartialEq, Eq, Default)]
19pub enum BLSConfigType {
20 EFI {
21 efi: Utf8PathBuf,
23 },
24 NonEFI {
25 linux: Utf8PathBuf,
27 initrd: Vec<Utf8PathBuf>,
29 options: Option<CmdlineOwned>,
31 },
32 #[default]
33 Unknown,
34}
35
36#[derive(Debug, Eq, PartialEq, Default)]
42#[non_exhaustive]
43pub(crate) struct BLSConfig {
44 pub(crate) title: Option<String>,
46 version: String,
51
52 pub(crate) cfg_type: BLSConfigType,
53
54 pub(crate) machine_id: Option<String>,
56 pub(crate) sort_key: Option<String>,
58
59 pub(crate) extra: HashMap<String, String>,
61}
62
63impl PartialOrd for BLSConfig {
64 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
65 Some(self.cmp(other))
66 }
67}
68
69impl Ord for BLSConfig {
70 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
75 if let (Some(key1), Some(key2)) = (&self.sort_key, &other.sort_key) {
77 let ord = key1.cmp(key2);
78 if ord != std::cmp::Ordering::Equal {
79 return ord;
80 }
81 }
82
83 if let (Some(id1), Some(id2)) = (&self.machine_id, &other.machine_id) {
85 let ord = id1.cmp(id2);
86 if ord != std::cmp::Ordering::Equal {
87 return ord;
88 }
89 }
90
91 self.version().cmp(&other.version()).reverse()
93 }
94}
95
96impl Display for BLSConfig {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 if let Some(title) = &self.title {
99 writeln!(f, "title {}", title)?;
100 }
101
102 writeln!(f, "version {}", self.version)?;
103
104 match &self.cfg_type {
105 BLSConfigType::EFI { efi } => {
106 writeln!(f, "efi {}", efi)?;
107 }
108
109 BLSConfigType::NonEFI {
110 linux,
111 initrd,
112 options,
113 } => {
114 writeln!(f, "linux {}", linux)?;
115 for initrd in initrd.iter() {
116 writeln!(f, "initrd {}", initrd)?;
117 }
118
119 if let Some(options) = options.as_deref() {
120 writeln!(f, "options {}", options)?;
121 }
122 }
123
124 BLSConfigType::Unknown => return Err(fmt::Error),
125 }
126
127 if let Some(machine_id) = self.machine_id.as_deref() {
128 writeln!(f, "machine-id {}", machine_id)?;
129 }
130 if let Some(sort_key) = self.sort_key.as_deref() {
131 writeln!(f, "sort-key {}", sort_key)?;
132 }
133
134 for (key, value) in &self.extra {
135 writeln!(f, "{} {}", key, value)?;
136 }
137
138 Ok(())
139 }
140}
141
142impl BLSConfig {
143 pub(crate) fn version(&self) -> Version {
144 Version::from(&self.version)
145 }
146
147 pub(crate) fn with_title(&mut self, new_val: String) -> &mut Self {
148 self.title = Some(new_val);
149 self
150 }
151 pub(crate) fn with_version(&mut self, new_val: String) -> &mut Self {
152 self.version = new_val;
153 self
154 }
155 pub(crate) fn with_cfg(&mut self, config: BLSConfigType) -> &mut Self {
156 self.cfg_type = config;
157 self
158 }
159 #[allow(dead_code)]
160 pub(crate) fn with_machine_id(&mut self, new_val: String) -> &mut Self {
161 self.machine_id = Some(new_val);
162 self
163 }
164 pub(crate) fn with_sort_key(&mut self, new_val: String) -> &mut Self {
165 self.sort_key = Some(new_val);
166 self
167 }
168 #[allow(dead_code)]
169 pub(crate) fn with_extra(&mut self, new_val: HashMap<String, String>) -> &mut Self {
170 self.extra = new_val;
171 self
172 }
173
174 pub(crate) fn get_verity(&self) -> Result<String> {
175 match &self.cfg_type {
176 BLSConfigType::EFI { efi } => Ok(efi
177 .components()
178 .last()
179 .ok_or(anyhow::anyhow!("Empty efi field"))?
180 .to_string()
181 .strip_suffix(EFI_EXT)
182 .ok_or(anyhow::anyhow!("efi doesn't end with .efi"))?
183 .to_string()),
184
185 BLSConfigType::NonEFI { options, .. } => {
186 let options = options.as_ref().ok_or(anyhow::anyhow!("No options"))?;
187
188 let cmdline = Cmdline::from(&options);
189
190 let kv = cmdline
191 .find(COMPOSEFS_CMDLINE)
192 .ok_or(anyhow::anyhow!("No composefs= param"))?;
193
194 let value = kv
195 .value()
196 .ok_or(anyhow::anyhow!("Empty composefs= param"))?;
197
198 let value = value.to_owned();
199
200 Ok(value)
201 }
202
203 BLSConfigType::Unknown => anyhow::bail!("Unknown config type"),
204 }
205 }
206
207 pub(crate) fn get_cmdline(&self) -> Result<&Cmdline<'_>> {
211 match &self.cfg_type {
212 BLSConfigType::NonEFI { options, .. } => {
213 let options = options
214 .as_ref()
215 .ok_or_else(|| anyhow::anyhow!("No cmdline found for config"))?;
216
217 Ok(options)
218 }
219
220 _ => anyhow::bail!("No cmdline found for config"),
221 }
222 }
223}
224
225pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
226 let mut title = None;
227 let mut version = None;
228 let mut linux = None;
229 let mut efi = None;
230 let mut initrd = Vec::new();
231 let mut options = None;
232 let mut machine_id = None;
233 let mut sort_key = None;
234 let mut extra = HashMap::new();
235
236 for line in input.lines() {
237 let line = line.trim();
238 if line.is_empty() || line.starts_with('#') {
239 continue;
240 }
241
242 if let Some((key, value)) = line.split_once(' ') {
243 let value = value.trim().to_string();
244 match key {
245 "title" => title = Some(value),
246 "version" => version = Some(value),
247 "linux" => linux = Some(Utf8PathBuf::from(value)),
248 "initrd" => initrd.push(Utf8PathBuf::from(value)),
249 "options" => options = Some(CmdlineOwned::from(value)),
250 "machine-id" => machine_id = Some(value),
251 "sort-key" => sort_key = Some(value),
252 "efi" => efi = Some(Utf8PathBuf::from(value)),
253 _ => {
254 extra.insert(key.to_string(), value);
255 }
256 }
257 }
258 }
259
260 let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?;
261
262 let cfg_type = match (linux, efi) {
263 (None, Some(efi)) => BLSConfigType::EFI { efi },
264
265 (Some(linux), None) => BLSConfigType::NonEFI {
266 linux,
267 initrd,
268 options,
269 },
270
271 (Some(_), Some(_)) => anyhow::bail!("'linux' and 'efi' values present"),
274 (None, None) => anyhow::bail!("Missing 'linux' or 'efi' value"),
275 };
276
277 Ok(BLSConfig {
278 title,
279 version,
280 cfg_type,
281 machine_id,
282 sort_key,
283 extra,
284 })
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn test_parse_valid_bls_config() -> Result<()> {
293 let input = r#"
294 title Fedora 42.20250623.3.1 (CoreOS)
295 version 2
296 linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
297 initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
298 options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
299 custom1 value1
300 custom2 value2
301 "#;
302
303 let config = parse_bls_config(input)?;
304
305 let BLSConfigType::NonEFI {
306 linux,
307 initrd,
308 options,
309 } = config.cfg_type
310 else {
311 panic!("Expected non EFI variant");
312 };
313
314 assert_eq!(
315 config.title,
316 Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
317 );
318 assert_eq!(config.version, "2");
319 assert_eq!(
320 linux,
321 "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"
322 );
323 assert_eq!(
324 initrd,
325 vec![
326 "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"
327 ]
328 );
329 assert_eq!(
330 &*options.unwrap(),
331 "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"
332 );
333 assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
334 assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
335
336 Ok(())
337 }
338
339 #[test]
340 fn test_parse_multiple_initrd() -> Result<()> {
341 let input = r#"
342 title Fedora 42.20250623.3.1 (CoreOS)
343 version 2
344 linux /boot/vmlinuz
345 initrd /boot/initramfs-1.img
346 initrd /boot/initramfs-2.img
347 options root=UUID=abc123 rw
348 "#;
349
350 let config = parse_bls_config(input)?;
351
352 let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else {
353 panic!("Expected non EFI variant");
354 };
355
356 assert_eq!(
357 initrd,
358 vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
359 );
360
361 Ok(())
362 }
363
364 #[test]
365 fn test_parse_missing_version() {
366 let input = r#"
367 title Fedora
368 linux /vmlinuz
369 initrd /initramfs.img
370 options root=UUID=xyz ro quiet
371 "#;
372
373 let parsed = parse_bls_config(input);
374 assert!(parsed.is_err());
375 }
376
377 #[test]
378 fn test_parse_missing_linux() {
379 let input = r#"
380 title Fedora
381 version 1
382 initrd /initramfs.img
383 options root=UUID=xyz ro quiet
384 "#;
385
386 let parsed = parse_bls_config(input);
387 assert!(parsed.is_err());
388 }
389
390 #[test]
391 fn test_display_output() -> Result<()> {
392 let input = r#"
393 title Test OS
394 version 10
395 linux /boot/vmlinuz
396 initrd /boot/initrd.img
397 initrd /boot/initrd-extra.img
398 options root=UUID=abc composefs=some-uuid
399 foo bar
400 "#;
401
402 let config = parse_bls_config(input)?;
403 let output = format!("{}", config);
404 let mut output_lines = output.lines();
405
406 assert_eq!(output_lines.next().unwrap(), "title Test OS");
407 assert_eq!(output_lines.next().unwrap(), "version 10");
408 assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
409 assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
410 assert_eq!(
411 output_lines.next().unwrap(),
412 "initrd /boot/initrd-extra.img"
413 );
414 assert_eq!(
415 output_lines.next().unwrap(),
416 "options root=UUID=abc composefs=some-uuid"
417 );
418 assert_eq!(output_lines.next().unwrap(), "foo bar");
419
420 Ok(())
421 }
422
423 #[test]
424 fn test_ordering_by_version() -> Result<()> {
425 let config1 = parse_bls_config(
426 r#"
427 title Entry 1
428 version 3
429 linux /vmlinuz-3
430 initrd /initrd-3
431 options opt1
432 "#,
433 )?;
434
435 let config2 = parse_bls_config(
436 r#"
437 title Entry 2
438 version 5
439 linux /vmlinuz-5
440 initrd /initrd-5
441 options opt2
442 "#,
443 )?;
444
445 assert!(config1 > config2);
446 Ok(())
447 }
448
449 #[test]
450 fn test_ordering_by_sort_key() -> Result<()> {
451 let config1 = parse_bls_config(
452 r#"
453 title Entry 1
454 version 3
455 sort-key a
456 linux /vmlinuz-3
457 initrd /initrd-3
458 options opt1
459 "#,
460 )?;
461
462 let config2 = parse_bls_config(
463 r#"
464 title Entry 2
465 version 5
466 sort-key b
467 linux /vmlinuz-5
468 initrd /initrd-5
469 options opt2
470 "#,
471 )?;
472
473 assert!(config1 < config2);
474 Ok(())
475 }
476
477 #[test]
478 fn test_ordering_by_sort_key_and_version() -> Result<()> {
479 let config1 = parse_bls_config(
480 r#"
481 title Entry 1
482 version 3
483 sort-key a
484 linux /vmlinuz-3
485 initrd /initrd-3
486 options opt1
487 "#,
488 )?;
489
490 let config2 = parse_bls_config(
491 r#"
492 title Entry 2
493 version 5
494 sort-key a
495 linux /vmlinuz-5
496 initrd /initrd-5
497 options opt2
498 "#,
499 )?;
500
501 assert!(config1 > config2);
502 Ok(())
503 }
504
505 #[test]
506 fn test_ordering_by_machine_id() -> Result<()> {
507 let config1 = parse_bls_config(
508 r#"
509 title Entry 1
510 version 3
511 machine-id a
512 linux /vmlinuz-3
513 initrd /initrd-3
514 options opt1
515 "#,
516 )?;
517
518 let config2 = parse_bls_config(
519 r#"
520 title Entry 2
521 version 5
522 machine-id b
523 linux /vmlinuz-5
524 initrd /initrd-5
525 options opt2
526 "#,
527 )?;
528
529 assert!(config1 < config2);
530 Ok(())
531 }
532
533 #[test]
534 fn test_ordering_by_machine_id_and_version() -> Result<()> {
535 let config1 = parse_bls_config(
536 r#"
537 title Entry 1
538 version 3
539 machine-id a
540 linux /vmlinuz-3
541 initrd /initrd-3
542 options opt1
543 "#,
544 )?;
545
546 let config2 = parse_bls_config(
547 r#"
548 title Entry 2
549 version 5
550 machine-id a
551 linux /vmlinuz-5
552 initrd /initrd-5
553 options opt2
554 "#,
555 )?;
556
557 assert!(config1 > config2);
558 Ok(())
559 }
560
561 #[test]
562 fn test_ordering_by_nontrivial_version() -> Result<()> {
563 let config_final = parse_bls_config(
564 r#"
565 title Entry 1
566 version 1.0
567 linux /vmlinuz-1
568 initrd /initrd-1
569 "#,
570 )?;
571
572 let config_rc1 = parse_bls_config(
573 r#"
574 title Entry 2
575 version 1.0~rc1
576 linux /vmlinuz-2
577 initrd /initrd-2
578 "#,
579 )?;
580
581 assert!(config_final < config_rc1);
585 Ok(())
586 }
587}