Skip to main content

bootc_lib/parsers/
grub_menuconfig.rs

1//! Parser for GRUB menuentry configuration files using nom combinators.
2
3use std::fmt::Display;
4
5use anyhow::Result;
6use camino::Utf8PathBuf;
7use composefs_boot::bootloader::EFI_EXT;
8use composefs_ctl::composefs_boot;
9use nom::{
10    Err, IResult, Parser,
11    bytes::complete::{escaped, tag, take_until},
12    character::complete::{multispace0, multispace1, none_of},
13    error::{Error, ErrorKind, ParseError},
14    sequence::delimited,
15};
16
17use crate::{
18    bootc_composefs::boot::{BOOTC_UKI_DIR, get_uki_name},
19    composefs_consts::UKI_NAME_PREFIX,
20};
21
22/// Body content of a GRUB menuentry containing parsed commands.
23#[derive(Debug, PartialEq, Eq, Clone)]
24pub(crate) struct MenuentryBody<'a> {
25    /// Kernel modules to load
26    pub(crate) insmod: Vec<&'a str>,
27    /// Chainloader path (optional)
28    pub(crate) chainloader: String,
29    /// Search command (optional)
30    pub(crate) search: &'a str,
31    /// The version
32    pub(crate) version: u8,
33    /// Additional commands
34    pub(crate) extra: Vec<(&'a str, &'a str)>,
35}
36
37impl<'a> Display for MenuentryBody<'a> {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        for insmod in &self.insmod {
40            writeln!(f, "insmod {}", insmod)?;
41        }
42
43        writeln!(f, "search {}", self.search)?;
44        writeln!(f, "chainloader {}", self.chainloader)?;
45
46        for (k, v) in &self.extra {
47            writeln!(f, "{k} {v}")?;
48        }
49
50        Ok(())
51    }
52}
53
54impl<'a> From<Vec<(&'a str, &'a str)>> for MenuentryBody<'a> {
55    fn from(vec: Vec<(&'a str, &'a str)>) -> Self {
56        let mut entry = Self {
57            insmod: vec![],
58            chainloader: "".into(),
59            search: "",
60            version: 0,
61            extra: vec![],
62        };
63
64        for (key, value) in vec {
65            match key {
66                "insmod" => entry.insmod.push(value),
67                "chainloader" => entry.chainloader = value.into(),
68                "search" => entry.search = value,
69                "set" => {}
70                _ => entry.extra.push((key, value)),
71            }
72        }
73
74        entry
75    }
76}
77
78/// A complete GRUB menuentry with title and body commands.
79#[derive(Debug, PartialEq, Eq, Clone)]
80pub(crate) struct MenuEntry<'a> {
81    /// Display title (supports escaped quotes)
82    pub(crate) title: String,
83    /// Commands within the menuentry block
84    pub(crate) body: MenuentryBody<'a>,
85}
86
87impl<'a> Display for MenuEntry<'a> {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        writeln!(f, "menuentry \"{}\" {{", self.title)?;
90        write!(f, "{}", self.body)?;
91        writeln!(f, "}}")
92    }
93}
94
95impl<'a> MenuEntry<'a> {
96    pub(crate) fn new(boot_label: &str, uki_id: &str) -> Self {
97        Self {
98            title: format!("{boot_label}: ({uki_id})"),
99            body: MenuentryBody {
100                insmod: vec!["fat", "chain"],
101                chainloader: format!("/{BOOTC_UKI_DIR}/{}", get_uki_name(uki_id)),
102                search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
103                version: 0,
104                extra: vec![],
105            },
106        }
107    }
108
109    pub(crate) fn get_verity(&self) -> Result<String> {
110        let to_path = Utf8PathBuf::from(self.body.chainloader.clone());
111
112        let name = to_path
113            .components()
114            .last()
115            .ok_or_else(|| anyhow::anyhow!("Empty efi field"))?
116            .to_string()
117            .strip_prefix(UKI_NAME_PREFIX)
118            .ok_or_else(|| anyhow::anyhow!("efi does not start with custom prefix"))?
119            .strip_suffix(EFI_EXT)
120            .ok_or_else(|| anyhow::anyhow!("efi doesn't end with .efi"))?
121            .to_string();
122
123        Ok(name)
124    }
125
126    /// Returns name of UKI in case of EFI config
127    ///
128    /// The names are stripped of our custom prefix and suffixes, so this returns
129    /// the verity digest part of the name
130    pub(crate) fn boot_artifact_name(&self) -> Result<String> {
131        Ok(self.boot_artifact_info()?.0)
132    }
133
134    pub(crate) fn boot_artifact_info(&self) -> Result<(String, bool)> {
135        let chainloader_path = Utf8PathBuf::from(&self.body.chainloader);
136
137        let file_name = chainloader_path.file_name().ok_or_else(|| {
138            anyhow::anyhow!(
139                "Chainloader path missing file name: {}",
140                &self.body.chainloader
141            )
142        })?;
143
144        let without_suffix = file_name.strip_suffix(EFI_EXT).ok_or_else(|| {
145            anyhow::anyhow!(
146                "EFI file name missing expected suffix '{}': {}",
147                EFI_EXT,
148                file_name
149            )
150        })?;
151
152        // For backwards compatibility, we don't make this prefix mandatory
153        match without_suffix.strip_prefix(UKI_NAME_PREFIX) {
154            Some(no_prefix) => Ok((no_prefix.into(), true)),
155            None => Ok((without_suffix.into(), false)),
156        }
157    }
158}
159
160/// Parser that takes content until balanced brackets, handling nested brackets and escapes.
161fn take_until_balanced_allow_nested(
162    opening_bracket: char,
163    closing_bracket: char,
164) -> impl Fn(&str) -> IResult<&str, &str> {
165    move |i: &str| {
166        let mut index = 0;
167        let mut bracket_counter = 0;
168
169        while let Some(n) = &i[index..].find(&[opening_bracket, closing_bracket, '\\'][..]) {
170            index += n;
171            let mut characters = i[index..].chars();
172
173            match characters.next().unwrap_or_default() {
174                c if c == '\\' => {
175                    // Skip '\'
176                    index += '\\'.len_utf8();
177                    // Skip char following '\'
178                    let c = characters.next().unwrap_or_default();
179                    index += c.len_utf8();
180                }
181
182                c if c == opening_bracket => {
183                    bracket_counter += 1;
184                    index += opening_bracket.len_utf8();
185                }
186
187                c if c == closing_bracket => {
188                    bracket_counter -= 1;
189                    index += closing_bracket.len_utf8();
190                }
191
192                // Should not happen
193                _ => unreachable!(),
194            };
195
196            // We found the unmatched closing bracket.
197            if bracket_counter == -1 {
198                // Don't consume it as we'll "tag" it afterwards
199                index -= closing_bracket.len_utf8();
200                return Ok((&i[index..], &i[0..index]));
201            };
202        }
203
204        if bracket_counter == 0 {
205            Ok(("", i))
206        } else {
207            Err(Err::Error(Error::from_error_kind(i, ErrorKind::TakeUntil)))
208        }
209    }
210}
211
212/// Parses a single menuentry with title and body commands.
213fn parse_menuentry(input: &str) -> IResult<&str, MenuEntry<'_>> {
214    let (input, _) = tag("menuentry").parse(input)?;
215
216    // Require at least one space after "menuentry"
217    let (input, _) = multispace1.parse(input)?;
218    // Eat up the title, handling escaped quotes
219    let (input, title) = delimited(
220        tag("\""),
221        escaped(none_of("\\\""), '\\', none_of("")),
222        tag("\""),
223    )
224    .parse(input)?;
225
226    // Skip any whitespace after title
227    let (input, _) = multispace0.parse(input)?;
228
229    // Eat up everything inside { .. }
230    let (input, body) = delimited(
231        tag("{"),
232        take_until_balanced_allow_nested('{', '}'),
233        tag("}"),
234    )
235    .parse(input)?;
236
237    let mut map = vec![];
238
239    for line in body.lines() {
240        let line = line.trim();
241
242        if line.is_empty() || line.starts_with('#') {
243            continue;
244        }
245
246        if let Some((key, value)) = line.split_once(' ') {
247            map.push((key, value.trim()));
248        }
249    }
250
251    Ok((
252        input,
253        MenuEntry {
254            title: title.to_string(),
255            body: MenuentryBody::from(map),
256        },
257    ))
258}
259
260/// Skips content until finding "menuentry" keyword or end of input.
261fn skip_to_menuentry(input: &str) -> IResult<&str, ()> {
262    let (input, _) = take_until("menuentry")(input)?;
263    Ok((input, ()))
264}
265
266/// Parses all menuentries from a GRUB configuration file.
267fn parse_all(input: &str) -> IResult<&str, Vec<MenuEntry<'_>>> {
268    let mut remaining = input;
269    let mut entries = Vec::new();
270
271    // Skip any content before the first menuentry
272    let Ok((new_input, _)) = skip_to_menuentry(remaining) else {
273        return Ok(("", Default::default()));
274    };
275    remaining = new_input;
276
277    while !remaining.trim().is_empty() {
278        let (new_input, entry) = parse_menuentry(remaining)?;
279        entries.push(entry);
280        remaining = new_input;
281
282        // Skip whitespace and try to find next menuentry
283        let (ws_input, _) = multispace0(remaining)?;
284        remaining = ws_input;
285
286        if let Ok((next_input, _)) = skip_to_menuentry(remaining) {
287            remaining = next_input;
288        } else if !remaining.trim().is_empty() {
289            // No more menuentries found, but content remains
290            break;
291        }
292    }
293
294    Ok((remaining, entries))
295}
296
297/// Main entry point for parsing GRUB menuentry files.
298pub(crate) fn parse_grub_menuentry_file(contents: &str) -> anyhow::Result<Vec<MenuEntry<'_>>> {
299    let (_, entries) = parse_all(&contents)
300        .map_err(|e| anyhow::anyhow!("Failed to parse GRUB menuentries: {e}"))?;
301    // Validate that entries have reasonable structure
302    for entry in &entries {
303        if entry.title.is_empty() {
304            anyhow::bail!("Found menuentry with empty title");
305        }
306    }
307
308    Ok(entries)
309}
310
311#[cfg(test)]
312mod test {
313    use super::*;
314
315    #[test]
316    fn test_menuconfig_parser() {
317        let menuentry = r#"
318            if [ -f ${config_directory}/efiuuid.cfg ]; then
319                    source ${config_directory}/efiuuid.cfg
320            fi
321
322            # Skip this comment
323
324            menuentry "Fedora 42: (Verity-42)" {
325                insmod fat
326                insmod chain
327                # This should also be skipped
328                search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
329                chainloader /EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi
330            }
331
332            menuentry "Fedora 43: (Verity-43)" {
333                insmod fat
334                insmod chain
335                search --no-floppy --set=root --fs-uuid "${EFI_PART_UUID}"
336                chainloader /EFI/Linux/uki.efi
337                extra_field1 this is extra
338                extra_field2 this is also extra
339            }
340        "#;
341
342        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
343
344        let expected = vec![
345            MenuEntry {
346                title: "Fedora 42: (Verity-42)".into(),
347                body: MenuentryBody {
348                    insmod: vec!["fat", "chain"],
349                    search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
350                    chainloader: "/EFI/Linux/7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6.efi".into(),
351                    version: 0,
352                    extra: vec![],
353                },
354            },
355            MenuEntry {
356                title: "Fedora 43: (Verity-43)".into(),
357                body: MenuentryBody {
358                    insmod: vec!["fat", "chain"],
359                    search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
360                    chainloader: "/EFI/Linux/uki.efi".into(),
361                    version: 0,
362                    extra: vec![
363                        ("extra_field1", "this is extra"), 
364                        ("extra_field2", "this is also extra")
365                    ]
366                },
367            },
368        ];
369
370        println!("{}", expected[0]);
371
372        assert_eq!(result, expected);
373    }
374
375    #[test]
376    fn test_escaped_quotes_in_title() {
377        let menuentry = r#"
378            menuentry "Title with \"escaped quotes\" inside" {
379                insmod fat
380                chainloader /EFI/Linux/test.efi
381            }
382        "#;
383
384        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
385
386        assert_eq!(result.len(), 1);
387        assert_eq!(result[0].title, "Title with \\\"escaped quotes\\\" inside");
388        assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
389    }
390
391    #[test]
392    fn test_multiple_escaped_quotes() {
393        let menuentry = r#"
394            menuentry "Test \"first\" and \"second\" quotes" {
395                insmod fat
396                chainloader /EFI/Linux/test.efi
397            }
398        "#;
399
400        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
401
402        assert_eq!(result.len(), 1);
403        assert_eq!(
404            result[0].title,
405            "Test \\\"first\\\" and \\\"second\\\" quotes"
406        );
407    }
408
409    #[test]
410    fn test_escaped_backslash_in_title() {
411        let menuentry = r#"
412            menuentry "Path with \\ backslash" {
413                insmod fat
414                chainloader /EFI/Linux/test.efi
415            }
416        "#;
417
418        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
419
420        assert_eq!(result.len(), 1);
421        assert_eq!(result[0].title, "Path with \\\\ backslash");
422    }
423
424    #[test]
425    fn test_minimal_menuentry() {
426        let menuentry = r#"
427            menuentry "Minimal Entry" {
428                # Just a comment
429            }
430        "#;
431
432        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
433
434        assert_eq!(result.len(), 1);
435        assert_eq!(result[0].title, "Minimal Entry");
436        assert_eq!(result[0].body.insmod.len(), 0);
437        assert_eq!(result[0].body.chainloader, "");
438        assert_eq!(result[0].body.search, "");
439        assert_eq!(result[0].body.extra.len(), 0);
440    }
441
442    #[test]
443    fn test_menuentry_with_only_insmod() {
444        let menuentry = r#"
445            menuentry "Insmod Only" {
446                insmod fat
447                insmod chain
448                insmod ext2
449            }
450        "#;
451
452        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
453
454        assert_eq!(result.len(), 1);
455        assert_eq!(result[0].body.insmod, vec!["fat", "chain", "ext2"]);
456        assert_eq!(result[0].body.chainloader, "");
457        assert_eq!(result[0].body.search, "");
458    }
459
460    #[test]
461    fn test_menuentry_with_set_commands_ignored() {
462        let menuentry = r#"
463            menuentry "With Set Commands" {
464                set timeout=5
465                set root=(hd0,1)
466                insmod fat
467                chainloader /EFI/Linux/test.efi
468            }
469        "#;
470
471        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
472
473        assert_eq!(result.len(), 1);
474        assert_eq!(result[0].body.insmod, vec!["fat"]);
475        assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
476        // set commands should be ignored
477        assert!(!result[0].body.extra.iter().any(|(k, _)| k == &"set"));
478    }
479
480    #[test]
481    fn test_nested_braces_in_body() {
482        let menuentry = r#"
483            menuentry "Nested Braces" {
484                if [ -f ${config_directory}/test.cfg ]; then
485                    source ${config_directory}/test.cfg
486                fi
487                insmod fat
488                chainloader /EFI/Linux/test.efi
489            }
490        "#;
491
492        let result = parse_grub_menuentry_file(menuentry).expect("Expected parsed entries");
493
494        assert_eq!(result.len(), 1);
495        assert_eq!(result[0].title, "Nested Braces");
496        assert_eq!(result[0].body.insmod, vec!["fat"]);
497        assert_eq!(result[0].body.chainloader, "/EFI/Linux/test.efi");
498        // The if/fi block should be captured as extra commands
499        assert!(result[0].body.extra.iter().any(|(k, _)| k == &"if"));
500    }
501
502    #[test]
503    fn test_empty_file() {
504        let result = parse_grub_menuentry_file("").expect("Should handle empty file");
505        assert_eq!(result.len(), 0);
506    }
507
508    #[test]
509    fn test_file_with_no_menuentries() {
510        let content = r#"
511            # Just comments and other stuff
512            set timeout=10
513            if [ -f /boot/grub/custom.cfg ]; then
514                source /boot/grub/custom.cfg
515            fi
516        "#;
517
518        let result =
519            parse_grub_menuentry_file(content).expect("Should handle file with no menuentries");
520        assert_eq!(result.len(), 0);
521    }
522
523    #[test]
524    fn test_malformed_menuentry_missing_quote() {
525        let menuentry = r#"
526            menuentry "Missing closing quote {
527                insmod fat
528            }
529        "#;
530
531        let result = parse_grub_menuentry_file(menuentry);
532        assert!(result.is_err(), "Should fail on malformed menuentry");
533    }
534
535    #[test]
536    fn test_malformed_menuentry_missing_brace() {
537        let menuentry = r#"
538            menuentry "Missing Brace" {
539                insmod fat
540                chainloader /EFI/Linux/test.efi
541            // Missing closing brace
542        "#;
543
544        let result = parse_grub_menuentry_file(menuentry);
545        assert!(result.is_err(), "Should fail on unbalanced braces");
546    }
547
548    #[test]
549    fn test_multiple_menuentries_with_content_between() {
550        let content = r#"
551            # Some initial config
552            set timeout=10
553            
554            menuentry "First Entry" {
555                insmod fat
556                chainloader /EFI/Linux/first.efi
557            }
558            
559            # Some comments between entries
560            set default=0
561            
562            menuentry "Second Entry" {
563                insmod ext2
564                search --set=root --fs-uuid "some-uuid"
565                chainloader /EFI/Linux/second.efi
566            }
567            
568            # Trailing content
569        "#;
570
571        let result = parse_grub_menuentry_file(content)
572            .expect("Should parse multiple entries with content between");
573
574        assert_eq!(result.len(), 2);
575        assert_eq!(result[0].title, "First Entry");
576        assert_eq!(result[0].body.chainloader, "/EFI/Linux/first.efi");
577        assert_eq!(result[1].title, "Second Entry");
578        assert_eq!(result[1].body.chainloader, "/EFI/Linux/second.efi");
579        assert_eq!(result[1].body.search, "--set=root --fs-uuid \"some-uuid\"");
580    }
581
582    #[test]
583    fn test_menuentry_boot_artifact_name_success() {
584        let body = MenuentryBody {
585            insmod: vec!["fat", "chain"],
586            chainloader: "/EFI/Linux/bootc/bootc_composefs-abcd1234.efi".to_string(),
587            search: "--no-floppy --set=root --fs-uuid test",
588            version: 0,
589            extra: vec![],
590        };
591
592        let entry = MenuEntry {
593            title: "Test Entry".to_string(),
594            body,
595        };
596
597        let artifact_name = entry
598            .boot_artifact_name()
599            .expect("Should extract artifact name");
600        assert_eq!(artifact_name, "abcd1234");
601    }
602
603    #[test]
604    fn test_menuentry_boot_artifact_name_missing_prefix() {
605        let body = MenuentryBody {
606            insmod: vec!["fat", "chain"],
607            chainloader: "/EFI/Linux/abcd1234.efi".to_string(),
608            search: "--no-floppy --set=root --fs-uuid test",
609            version: 0,
610            extra: vec![],
611        };
612
613        let entry = MenuEntry {
614            title: "Test Entry".to_string(),
615            body,
616        };
617
618        let artifact_name = entry
619            .boot_artifact_name()
620            .expect("Should extract artifact name");
621        assert_eq!(artifact_name, "abcd1234");
622    }
623
624    #[test]
625    fn test_menuentry_boot_artifact_name_missing_suffix() {
626        let body = MenuentryBody {
627            insmod: vec!["fat", "chain"],
628            chainloader: "/EFI/Linux/bootc/bootc_composefs-abcd1234".to_string(),
629            search: "--no-floppy --set=root --fs-uuid test",
630            version: 0,
631            extra: vec![],
632        };
633
634        let entry = MenuEntry {
635            title: "Test Entry".to_string(),
636            body,
637        };
638
639        let result = entry.boot_artifact_name();
640        assert!(result.is_err());
641        assert!(
642            result
643                .unwrap_err()
644                .to_string()
645                .contains("missing expected suffix")
646        );
647    }
648
649    #[test]
650    fn test_menuentry_boot_artifact_name_empty_chainloader() {
651        let body = MenuentryBody {
652            insmod: vec![],
653            chainloader: "".to_string(),
654            search: "",
655            version: 0,
656            extra: vec![],
657        };
658
659        let entry = MenuEntry {
660            title: "Empty".to_string(),
661            body,
662        };
663
664        let result = entry.boot_artifact_name();
665        assert!(result.is_err());
666    }
667
668    /// Test that boot_artifact_name and get_verity return the same value
669    /// for a standard UKI entry.
670    ///
671    /// Note: GRUB/UKI entries always have matching boot_artifact_name and
672    /// get_verity because both derive from the same chainloader path. The
673    /// shared-entry divergence (where boot_artifact_name != get_verity) only
674    /// applies to Type1 BLS entries, which have separate linux path and
675    /// composefs= cmdline parameter.
676    #[test]
677    fn test_menuentry_boot_artifact_name_matches_get_verity() {
678        let digest = "f7415d75017a12a387a39d2281e033a288fc15775108250ef70a01dcadb93346";
679
680        let body = MenuentryBody {
681            insmod: vec!["fat", "chain"],
682            chainloader: format!("/EFI/Linux/bootc/bootc_composefs-{digest}.efi"),
683            search: "--no-floppy --set=root --fs-uuid test",
684            version: 0,
685            extra: vec![],
686        };
687
688        let entry = MenuEntry {
689            title: "Test".to_string(),
690            body,
691        };
692
693        let artifact_name = entry.boot_artifact_name().unwrap();
694        let verity = entry.get_verity().unwrap();
695        assert_eq!(artifact_name, verity);
696        assert_eq!(artifact_name, digest);
697    }
698
699    /// Test boot_artifact_name with realistic full-length hex digest
700    #[test]
701    fn test_menuentry_boot_artifact_name_full_digest() {
702        let digest = "7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6";
703
704        let body = MenuentryBody {
705            insmod: vec!["fat", "chain"],
706            chainloader: format!("/EFI/Linux/bootc/bootc_composefs-{digest}.efi"),
707            search: "--no-floppy --set=root --fs-uuid \"${EFI_PART_UUID}\"",
708            version: 0,
709            extra: vec![],
710        };
711
712        let entry = MenuEntry {
713            title: format!("Fedora Bootc UKI: ({digest})"),
714            body,
715        };
716
717        assert_eq!(entry.boot_artifact_name().unwrap(), digest);
718    }
719
720    /// Test boot_artifact_name via MenuEntry::new constructor
721    #[test]
722    fn test_menuentry_new_boot_artifact_name() {
723        let uki_id = "abc123def456";
724        let entry = MenuEntry::new("Fedora 42", uki_id);
725
726        assert_eq!(entry.boot_artifact_name().unwrap(), uki_id);
727        assert_eq!(entry.get_verity().unwrap(), uki_id);
728    }
729
730    /// Test boot_artifact_name from a parsed grub config
731    #[test]
732    fn test_menuentry_boot_artifact_name_from_parsed() {
733        let digest = "7e11ac46e3e022053e7226a20104ac656bf72d1a84e3a398b7cce70e9df188b6";
734        let menuentry = format!(
735            r#"
736            menuentry "Fedora 42: ({digest})" {{
737                insmod fat
738                insmod chain
739                search --no-floppy --set=root --fs-uuid "${{EFI_PART_UUID}}"
740                chainloader /EFI/Linux/bootc/bootc_composefs-{digest}.efi
741            }}
742        "#
743        );
744
745        let result = parse_grub_menuentry_file(&menuentry).unwrap();
746        assert_eq!(result.len(), 1);
747        assert_eq!(result[0].boot_artifact_name().unwrap(), digest);
748        assert_eq!(result[0].get_verity().unwrap(), digest);
749    }
750}