1use anyhow::{Result, anyhow};
6use bootc_kernel_cmdline::utf8::{Cmdline, CmdlineOwned};
7use camino::Utf8PathBuf;
8use composefs_boot::bootloader::EFI_EXT;
9use composefs_ctl::composefs_boot;
10use core::fmt;
11use std::collections::HashMap;
12use std::fmt::Display;
13use uapi_version::Version;
14
15use crate::bootc_composefs::status::ComposefsCmdline;
16use crate::bootloader::bootctl_systemd_version;
17use crate::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX};
18use crate::spec::Bootloader;
19
20const SYSTEMD_UKI_MIN_VERSION: u32 = 258;
22
23#[derive(Debug, PartialEq, Eq, Clone)]
24pub enum EFIKey {
25 Efi(Utf8PathBuf),
27 Uki(Utf8PathBuf),
29}
30
31impl EFIKey {
32 pub(crate) fn for_bootloader(path: Utf8PathBuf, bootloader: &Bootloader) -> EFIKey {
37 if *bootloader == Bootloader::GrubCC {
38 EFIKey::Efi(path)
41 } else {
42 match bootctl_systemd_version() {
44 Ok(version) if version >= SYSTEMD_UKI_MIN_VERSION => EFIKey::Uki(path),
45 _ => EFIKey::Efi(path),
46 }
47 }
48 }
49}
50
51#[derive(Debug, PartialEq, Eq, Default, Clone)]
52pub enum BLSConfigType {
53 EFI {
54 key: EFIKey,
56 },
57 NonEFI {
58 linux: Utf8PathBuf,
60 initrd: Vec<Utf8PathBuf>,
62 options: Option<CmdlineOwned>,
64 },
65 #[default]
66 Unknown,
67}
68
69#[derive(Debug, Eq, PartialEq, Default, Clone)]
75#[non_exhaustive]
76pub(crate) struct BLSConfig {
77 pub(crate) title: Option<String>,
79 version: String,
84
85 pub(crate) cfg_type: BLSConfigType,
86
87 pub(crate) machine_id: Option<String>,
89 pub(crate) sort_key: Option<String>,
91
92 pub(crate) extra: HashMap<String, String>,
94}
95
96impl PartialOrd for BLSConfig {
97 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
98 Some(self.cmp(other))
99 }
100}
101
102impl Ord for BLSConfig {
103 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
108 if let (Some(key1), Some(key2)) = (&self.sort_key, &other.sort_key) {
110 let ord = key1.cmp(key2);
111 if ord != std::cmp::Ordering::Equal {
112 return ord;
113 }
114 }
115
116 if let (Some(id1), Some(id2)) = (&self.machine_id, &other.machine_id) {
118 let ord = id1.cmp(id2);
119 if ord != std::cmp::Ordering::Equal {
120 return ord;
121 }
122 }
123
124 self.version().cmp(&other.version()).reverse()
126 }
127}
128
129impl Display for BLSConfig {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 if let Some(title) = &self.title {
132 writeln!(f, "title {}", title)?;
133 }
134
135 writeln!(f, "version {}", self.version)?;
136
137 match &self.cfg_type {
138 BLSConfigType::EFI { key } => match key {
139 EFIKey::Efi(path) => writeln!(f, "efi {}", path)?,
140 EFIKey::Uki(path) => writeln!(f, "uki {}", path)?,
141 },
142
143 BLSConfigType::NonEFI {
144 linux,
145 initrd,
146 options,
147 } => {
148 writeln!(f, "linux {}", linux)?;
149 for initrd in initrd.iter() {
150 writeln!(f, "initrd {}", initrd)?;
151 }
152
153 if let Some(options) = options.as_deref() {
154 writeln!(f, "options {}", options)?;
155 }
156 }
157
158 BLSConfigType::Unknown => return Err(fmt::Error),
159 }
160
161 if let Some(machine_id) = self.machine_id.as_deref() {
162 writeln!(f, "machine-id {}", machine_id)?;
163 }
164 if let Some(sort_key) = self.sort_key.as_deref() {
165 writeln!(f, "sort-key {}", sort_key)?;
166 }
167
168 for (key, value) in &self.extra {
169 writeln!(f, "{} {}", key, value)?;
170 }
171
172 Ok(())
173 }
174}
175
176impl BLSConfig {
177 pub(crate) fn version(&self) -> Version {
178 Version::from(&self.version)
179 }
180
181 pub(crate) fn with_title(&mut self, new_val: String) -> &mut Self {
182 self.title = Some(new_val);
183 self
184 }
185 pub(crate) fn with_version(&mut self, new_val: String) -> &mut Self {
186 self.version = new_val;
187 self
188 }
189 pub(crate) fn with_cfg(&mut self, config: BLSConfigType) -> &mut Self {
190 self.cfg_type = config;
191 self
192 }
193 #[allow(dead_code)]
194 pub(crate) fn with_machine_id(&mut self, new_val: String) -> &mut Self {
195 self.machine_id = Some(new_val);
196 self
197 }
198 pub(crate) fn with_sort_key(&mut self, new_val: String) -> &mut Self {
199 self.sort_key = Some(new_val);
200 self
201 }
202 #[allow(dead_code)]
203 pub(crate) fn with_extra(&mut self, new_val: HashMap<String, String>) -> &mut Self {
204 self.extra = new_val;
205 self
206 }
207
208 pub(crate) fn get_verity(&self) -> Result<String> {
212 match &self.cfg_type {
213 BLSConfigType::EFI { key } => {
214 let path = match key {
215 EFIKey::Efi(path) | EFIKey::Uki(path) => path,
216 };
217 let name = path
218 .components()
219 .last()
220 .ok_or(anyhow::anyhow!("Empty efi field"))?
221 .to_string()
222 .strip_prefix(UKI_NAME_PREFIX)
223 .ok_or_else(|| anyhow::anyhow!("efi does not start with custom prefix"))?
224 .strip_suffix(EFI_EXT)
225 .ok_or_else(|| anyhow::anyhow!("efi doesn't end with .efi"))?
226 .to_string();
227
228 Ok(name)
229 }
230
231 BLSConfigType::NonEFI { options, .. } => {
232 let options = options
233 .as_ref()
234 .ok_or_else(|| anyhow::anyhow!("No options"))?;
235
236 let cfs_cmdline = ComposefsCmdline::find_in_cmdline(&Cmdline::from(&options))
237 .ok_or_else(|| anyhow::anyhow!("No composefs= param"))?;
238
239 Ok(cfs_cmdline.digest.to_string())
240 }
241
242 BLSConfigType::Unknown => anyhow::bail!("Unknown config type"),
243 }
244 }
245
246 pub(crate) fn boot_artifact_name(&self) -> Result<&str> {
252 Ok(self.boot_artifact_info()?.0)
253 }
254
255 pub(crate) fn boot_artifact_info(&self) -> Result<(&str, bool)> {
263 match &self.cfg_type {
264 BLSConfigType::EFI { key } => {
265 let path = match key {
266 EFIKey::Efi(path) | EFIKey::Uki(path) => path,
267 };
268 let file_name = path
269 .file_name()
270 .ok_or_else(|| anyhow::anyhow!("EFI path missing file name: {}", path))?;
271
272 let without_suffix = file_name.strip_suffix(EFI_EXT).ok_or_else(|| {
273 anyhow::anyhow!(
274 "EFI file name missing expected suffix '{}': {}",
275 EFI_EXT,
276 file_name
277 )
278 })?;
279
280 match without_suffix.strip_prefix(UKI_NAME_PREFIX) {
282 Some(no_prefix) => Ok((no_prefix, true)),
283 None => Ok((without_suffix, false)),
284 }
285 }
286
287 BLSConfigType::NonEFI { linux, .. } => {
288 let parent_dir = linux.parent().ok_or_else(|| {
289 anyhow::anyhow!("Linux kernel path has no parent directory: {}", linux)
290 })?;
291
292 let dir_name = parent_dir.file_name().ok_or_else(|| {
293 anyhow::anyhow!("Parent directory has no file name: {}", parent_dir)
294 })?;
295
296 match dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) {
298 Some(dir_name_no_prefix) => Ok((dir_name_no_prefix, true)),
299 None => Ok((dir_name, false)),
300 }
301 }
302
303 BLSConfigType::Unknown => {
304 anyhow::bail!("Cannot extract boot artifact name from unknown config type")
305 }
306 }
307 }
308
309 pub(crate) fn get_cmdline(&self) -> Result<&Cmdline<'_>> {
313 match &self.cfg_type {
314 BLSConfigType::NonEFI { options, .. } => {
315 let options = options
316 .as_ref()
317 .ok_or_else(|| anyhow::anyhow!("No cmdline found for config"))?;
318
319 Ok(options)
320 }
321
322 _ => anyhow::bail!("No cmdline found for config"),
323 }
324 }
325}
326
327pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
328 let mut title = None;
329 let mut version = None;
330 let mut linux = None;
331 let mut efi_key = None;
332 let mut initrd = Vec::new();
333 let mut options = None;
334 let mut machine_id = None;
335 let mut sort_key = None;
336 let mut extra = HashMap::new();
337
338 for line in input.lines() {
339 let line = line.trim();
340 if line.is_empty() || line.starts_with('#') {
341 continue;
342 }
343
344 if let Some((key, value)) = line.split_once(' ') {
345 let value = value.trim().to_string();
346 match key {
347 "title" => title = Some(value),
348 "version" => version = Some(value),
349 "linux" => linux = Some(Utf8PathBuf::from(value)),
350 "initrd" => initrd.push(Utf8PathBuf::from(value)),
351 "options" => options = Some(CmdlineOwned::from(value)),
352 "machine-id" => machine_id = Some(value),
353 "sort-key" => sort_key = Some(value),
354 "efi" => efi_key = Some(EFIKey::Efi(Utf8PathBuf::from(value))),
355 "uki" => efi_key = Some(EFIKey::Uki(Utf8PathBuf::from(value))),
356 _ => {
357 extra.insert(key.to_string(), value);
358 }
359 }
360 }
361 }
362
363 let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?;
364
365 let cfg_type = match (linux, efi_key) {
366 (None, Some(key)) => BLSConfigType::EFI { key },
367
368 (Some(linux), None) => BLSConfigType::NonEFI {
369 linux,
370 initrd,
371 options,
372 },
373
374 (Some(_), Some(_)) => anyhow::bail!("'linux' and 'efi'/'uki' values present"),
377 (None, None) => anyhow::bail!("Missing 'linux', 'efi', or 'uki' value"),
378 };
379
380 Ok(BLSConfig {
381 title,
382 version,
383 cfg_type,
384 machine_id,
385 sort_key,
386 extra,
387 })
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_parse_valid_bls_config() -> Result<()> {
396 let input = r#"
397 title Fedora 42.20250623.3.1 (CoreOS)
398 version 2
399 linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
400 initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
401 options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
402 custom1 value1
403 custom2 value2
404 "#;
405
406 let config = parse_bls_config(input)?;
407
408 let BLSConfigType::NonEFI {
409 linux,
410 initrd,
411 options,
412 } = config.cfg_type
413 else {
414 panic!("Expected non EFI variant");
415 };
416
417 assert_eq!(
418 config.title,
419 Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
420 );
421 assert_eq!(config.version, "2");
422 assert_eq!(
423 linux,
424 "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"
425 );
426 assert_eq!(
427 initrd,
428 vec![
429 "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"
430 ]
431 );
432 assert_eq!(
433 &*options.unwrap(),
434 "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"
435 );
436 assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
437 assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
438
439 Ok(())
440 }
441
442 #[test]
443 fn test_parse_multiple_initrd() -> Result<()> {
444 let input = r#"
445 title Fedora 42.20250623.3.1 (CoreOS)
446 version 2
447 linux /boot/vmlinuz
448 initrd /boot/initramfs-1.img
449 initrd /boot/initramfs-2.img
450 options root=UUID=abc123 rw
451 "#;
452
453 let config = parse_bls_config(input)?;
454
455 let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else {
456 panic!("Expected non EFI variant");
457 };
458
459 assert_eq!(
460 initrd,
461 vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
462 );
463
464 Ok(())
465 }
466
467 #[test]
468 fn test_parse_missing_version() {
469 let input = r#"
470 title Fedora
471 linux /vmlinuz
472 initrd /initramfs.img
473 options root=UUID=xyz ro quiet
474 "#;
475
476 let parsed = parse_bls_config(input);
477 assert!(parsed.is_err());
478 }
479
480 #[test]
481 fn test_parse_missing_linux() {
482 let input = r#"
483 title Fedora
484 version 1
485 initrd /initramfs.img
486 options root=UUID=xyz ro quiet
487 "#;
488
489 let parsed = parse_bls_config(input);
490 assert!(parsed.is_err());
491 }
492
493 #[test]
494 fn test_display_output() -> Result<()> {
495 let input = r#"
496 title Test OS
497 version 10
498 linux /boot/vmlinuz
499 initrd /boot/initrd.img
500 initrd /boot/initrd-extra.img
501 options root=UUID=abc composefs=some-uuid
502 foo bar
503 "#;
504
505 let config = parse_bls_config(input)?;
506 let output = format!("{}", config);
507 let mut output_lines = output.lines();
508
509 assert_eq!(output_lines.next().unwrap(), "title Test OS");
510 assert_eq!(output_lines.next().unwrap(), "version 10");
511 assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
512 assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
513 assert_eq!(
514 output_lines.next().unwrap(),
515 "initrd /boot/initrd-extra.img"
516 );
517 assert_eq!(
518 output_lines.next().unwrap(),
519 "options root=UUID=abc composefs=some-uuid"
520 );
521 assert_eq!(output_lines.next().unwrap(), "foo bar");
522
523 Ok(())
524 }
525
526 #[test]
527 fn test_ordering_by_version() -> Result<()> {
528 let config1 = parse_bls_config(
529 r#"
530 title Entry 1
531 version 3
532 linux /vmlinuz-3
533 initrd /initrd-3
534 options opt1
535 "#,
536 )?;
537
538 let config2 = parse_bls_config(
539 r#"
540 title Entry 2
541 version 5
542 linux /vmlinuz-5
543 initrd /initrd-5
544 options opt2
545 "#,
546 )?;
547
548 assert!(config1 > config2);
549 Ok(())
550 }
551
552 #[test]
553 fn test_ordering_by_sort_key() -> Result<()> {
554 let config1 = parse_bls_config(
555 r#"
556 title Entry 1
557 version 3
558 sort-key a
559 linux /vmlinuz-3
560 initrd /initrd-3
561 options opt1
562 "#,
563 )?;
564
565 let config2 = parse_bls_config(
566 r#"
567 title Entry 2
568 version 5
569 sort-key b
570 linux /vmlinuz-5
571 initrd /initrd-5
572 options opt2
573 "#,
574 )?;
575
576 assert!(config1 < config2);
577 Ok(())
578 }
579
580 #[test]
581 fn test_ordering_by_sort_key_and_version() -> Result<()> {
582 let config1 = parse_bls_config(
583 r#"
584 title Entry 1
585 version 3
586 sort-key a
587 linux /vmlinuz-3
588 initrd /initrd-3
589 options opt1
590 "#,
591 )?;
592
593 let config2 = parse_bls_config(
594 r#"
595 title Entry 2
596 version 5
597 sort-key a
598 linux /vmlinuz-5
599 initrd /initrd-5
600 options opt2
601 "#,
602 )?;
603
604 assert!(config1 > config2);
605 Ok(())
606 }
607
608 #[test]
609 fn test_ordering_by_machine_id() -> Result<()> {
610 let config1 = parse_bls_config(
611 r#"
612 title Entry 1
613 version 3
614 machine-id a
615 linux /vmlinuz-3
616 initrd /initrd-3
617 options opt1
618 "#,
619 )?;
620
621 let config2 = parse_bls_config(
622 r#"
623 title Entry 2
624 version 5
625 machine-id b
626 linux /vmlinuz-5
627 initrd /initrd-5
628 options opt2
629 "#,
630 )?;
631
632 assert!(config1 < config2);
633 Ok(())
634 }
635
636 #[test]
637 fn test_ordering_by_machine_id_and_version() -> Result<()> {
638 let config1 = parse_bls_config(
639 r#"
640 title Entry 1
641 version 3
642 machine-id a
643 linux /vmlinuz-3
644 initrd /initrd-3
645 options opt1
646 "#,
647 )?;
648
649 let config2 = parse_bls_config(
650 r#"
651 title Entry 2
652 version 5
653 machine-id a
654 linux /vmlinuz-5
655 initrd /initrd-5
656 options opt2
657 "#,
658 )?;
659
660 assert!(config1 > config2);
661 Ok(())
662 }
663
664 #[test]
665 fn test_ordering_by_nontrivial_version() -> Result<()> {
666 let config_final = parse_bls_config(
667 r#"
668 title Entry 1
669 version 1.0
670 linux /vmlinuz-1
671 initrd /initrd-1
672 "#,
673 )?;
674
675 let config_rc1 = parse_bls_config(
676 r#"
677 title Entry 2
678 version 1.0~rc1
679 linux /vmlinuz-2
680 initrd /initrd-2
681 "#,
682 )?;
683
684 assert!(config_final < config_rc1);
688 Ok(())
689 }
690
691 #[test]
692 fn test_boot_artifact_name_efi_success() -> Result<()> {
693 use camino::Utf8PathBuf;
694
695 let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234.efi");
696 let config = BLSConfig {
697 cfg_type: BLSConfigType::EFI {
698 key: EFIKey::Efi(efi_path),
699 },
700 version: "1".to_string(),
701 ..Default::default()
702 };
703
704 let artifact_name = config.boot_artifact_name()?;
705 assert_eq!(artifact_name, "abcd1234");
706 Ok(())
707 }
708
709 #[test]
710 fn test_boot_artifact_name_non_efi_success() -> Result<()> {
711 use camino::Utf8PathBuf;
712
713 let linux_path = Utf8PathBuf::from("/boot/bootc_composefs-xyz5678/vmlinuz");
714 let config = BLSConfig {
715 cfg_type: BLSConfigType::NonEFI {
716 linux: linux_path,
717 initrd: vec![],
718 options: None,
719 },
720 version: "1".to_string(),
721 ..Default::default()
722 };
723
724 let artifact_name = config.boot_artifact_name()?;
725 assert_eq!(artifact_name, "xyz5678");
726 Ok(())
727 }
728
729 #[test]
730 fn test_boot_artifact_name_efi_missing_prefix() {
731 use camino::Utf8PathBuf;
732
733 let efi_path = Utf8PathBuf::from("/EFI/Linux/abcd1234.efi");
734 let config = BLSConfig {
735 cfg_type: BLSConfigType::EFI {
736 key: EFIKey::Efi(efi_path),
737 },
738 version: "1".to_string(),
739 ..Default::default()
740 };
741
742 let artifact_name = config
743 .boot_artifact_name()
744 .expect("Should extract artifact name");
745 assert_eq!(artifact_name, "abcd1234");
746 }
747
748 #[test]
749 fn test_boot_artifact_name_efi_missing_suffix() {
750 use camino::Utf8PathBuf;
751
752 let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234");
753 let config = BLSConfig {
754 cfg_type: BLSConfigType::EFI {
755 key: EFIKey::Efi(efi_path),
756 },
757 version: "1".to_string(),
758 ..Default::default()
759 };
760
761 let result = config.boot_artifact_name();
762 assert!(result.is_err());
763 assert!(
764 result
765 .unwrap_err()
766 .to_string()
767 .contains("missing expected suffix")
768 );
769 }
770
771 #[test]
772 fn test_boot_artifact_name_efi_no_filename() {
773 use camino::Utf8PathBuf;
774
775 let efi_path = Utf8PathBuf::from("/");
776 let config = BLSConfig {
777 cfg_type: BLSConfigType::EFI {
778 key: EFIKey::Efi(efi_path),
779 },
780 version: "1".to_string(),
781 ..Default::default()
782 };
783
784 let result = config.boot_artifact_name();
785 assert!(result.is_err());
786 assert!(
787 result
788 .unwrap_err()
789 .to_string()
790 .contains("missing file name")
791 );
792 }
793
794 #[test]
795 fn test_boot_artifact_name_unknown_type() {
796 let config = BLSConfig {
797 cfg_type: BLSConfigType::Unknown,
798 version: "1".to_string(),
799 ..Default::default()
800 };
801
802 let result = config.boot_artifact_name();
803 assert!(result.is_err());
804 assert!(
805 result
806 .unwrap_err()
807 .to_string()
808 .contains("unknown config type")
809 );
810 }
811 #[test]
812 fn test_boot_artifact_name_efi_nested_path() -> Result<()> {
813 let efi_path = Utf8PathBuf::from("/EFI/Linux/bootc/bootc_composefs-deadbeef01234567.efi");
814 let config = BLSConfig {
815 cfg_type: BLSConfigType::EFI {
816 key: EFIKey::Efi(efi_path),
817 },
818 version: "1".to_string(),
819 ..Default::default()
820 };
821
822 assert_eq!(config.boot_artifact_name()?, "deadbeef01234567");
823 Ok(())
824 }
825
826 #[test]
827 fn test_boot_artifact_name_non_efi_deep_path() -> Result<()> {
828 let digest = "7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6";
830 let linux_path = Utf8PathBuf::from(format!("/boot/bootc_composefs-{digest}/vmlinuz"));
831 let config = BLSConfig {
832 cfg_type: BLSConfigType::NonEFI {
833 linux: linux_path,
834 initrd: vec![],
835 options: None,
836 },
837 version: "1".to_string(),
838 ..Default::default()
839 };
840
841 assert_eq!(config.boot_artifact_name()?, digest);
842 Ok(())
843 }
844
845 #[test]
847 fn test_boot_artifact_name_from_parsed_efi_config() -> Result<()> {
848 let digest = "f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346";
849 let input = format!(
851 r#"
852 title Fedora UKI
853 version 1
854 efi /EFI/Linux/bootc/bootc_composefs-{digest}.efi
855 sort-key bootc-fedora-0
856 "#
857 );
858
859 let config = parse_bls_config(&input)?;
860 assert_eq!(config.boot_artifact_name()?, digest);
861 assert_eq!(config.get_verity()?, digest);
862
863 let uki_input = input.replace("efi ", "uki ");
864 let config = parse_bls_config(&uki_input)?;
865 assert_eq!(config.boot_artifact_name()?, digest);
866 assert_eq!(config.get_verity()?, digest);
867
868 Ok(())
869 }
870
871 #[test]
873 fn test_boot_artifact_name_non_efi_no_parent() {
874 let config = BLSConfig {
875 cfg_type: BLSConfigType::NonEFI {
876 linux: Utf8PathBuf::from("vmlinuz"),
877 initrd: vec![],
878 options: None,
879 },
880 version: "1".to_string(),
881 ..Default::default()
882 };
883
884 let result = config.boot_artifact_name();
885 assert!(result.is_err());
886 }
887
888 #[test]
889 fn test_efi_key_variants_and_display() -> Result<()> {
890 use camino::Utf8PathBuf;
891
892 let efi_config = BLSConfig {
894 title: Some("Test EFI Entry".to_string()),
895 version: "1.0".to_string(),
896 cfg_type: BLSConfigType::EFI {
897 key: EFIKey::Efi(Utf8PathBuf::from("/EFI/Linux/test.efi")),
898 },
899 sort_key: Some("test-key".to_string()),
900 ..Default::default()
901 };
902
903 let result = efi_config.to_string();
904 assert!(result.contains("efi /EFI/Linux/test.efi"));
905 assert!(result.contains("title Test EFI Entry"));
906 assert!(result.contains("version 1.0"));
907 assert!(result.contains("sort-key test-key"));
908 assert!(!result.contains("uki"));
909
910 let uki_config = BLSConfig {
912 title: Some("Test UKI Entry".to_string()),
913 version: "1.0".to_string(),
914 cfg_type: BLSConfigType::EFI {
915 key: EFIKey::Uki(Utf8PathBuf::from("/EFI/Linux/test.efi")),
916 },
917 sort_key: Some("test-key".to_string()),
918 ..Default::default()
919 };
920
921 let result = uki_config.to_string();
922 assert!(result.contains("uki /EFI/Linux/test.efi"));
923 assert!(result.contains("title Test UKI Entry"));
924 assert!(result.contains("version 1.0"));
925 assert!(result.contains("sort-key test-key"));
926 assert!(!result.starts_with("efi ") && !result.contains("\nefi "));
928
929 let non_efi_config = BLSConfig {
931 version: "1.0".to_string(),
932 cfg_type: BLSConfigType::NonEFI {
933 linux: Utf8PathBuf::from("/boot/vmlinuz"),
934 initrd: vec![Utf8PathBuf::from("/boot/initrd.img")],
935 options: Some("root=UUID=abc123".into()),
936 },
937 ..Default::default()
938 };
939
940 let result = non_efi_config.to_string();
941 assert!(result.contains("linux /boot/vmlinuz"));
942 assert!(result.contains("initrd /boot/initrd.img"));
943 assert!(result.contains("options root=UUID=abc123"));
944 assert!(!result.contains("efi"));
945 assert!(!result.contains("uki"));
946
947 Ok(())
948 }
949}