1#![allow(dead_code)]
6
7use anyhow::{anyhow, Result};
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!(linux, "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10");
320 assert_eq!(initrd, vec!["/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"]);
321 assert_eq!(&*options.unwrap(), "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6");
322 assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
323 assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
324
325 Ok(())
326 }
327
328 #[test]
329 fn test_parse_multiple_initrd() -> Result<()> {
330 let input = r#"
331 title Fedora 42.20250623.3.1 (CoreOS)
332 version 2
333 linux /boot/vmlinuz
334 initrd /boot/initramfs-1.img
335 initrd /boot/initramfs-2.img
336 options root=UUID=abc123 rw
337 "#;
338
339 let config = parse_bls_config(input)?;
340
341 let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else {
342 panic!("Expected non EFI variant");
343 };
344
345 assert_eq!(
346 initrd,
347 vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
348 );
349
350 Ok(())
351 }
352
353 #[test]
354 fn test_parse_missing_version() {
355 let input = r#"
356 title Fedora
357 linux /vmlinuz
358 initrd /initramfs.img
359 options root=UUID=xyz ro quiet
360 "#;
361
362 let parsed = parse_bls_config(input);
363 assert!(parsed.is_err());
364 }
365
366 #[test]
367 fn test_parse_missing_linux() {
368 let input = r#"
369 title Fedora
370 version 1
371 initrd /initramfs.img
372 options root=UUID=xyz ro quiet
373 "#;
374
375 let parsed = parse_bls_config(input);
376 assert!(parsed.is_err());
377 }
378
379 #[test]
380 fn test_display_output() -> Result<()> {
381 let input = r#"
382 title Test OS
383 version 10
384 linux /boot/vmlinuz
385 initrd /boot/initrd.img
386 initrd /boot/initrd-extra.img
387 options root=UUID=abc composefs=some-uuid
388 foo bar
389 "#;
390
391 let config = parse_bls_config(input)?;
392 let output = format!("{}", config);
393 let mut output_lines = output.lines();
394
395 assert_eq!(output_lines.next().unwrap(), "title Test OS");
396 assert_eq!(output_lines.next().unwrap(), "version 10");
397 assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
398 assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
399 assert_eq!(
400 output_lines.next().unwrap(),
401 "initrd /boot/initrd-extra.img"
402 );
403 assert_eq!(
404 output_lines.next().unwrap(),
405 "options root=UUID=abc composefs=some-uuid"
406 );
407 assert_eq!(output_lines.next().unwrap(), "foo bar");
408
409 Ok(())
410 }
411
412 #[test]
413 fn test_ordering_by_version() -> Result<()> {
414 let config1 = parse_bls_config(
415 r#"
416 title Entry 1
417 version 3
418 linux /vmlinuz-3
419 initrd /initrd-3
420 options opt1
421 "#,
422 )?;
423
424 let config2 = parse_bls_config(
425 r#"
426 title Entry 2
427 version 5
428 linux /vmlinuz-5
429 initrd /initrd-5
430 options opt2
431 "#,
432 )?;
433
434 assert!(config1 > config2);
435 Ok(())
436 }
437
438 #[test]
439 fn test_ordering_by_sort_key() -> Result<()> {
440 let config1 = parse_bls_config(
441 r#"
442 title Entry 1
443 version 3
444 sort-key a
445 linux /vmlinuz-3
446 initrd /initrd-3
447 options opt1
448 "#,
449 )?;
450
451 let config2 = parse_bls_config(
452 r#"
453 title Entry 2
454 version 5
455 sort-key b
456 linux /vmlinuz-5
457 initrd /initrd-5
458 options opt2
459 "#,
460 )?;
461
462 assert!(config1 < config2);
463 Ok(())
464 }
465
466 #[test]
467 fn test_ordering_by_sort_key_and_version() -> Result<()> {
468 let config1 = parse_bls_config(
469 r#"
470 title Entry 1
471 version 3
472 sort-key a
473 linux /vmlinuz-3
474 initrd /initrd-3
475 options opt1
476 "#,
477 )?;
478
479 let config2 = parse_bls_config(
480 r#"
481 title Entry 2
482 version 5
483 sort-key a
484 linux /vmlinuz-5
485 initrd /initrd-5
486 options opt2
487 "#,
488 )?;
489
490 assert!(config1 > config2);
491 Ok(())
492 }
493
494 #[test]
495 fn test_ordering_by_machine_id() -> Result<()> {
496 let config1 = parse_bls_config(
497 r#"
498 title Entry 1
499 version 3
500 machine-id a
501 linux /vmlinuz-3
502 initrd /initrd-3
503 options opt1
504 "#,
505 )?;
506
507 let config2 = parse_bls_config(
508 r#"
509 title Entry 2
510 version 5
511 machine-id b
512 linux /vmlinuz-5
513 initrd /initrd-5
514 options opt2
515 "#,
516 )?;
517
518 assert!(config1 < config2);
519 Ok(())
520 }
521
522 #[test]
523 fn test_ordering_by_machine_id_and_version() -> Result<()> {
524 let config1 = parse_bls_config(
525 r#"
526 title Entry 1
527 version 3
528 machine-id a
529 linux /vmlinuz-3
530 initrd /initrd-3
531 options opt1
532 "#,
533 )?;
534
535 let config2 = parse_bls_config(
536 r#"
537 title Entry 2
538 version 5
539 machine-id a
540 linux /vmlinuz-5
541 initrd /initrd-5
542 options opt2
543 "#,
544 )?;
545
546 assert!(config1 > config2);
547 Ok(())
548 }
549
550 #[test]
551 fn test_ordering_by_nontrivial_version() -> Result<()> {
552 let config_final = parse_bls_config(
553 r#"
554 title Entry 1
555 version 1.0
556 linux /vmlinuz-1
557 initrd /initrd-1
558 "#,
559 )?;
560
561 let config_rc1 = parse_bls_config(
562 r#"
563 title Entry 2
564 version 1.0~rc1
565 linux /vmlinuz-2
566 initrd /initrd-2
567 "#,
568 )?;
569
570 assert!(config_final < config_rc1);
574 Ok(())
575 }
576}