Skip to main content

bootc_lib/parsers/
bls_config.rs

1//! See <https://uapi-group.org/specifications/specs/boot_loader_specification/>
2//!
3//! This module parses the config files for the spec.
4
5use 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::composefs_consts::{TYPE1_BOOT_DIR_PREFIX, UKI_NAME_PREFIX};
17
18#[derive(Debug, PartialEq, Eq, Default, Clone)]
19pub enum BLSConfigType {
20    EFI {
21        /// The path to the EFI binary, usually a UKI
22        efi: Utf8PathBuf,
23    },
24    NonEFI {
25        /// The path to the linux kernel to boot.
26        linux: Utf8PathBuf,
27        /// The paths to the initrd images.
28        initrd: Vec<Utf8PathBuf>,
29        /// Kernel command line options.
30        options: Option<CmdlineOwned>,
31    },
32    #[default]
33    Unknown,
34}
35
36/// Represents a single Boot Loader Specification config file.
37///
38/// The boot loader should present the available boot menu entries to the user in a sorted list.
39/// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
40/// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
41#[derive(Debug, Eq, PartialEq, Default, Clone)]
42#[non_exhaustive]
43pub(crate) struct BLSConfig {
44    /// The title of the boot entry, to be displayed in the boot menu.
45    pub(crate) title: Option<String>,
46    /// The version of the boot entry.
47    /// See <https://uapi-group.org/specifications/specs/version_format_specification/>
48    ///
49    /// This is hidden and must be accessed via [`Self::version()`];
50    version: String,
51
52    pub(crate) cfg_type: BLSConfigType,
53
54    /// The machine ID of the OS.
55    pub(crate) machine_id: Option<String>,
56    /// The sort key for the boot menu.
57    pub(crate) sort_key: Option<String>,
58
59    /// Any extra fields not defined in the spec.
60    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    /// This implements the sorting logic from the Boot Loader Specification.
71    ///
72    /// The list should be sorted by the `sort-key` field, if it exists, otherwise by the `machine-id` field.
73    /// If multiple entries have the same `sort-key` (or `machine-id`), they should be sorted by the `version` field in descending order.
74    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
75        // If both configs have a sort key, compare them.
76        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 both configs have a machine ID, compare them.
84        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        // Finally, sort by version in descending order.
92        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    /// Get the fs-verity digest from a BLS config
175    /// For EFI BLS entries, this returns the name of the UKI
176    /// For Non-EFI BLS entries, this returns the fs-verity digest in the "options" field
177    pub(crate) fn get_verity(&self) -> Result<String> {
178        match &self.cfg_type {
179            BLSConfigType::EFI { efi } => {
180                let name = efi
181                    .components()
182                    .last()
183                    .ok_or(anyhow::anyhow!("Empty efi field"))?
184                    .to_string()
185                    .strip_prefix(UKI_NAME_PREFIX)
186                    .ok_or_else(|| anyhow::anyhow!("efi does not start with custom prefix"))?
187                    .strip_suffix(EFI_EXT)
188                    .ok_or_else(|| anyhow::anyhow!("efi doesn't end with .efi"))?
189                    .to_string();
190
191                Ok(name)
192            }
193
194            BLSConfigType::NonEFI { options, .. } => {
195                let options = options
196                    .as_ref()
197                    .ok_or_else(|| anyhow::anyhow!("No options"))?;
198
199                let cfs_cmdline = ComposefsCmdline::find_in_cmdline(&Cmdline::from(&options))
200                    .ok_or_else(|| anyhow::anyhow!("No composefs= param"))?;
201
202                Ok(cfs_cmdline.digest.to_string())
203            }
204
205            BLSConfigType::Unknown => anyhow::bail!("Unknown config type"),
206        }
207    }
208
209    /// Returns name of UKI in case of EFI config
210    /// Returns name of the directory containing Kernel + Initrd in case of Non-EFI config
211    ///
212    /// The names are stripped of our custom prefix and suffixes, so this returns the
213    /// verity digest part of the name
214    pub(crate) fn boot_artifact_name(&self) -> Result<&str> {
215        Ok(self.boot_artifact_info()?.0)
216    }
217
218    /// Returns name of UKI in case of EFI config
219    /// Returns name of the directory containing Kernel + Initrd in case of Non-EFI config
220    ///
221    /// The names are stripped of our custom prefix and suffixes, so this returns the
222    /// verity digest part of the name as the first value
223    ///
224    /// The second value is a boolean indicating whether it found our custom prefix or not
225    pub(crate) fn boot_artifact_info(&self) -> Result<(&str, bool)> {
226        match &self.cfg_type {
227            BLSConfigType::EFI { efi } => {
228                let file_name = efi
229                    .file_name()
230                    .ok_or_else(|| anyhow::anyhow!("EFI path missing file name: {}", efi))?;
231
232                let without_suffix = file_name.strip_suffix(EFI_EXT).ok_or_else(|| {
233                    anyhow::anyhow!(
234                        "EFI file name missing expected suffix '{}': {}",
235                        EFI_EXT,
236                        file_name
237                    )
238                })?;
239
240                // For backwards compatibility, we don't make this prefix mandatory
241                match without_suffix.strip_prefix(UKI_NAME_PREFIX) {
242                    Some(no_prefix) => Ok((no_prefix, true)),
243                    None => Ok((without_suffix, false)),
244                }
245            }
246
247            BLSConfigType::NonEFI { linux, .. } => {
248                let parent_dir = linux.parent().ok_or_else(|| {
249                    anyhow::anyhow!("Linux kernel path has no parent directory: {}", linux)
250                })?;
251
252                let dir_name = parent_dir.file_name().ok_or_else(|| {
253                    anyhow::anyhow!("Parent directory has no file name: {}", parent_dir)
254                })?;
255
256                // For backwards compatibility, we don't make this prefix mandatory
257                match dir_name.strip_prefix(TYPE1_BOOT_DIR_PREFIX) {
258                    Some(dir_name_no_prefix) => Ok((dir_name_no_prefix, true)),
259                    None => Ok((dir_name, false)),
260                }
261            }
262
263            BLSConfigType::Unknown => {
264                anyhow::bail!("Cannot extract boot artifact name from unknown config type")
265            }
266        }
267    }
268
269    /// Gets the `options` field from the config
270    /// Returns an error if the field doesn't exist
271    /// or if the config is of type `EFI`
272    pub(crate) fn get_cmdline(&self) -> Result<&Cmdline<'_>> {
273        match &self.cfg_type {
274            BLSConfigType::NonEFI { options, .. } => {
275                let options = options
276                    .as_ref()
277                    .ok_or_else(|| anyhow::anyhow!("No cmdline found for config"))?;
278
279                Ok(options)
280            }
281
282            _ => anyhow::bail!("No cmdline found for config"),
283        }
284    }
285}
286
287pub(crate) fn parse_bls_config(input: &str) -> Result<BLSConfig> {
288    let mut title = None;
289    let mut version = None;
290    let mut linux = None;
291    let mut efi = None;
292    let mut initrd = Vec::new();
293    let mut options = None;
294    let mut machine_id = None;
295    let mut sort_key = None;
296    let mut extra = HashMap::new();
297
298    for line in input.lines() {
299        let line = line.trim();
300        if line.is_empty() || line.starts_with('#') {
301            continue;
302        }
303
304        if let Some((key, value)) = line.split_once(' ') {
305            let value = value.trim().to_string();
306            match key {
307                "title" => title = Some(value),
308                "version" => version = Some(value),
309                "linux" => linux = Some(Utf8PathBuf::from(value)),
310                "initrd" => initrd.push(Utf8PathBuf::from(value)),
311                "options" => options = Some(CmdlineOwned::from(value)),
312                "machine-id" => machine_id = Some(value),
313                "sort-key" => sort_key = Some(value),
314                "efi" => efi = Some(Utf8PathBuf::from(value)),
315                _ => {
316                    extra.insert(key.to_string(), value);
317                }
318            }
319        }
320    }
321
322    let version = version.ok_or_else(|| anyhow!("Missing 'version' value"))?;
323
324    let cfg_type = match (linux, efi) {
325        (None, Some(efi)) => BLSConfigType::EFI { efi },
326
327        (Some(linux), None) => BLSConfigType::NonEFI {
328            linux,
329            initrd,
330            options,
331        },
332
333        // The spec makes no mention of whether both can be present or not
334        // Fow now, for us, we won't have both at the same time
335        (Some(_), Some(_)) => anyhow::bail!("'linux' and 'efi' values present"),
336        (None, None) => anyhow::bail!("Missing 'linux' or 'efi' value"),
337    };
338
339    Ok(BLSConfig {
340        title,
341        version,
342        cfg_type,
343        machine_id,
344        sort_key,
345        extra,
346    })
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_parse_valid_bls_config() -> Result<()> {
355        let input = r#"
356            title Fedora 42.20250623.3.1 (CoreOS)
357            version 2
358            linux /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10
359            initrd /boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img
360            options root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6
361            custom1 value1
362            custom2 value2
363        "#;
364
365        let config = parse_bls_config(input)?;
366
367        let BLSConfigType::NonEFI {
368            linux,
369            initrd,
370            options,
371        } = config.cfg_type
372        else {
373            panic!("Expected non EFI variant");
374        };
375
376        assert_eq!(
377            config.title,
378            Some("Fedora 42.20250623.3.1 (CoreOS)".to_string())
379        );
380        assert_eq!(config.version, "2");
381        assert_eq!(
382            linux,
383            "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/vmlinuz-5.14.10"
384        );
385        assert_eq!(
386            initrd,
387            vec![
388                "/boot/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6/initramfs-5.14.10.img"
389            ]
390        );
391        assert_eq!(
392            &*options.unwrap(),
393            "root=UUID=abc123 rw composefs=7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6"
394        );
395        assert_eq!(config.extra.get("custom1"), Some(&"value1".to_string()));
396        assert_eq!(config.extra.get("custom2"), Some(&"value2".to_string()));
397
398        Ok(())
399    }
400
401    #[test]
402    fn test_parse_multiple_initrd() -> Result<()> {
403        let input = r#"
404            title Fedora 42.20250623.3.1 (CoreOS)
405            version 2
406            linux /boot/vmlinuz
407            initrd /boot/initramfs-1.img
408            initrd /boot/initramfs-2.img
409            options root=UUID=abc123 rw
410        "#;
411
412        let config = parse_bls_config(input)?;
413
414        let BLSConfigType::NonEFI { initrd, .. } = config.cfg_type else {
415            panic!("Expected non EFI variant");
416        };
417
418        assert_eq!(
419            initrd,
420            vec!["/boot/initramfs-1.img", "/boot/initramfs-2.img"]
421        );
422
423        Ok(())
424    }
425
426    #[test]
427    fn test_parse_missing_version() {
428        let input = r#"
429            title Fedora
430            linux /vmlinuz
431            initrd /initramfs.img
432            options root=UUID=xyz ro quiet
433        "#;
434
435        let parsed = parse_bls_config(input);
436        assert!(parsed.is_err());
437    }
438
439    #[test]
440    fn test_parse_missing_linux() {
441        let input = r#"
442            title Fedora
443            version 1
444            initrd /initramfs.img
445            options root=UUID=xyz ro quiet
446        "#;
447
448        let parsed = parse_bls_config(input);
449        assert!(parsed.is_err());
450    }
451
452    #[test]
453    fn test_display_output() -> Result<()> {
454        let input = r#"
455            title Test OS
456            version 10
457            linux /boot/vmlinuz
458            initrd /boot/initrd.img
459            initrd /boot/initrd-extra.img
460            options root=UUID=abc composefs=some-uuid
461            foo bar
462        "#;
463
464        let config = parse_bls_config(input)?;
465        let output = format!("{}", config);
466        let mut output_lines = output.lines();
467
468        assert_eq!(output_lines.next().unwrap(), "title Test OS");
469        assert_eq!(output_lines.next().unwrap(), "version 10");
470        assert_eq!(output_lines.next().unwrap(), "linux /boot/vmlinuz");
471        assert_eq!(output_lines.next().unwrap(), "initrd /boot/initrd.img");
472        assert_eq!(
473            output_lines.next().unwrap(),
474            "initrd /boot/initrd-extra.img"
475        );
476        assert_eq!(
477            output_lines.next().unwrap(),
478            "options root=UUID=abc composefs=some-uuid"
479        );
480        assert_eq!(output_lines.next().unwrap(), "foo bar");
481
482        Ok(())
483    }
484
485    #[test]
486    fn test_ordering_by_version() -> Result<()> {
487        let config1 = parse_bls_config(
488            r#"
489            title Entry 1
490            version 3
491            linux /vmlinuz-3
492            initrd /initrd-3
493            options opt1
494        "#,
495        )?;
496
497        let config2 = parse_bls_config(
498            r#"
499            title Entry 2
500            version 5
501            linux /vmlinuz-5
502            initrd /initrd-5
503            options opt2
504        "#,
505        )?;
506
507        assert!(config1 > config2);
508        Ok(())
509    }
510
511    #[test]
512    fn test_ordering_by_sort_key() -> Result<()> {
513        let config1 = parse_bls_config(
514            r#"
515            title Entry 1
516            version 3
517            sort-key a
518            linux /vmlinuz-3
519            initrd /initrd-3
520            options opt1
521        "#,
522        )?;
523
524        let config2 = parse_bls_config(
525            r#"
526            title Entry 2
527            version 5
528            sort-key b
529            linux /vmlinuz-5
530            initrd /initrd-5
531            options opt2
532        "#,
533        )?;
534
535        assert!(config1 < config2);
536        Ok(())
537    }
538
539    #[test]
540    fn test_ordering_by_sort_key_and_version() -> Result<()> {
541        let config1 = parse_bls_config(
542            r#"
543            title Entry 1
544            version 3
545            sort-key a
546            linux /vmlinuz-3
547            initrd /initrd-3
548            options opt1
549        "#,
550        )?;
551
552        let config2 = parse_bls_config(
553            r#"
554            title Entry 2
555            version 5
556            sort-key a
557            linux /vmlinuz-5
558            initrd /initrd-5
559            options opt2
560        "#,
561        )?;
562
563        assert!(config1 > config2);
564        Ok(())
565    }
566
567    #[test]
568    fn test_ordering_by_machine_id() -> Result<()> {
569        let config1 = parse_bls_config(
570            r#"
571            title Entry 1
572            version 3
573            machine-id a
574            linux /vmlinuz-3
575            initrd /initrd-3
576            options opt1
577        "#,
578        )?;
579
580        let config2 = parse_bls_config(
581            r#"
582            title Entry 2
583            version 5
584            machine-id b
585            linux /vmlinuz-5
586            initrd /initrd-5
587            options opt2
588        "#,
589        )?;
590
591        assert!(config1 < config2);
592        Ok(())
593    }
594
595    #[test]
596    fn test_ordering_by_machine_id_and_version() -> Result<()> {
597        let config1 = parse_bls_config(
598            r#"
599            title Entry 1
600            version 3
601            machine-id a
602            linux /vmlinuz-3
603            initrd /initrd-3
604            options opt1
605        "#,
606        )?;
607
608        let config2 = parse_bls_config(
609            r#"
610            title Entry 2
611            version 5
612            machine-id a
613            linux /vmlinuz-5
614            initrd /initrd-5
615            options opt2
616        "#,
617        )?;
618
619        assert!(config1 > config2);
620        Ok(())
621    }
622
623    #[test]
624    fn test_ordering_by_nontrivial_version() -> Result<()> {
625        let config_final = parse_bls_config(
626            r#"
627            title Entry 1
628            version 1.0
629            linux /vmlinuz-1
630            initrd /initrd-1
631        "#,
632        )?;
633
634        let config_rc1 = parse_bls_config(
635            r#"
636            title Entry 2
637            version 1.0~rc1
638            linux /vmlinuz-2
639            initrd /initrd-2
640        "#,
641        )?;
642
643        // In a sorted list, we want 1.0 to appear before 1.0~rc1 because
644        // versions are sorted descending. This means that in Rust's sort order,
645        // config_final should be "less than" config_rc1.
646        assert!(config_final < config_rc1);
647        Ok(())
648    }
649
650    #[test]
651    fn test_boot_artifact_name_efi_success() -> Result<()> {
652        use camino::Utf8PathBuf;
653
654        let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234.efi");
655        let config = BLSConfig {
656            cfg_type: BLSConfigType::EFI { efi: efi_path },
657            version: "1".to_string(),
658            ..Default::default()
659        };
660
661        let artifact_name = config.boot_artifact_name()?;
662        assert_eq!(artifact_name, "abcd1234");
663        Ok(())
664    }
665
666    #[test]
667    fn test_boot_artifact_name_non_efi_success() -> Result<()> {
668        use camino::Utf8PathBuf;
669
670        let linux_path = Utf8PathBuf::from("/boot/bootc_composefs-xyz5678/vmlinuz");
671        let config = BLSConfig {
672            cfg_type: BLSConfigType::NonEFI {
673                linux: linux_path,
674                initrd: vec![],
675                options: None,
676            },
677            version: "1".to_string(),
678            ..Default::default()
679        };
680
681        let artifact_name = config.boot_artifact_name()?;
682        assert_eq!(artifact_name, "xyz5678");
683        Ok(())
684    }
685
686    #[test]
687    fn test_boot_artifact_name_efi_missing_prefix() {
688        use camino::Utf8PathBuf;
689
690        let efi_path = Utf8PathBuf::from("/EFI/Linux/abcd1234.efi");
691        let config = BLSConfig {
692            cfg_type: BLSConfigType::EFI { efi: efi_path },
693            version: "1".to_string(),
694            ..Default::default()
695        };
696
697        let artifact_name = config
698            .boot_artifact_name()
699            .expect("Should extract artifact name");
700        assert_eq!(artifact_name, "abcd1234");
701    }
702
703    #[test]
704    fn test_boot_artifact_name_efi_missing_suffix() {
705        use camino::Utf8PathBuf;
706
707        let efi_path = Utf8PathBuf::from("bootc_composefs-abcd1234");
708        let config = BLSConfig {
709            cfg_type: BLSConfigType::EFI { efi: efi_path },
710            version: "1".to_string(),
711            ..Default::default()
712        };
713
714        let result = config.boot_artifact_name();
715        assert!(result.is_err());
716        assert!(
717            result
718                .unwrap_err()
719                .to_string()
720                .contains("missing expected suffix")
721        );
722    }
723
724    #[test]
725    fn test_boot_artifact_name_efi_no_filename() {
726        use camino::Utf8PathBuf;
727
728        let efi_path = Utf8PathBuf::from("/");
729        let config = BLSConfig {
730            cfg_type: BLSConfigType::EFI { efi: efi_path },
731            version: "1".to_string(),
732            ..Default::default()
733        };
734
735        let result = config.boot_artifact_name();
736        assert!(result.is_err());
737        assert!(
738            result
739                .unwrap_err()
740                .to_string()
741                .contains("missing file name")
742        );
743    }
744
745    #[test]
746    fn test_boot_artifact_name_unknown_type() {
747        let config = BLSConfig {
748            cfg_type: BLSConfigType::Unknown,
749            version: "1".to_string(),
750            ..Default::default()
751        };
752
753        let result = config.boot_artifact_name();
754        assert!(result.is_err());
755        assert!(
756            result
757                .unwrap_err()
758                .to_string()
759                .contains("unknown config type")
760        );
761    }
762    #[test]
763    fn test_boot_artifact_name_efi_nested_path() -> Result<()> {
764        let efi_path = Utf8PathBuf::from("/EFI/Linux/bootc/bootc_composefs-deadbeef01234567.efi");
765        let config = BLSConfig {
766            cfg_type: BLSConfigType::EFI { efi: efi_path },
767            version: "1".to_string(),
768            ..Default::default()
769        };
770
771        assert_eq!(config.boot_artifact_name()?, "deadbeef01234567");
772        Ok(())
773    }
774
775    #[test]
776    fn test_boot_artifact_name_non_efi_deep_path() -> Result<()> {
777        // Realistic Type1 path: /boot/bootc_composefs-<digest>/vmlinuz
778        let digest = "7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6";
779        let linux_path = Utf8PathBuf::from(format!("/boot/bootc_composefs-{digest}/vmlinuz"));
780        let config = BLSConfig {
781            cfg_type: BLSConfigType::NonEFI {
782                linux: linux_path,
783                initrd: vec![],
784                options: None,
785            },
786            version: "1".to_string(),
787            ..Default::default()
788        };
789
790        assert_eq!(config.boot_artifact_name()?, digest);
791        Ok(())
792    }
793
794    /// Test boot_artifact_name from parsed EFI config
795    #[test]
796    fn test_boot_artifact_name_from_parsed_efi_config() -> Result<()> {
797        let digest = "f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346";
798        let input = format!(
799            r#"
800            title Fedora UKI
801            version 1
802            efi /EFI/Linux/bootc/bootc_composefs-{digest}.efi
803            sort-key bootc-fedora-0
804        "#
805        );
806
807        let config = parse_bls_config(&input)?;
808        assert_eq!(config.boot_artifact_name()?, digest);
809        assert_eq!(config.get_verity()?, digest);
810        Ok(())
811    }
812
813    /// Test that Non-EFI boot_artifact_name fails when linux path has no parent
814    #[test]
815    fn test_boot_artifact_name_non_efi_no_parent() {
816        let config = BLSConfig {
817            cfg_type: BLSConfigType::NonEFI {
818                linux: Utf8PathBuf::from("vmlinuz"),
819                initrd: vec![],
820                options: None,
821            },
822            version: "1".to_string(),
823            ..Default::default()
824        };
825
826        let result = config.boot_artifact_name();
827        assert!(result.is_err());
828    }
829}