Skip to main content

boot_loader_spec/
lib.rs

1// bls.rs
2//
3// Copyright 2022 Alberto Ruiz <aruiz@gnome.org>
4//
5// This Source Code Form is subject to the terms of the Mozilla Public
6// License, v. 2.0. If a copy of the MPL was not distributed with this
7// file, You can obtain one at https://mozilla.org/MPL/2.0/.
8//
9// SPDX-License-Identifier: MPL-2.0
10
11//! Parse and modify [Boot Loader Specification](https://uapi-group.org/specifications/specs/boot_loader_specification/) (BLS) entry files.
12//!
13//! This library implements the UAPI Boot Loader Specification (Type #1 entries) and
14//! [Fedora/GRUB extensions](https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault)
15//! (`grub_class`, `grub_users`, `grub_hotkey`, `grub_arg`). Keys `uki`, `uki-url`, and
16//! `profile` from the spec are not yet supported.
17//!
18//! # Usage
19//!
20//! Parse a BLS snippet, optionally modify it with [`BLSEntry::set`] or [`BLSEntry::clear`],
21//! then serialize with [`BLSEntry::render`].
22//!
23//! # Compatibility
24//!
25//! Both hyphenated (`machine-id`, `sort-key`, `devicetree-overlay`) and underscore forms
26//! are accepted when parsing. Output uses the spec’s hyphenated form for those keys.
27//! An entry must contain at least one of `linux` or `efi`.
28//!
29//! # no_std
30//!
31//! Disable the default "std" feature with `--no-default-features` for a `no_std` build
32//! (requires `alloc`).
33//!
34//! **Note:** Full-line comments are moved to the header when re-rendering; order of
35//! commands and comments is not preserved.
36
37#![cfg_attr(not(feature = "std"), no_std)]
38#[cfg(not(feature = "std"))]
39extern crate alloc;
40
41#[cfg(not(feature = "std"))]
42use alloc::format;
43#[cfg(not(feature = "std"))]
44use alloc::string::String;
45#[cfg(not(feature = "std"))]
46use alloc::vec::Vec;
47
48#[cfg(not(feature = "std"))]
49use alloc::str::FromStr;
50#[cfg(feature = "std")]
51use std::str::FromStr;
52
53/// A BLS key value, optionally with an inline comment (text after `#` on the same line).
54#[derive(Debug, PartialEq)]
55pub enum BLSValue {
56    /// The argument string for the BLS command.
57    Value(String),
58    /// The argument string and a trailing comment (e.g. `value # comment`).
59    ValueWithComment(String, String),
60}
61
62/// Policy for [`BLSEntry::set`] when the key allows multiple values (e.g. `initrd`, `options`, `grub_class`).
63#[derive(Debug, PartialEq)]
64pub enum ValueSetPolicy {
65    /// Replace all existing values with this one.
66    ReplaceAll,
67    /// Append after existing values.
68    Append,
69    /// Insert at the beginning.
70    Prepend,
71    /// Insert at the given index; may panic if index is out of bounds (see `Vec::insert`).
72    InsertAt(usize),
73}
74
75/// BLS and Fedora/GRUB entry keys. Parse from key strings via `FromStr` (e.g. `"linux"`, `"machine-id"`).
76#[derive(Debug, PartialEq)]
77pub enum BLSKey {
78    /// Human-readable menu title.
79    Title,
80    /// Version string (e.g. kernel version).
81    Version,
82    /// Machine ID (32 hex chars).
83    MachineId,
84    /// Sort key for menu ordering.
85    SortKey,
86    /// Linux kernel image path (required unless `efi` is set).
87    Linux,
88    /// EFI program path.
89    Efi,
90    /// Initrd path (may appear multiple times).
91    Initrd,
92    /// Kernel/command-line options (may appear multiple times).
93    Options,
94    /// Device tree path.
95    Devicetree,
96    /// Device tree overlay path(s).
97    DevicetreeOverlay,
98    /// Architecture (e.g. `x64`, `aa64`).
99    Architecture,
100    /// Fedora/GRUB: hotkey for the entry.
101    GrubHotkey,
102    /// Fedora/GRUB: users allowed to boot this entry.
103    GrubUsers,
104    /// Fedora/GRUB: menu class (may appear multiple times).
105    GrubClass,
106    /// Fedora/GRUB: extra argument.
107    GrubArg,
108}
109
110impl FromStr for BLSKey {
111    type Err = String;
112
113    fn from_str(key: &str) -> Result<Self, Self::Err> {
114        match key {
115            "linux" => Ok(BLSKey::Linux),
116            "title" => Ok(BLSKey::Title),
117            "version" => Ok(BLSKey::Version),
118            "machine_id" | "machine-id" => Ok(BLSKey::MachineId),
119            "sort_key" | "sort-key" => Ok(BLSKey::SortKey),
120            "efi" => Ok(BLSKey::Efi),
121            "initrd" => Ok(BLSKey::Initrd),
122            "options" => Ok(BLSKey::Options),
123            "devicetree" => Ok(BLSKey::Devicetree),
124            "devicetree_overlay" | "devicetree-overlay" => Ok(BLSKey::DevicetreeOverlay),
125            "architecture" => Ok(BLSKey::Architecture),
126            "grub_hotkey" => Ok(BLSKey::GrubHotkey),
127            "grub_users" => Ok(BLSKey::GrubUsers),
128            "grub_class" => Ok(BLSKey::GrubClass),
129            "grub_arg" => Ok(BLSKey::GrubArg),
130            _ => Err(format!("Invalid key {}", key)),
131        }
132    }
133}
134
135/// A parsed Boot Loader Spec (Type #1) entry. Holds all standard and Fedora/GRUB keys.
136///
137/// Use [`BLSEntry::parse`] to parse from text and [`BLSEntry::render`] to serialize.
138#[derive(Debug)]
139pub struct BLSEntry {
140    /// Optional title (e.g. from `PRETTY_NAME`).
141    pub title: Option<BLSValue>,
142    /// Optional version (e.g. kernel version).
143    pub version: Option<BLSValue>,
144    /// Optional machine ID.
145    pub machine_id: Option<BLSValue>,
146    /// Optional sort key.
147    pub sort_key: Option<BLSValue>,
148    /// Kernel image path; always present (empty if entry is efi-only).
149    pub linux: BLSValue,
150    /// Optional EFI program path.
151    pub efi: Option<BLSValue>,
152    /// Initrd paths (multiple allowed).
153    pub initrd: Vec<BLSValue>,
154    /// Kernel/command-line options (multiple allowed).
155    pub options: Vec<BLSValue>,
156    /// Optional devicetree path.
157    pub devicetree: Option<BLSValue>,
158    /// Optional devicetree overlay path(s).
159    pub devicetree_overlay: Option<BLSValue>,
160    /// Optional architecture.
161    pub architecture: Option<BLSValue>,
162    /// Fedora/GRUB: optional hotkey.
163    pub grub_hotkey: Option<BLSValue>,
164    /// Fedora/GRUB: optional users.
165    pub grub_users: Option<BLSValue>,
166    /// Fedora/GRUB: menu classes (multiple allowed).
167    pub grub_class: Vec<BLSValue>,
168    /// Fedora/GRUB: optional extra argument.
169    pub grub_arg: Option<BLSValue>,
170    /// Full-line comments; when rendering they are output at the top.
171    pub comments: Vec<String>,
172}
173
174impl BLSEntry {
175    /// Creates an empty entry. Optional fields are `None`; `linux` is an empty string.
176    pub fn new() -> BLSEntry {
177        BLSEntry {
178            title: None,
179            version: None,
180            machine_id: None,
181            sort_key: None,
182            linux: BLSValue::Value(String::new()),
183            efi: None,
184            initrd: Vec::new(),
185            options: Vec::new(),
186            devicetree: None,
187            devicetree_overlay: None,
188            architecture: None,
189            grub_hotkey: None,
190            grub_users: None,
191            grub_class: Vec::new(),
192            grub_arg: None,
193            comments: Vec::new(),
194        }
195    }
196}
197
198impl Default for BLSEntry {
199    fn default() -> Self {
200        Self::new()
201    }
202}
203
204impl BLSEntry {
205    /// Parses a BLS entry from UTF-8 text. The entry must contain at least one of `linux` or `efi`.
206    /// Returns an error if required keys are missing or an unknown key is encountered.
207    /// Comment lines are collected in `comments` and output at the header when rendering.
208    ///
209    /// # Examples
210    ///
211    /// ```
212    /// use boot_loader_spec::{BLSEntry, BLSValue};
213    ///
214    /// let text = "title Fedora\nlinux /vmlinuz\noptions root=/dev/sda1";
215    /// let entry = BLSEntry::parse(text).unwrap();
216    /// assert_eq!(entry.title.as_ref().and_then(|v| match v { BLSValue::Value(s) => Some(s.as_str()), _ => None }).unwrap(), "Fedora");
217    /// assert!(matches!(&entry.linux, BLSValue::Value(p) if p == "/vmlinuz"));
218    /// ```
219    pub fn parse(buffer: &str) -> Result<BLSEntry, String> {
220        let mut entry = BLSEntry::new();
221        let mut has_linux = false;
222        let mut has_efi = false;
223
224        for line in buffer.lines() {
225            let mut comment = None;
226            // Extract the comment string from the line
227            let line = if line.contains("#") {
228                let split: Vec<_> = line.splitn(2, "#").collect();
229                comment = Some(String::from(split[1]));
230                split[0]
231            } else {
232                line
233            };
234
235            // NOTE: For now we put all comment lines in the header
236            if line.trim().contains(" ") {
237                let key_value: Vec<&str> = line.trim().splitn(2, " ").collect();
238
239                let key = BLSKey::from_str(key_value[0])?;
240                if key == BLSKey::Linux {
241                    has_linux = true;
242                } else if key == BLSKey::Efi {
243                    has_efi = true;
244                }
245                entry.set(
246                    key,
247                    String::from(key_value[1]),
248                    comment,
249                    ValueSetPolicy::Append,
250                );
251            } else if let Some(comment) = comment {
252                entry.comments.push(comment);
253            }
254        }
255
256        if has_linux || has_efi {
257            Ok(entry)
258        } else {
259            Err(String::from("No 'linux' or 'efi' command found."))
260        }
261    }
262
263    /// Serializes the entry to BLS text (UTF-8, newline-separated lines). Comments are output first.
264    ///
265    /// # Examples
266    ///
267    /// ```
268    /// use boot_loader_spec::{BLSEntry, BLSKey, ValueSetPolicy};
269    ///
270    /// let mut entry = BLSEntry::new();
271    /// entry.set(BLSKey::Linux, "/vmlinuz".into(), None, ValueSetPolicy::ReplaceAll);
272    /// entry.set(BLSKey::Title, "My OS".into(), None, ValueSetPolicy::ReplaceAll);
273    /// let out = entry.render();
274    /// assert!(out.contains("linux /vmlinuz"));
275    /// assert!(out.contains("title My OS"));
276    /// ```
277    pub fn render(&self) -> String {
278        let mut content = String::new();
279
280        fn render_value(content: &mut String, key: &str, value: &BLSValue) {
281            content.push_str(key);
282            content.push(' ');
283            match value {
284                BLSValue::Value(value) => content.push_str(value),
285                BLSValue::ValueWithComment(value, comment) => {
286                    content.push_str(value);
287                    content.push_str(" #");
288                    content.push_str(comment);
289                }
290            }
291            content.push('\n');
292        }
293
294        fn render_single_value(content: &mut String, key: &str, value: &Option<BLSValue>) {
295            if let Some(value) = value {
296                render_value(content, key, value)
297            }
298        }
299
300        fn render_multiple_values(content: &mut String, key: &str, values: &Vec<BLSValue>) {
301            for val in values {
302                render_value(content, key, val)
303            }
304        }
305
306        // We push all comments in the header
307        for comment in &self.comments {
308            content.push('#');
309            content.push_str(comment);
310            content.push('\n');
311        }
312
313        // Mandatory commands
314        render_value(&mut content, "linux", &self.linux);
315
316        // Optional commands
317        render_single_value(&mut content, "title", &self.title);
318        render_single_value(&mut content, "version", &self.version);
319        render_single_value(&mut content, "machine-id", &self.machine_id);
320        render_single_value(&mut content, "sort-key", &self.sort_key);
321        render_single_value(&mut content, "efi", &self.efi);
322        render_single_value(&mut content, "devicetree", &self.devicetree);
323        render_single_value(&mut content, "devicetree-overlay", &self.devicetree_overlay);
324        render_single_value(&mut content, "architecture", &self.architecture);
325        render_single_value(&mut content, "grub_hotkey", &self.grub_hotkey);
326        render_single_value(&mut content, "grub_users", &self.grub_users);
327        render_single_value(&mut content, "grub_arg", &self.grub_arg);
328
329        // Commands with multiple values
330        render_multiple_values(&mut content, "initrd", &self.initrd);
331        render_multiple_values(&mut content, "options", &self.options);
332        render_multiple_values(&mut content, "grub_class", &self.grub_class);
333
334        content
335    }
336
337    /// Sets a value for the given key. For multi-value keys (`initrd`, `options`, `grub_class`),
338    /// `set_policy` controls append, prepend, replace, or insert-at-index.
339    ///
340    /// # Panics
341    ///
342    /// May panic if `ValueSetPolicy::InsertAt(i)` is used with an index out of range.
343    pub fn set(
344        &mut self,
345        key: BLSKey,
346        value: String,
347        comment: Option<String>,
348        set_policy: ValueSetPolicy,
349    ) {
350        fn value_generator(value: String, comment: Option<String>) -> BLSValue {
351            match comment {
352                Some(comment) => BLSValue::ValueWithComment(value, comment),
353                None => BLSValue::Value(value),
354            }
355        }
356
357        fn push_value(values: &mut Vec<BLSValue>, val: BLSValue, policy: ValueSetPolicy) {
358            match policy {
359                ValueSetPolicy::Append => values.push(val),
360                ValueSetPolicy::InsertAt(i) => values.insert(i, val),
361                ValueSetPolicy::Prepend => values.insert(0, val),
362                ValueSetPolicy::ReplaceAll => {
363                    values.clear();
364                    values.push(val);
365                }
366            }
367        }
368
369        match key {
370            BLSKey::Title => self.title = Some(value_generator(value, comment)),
371            BLSKey::Version => self.version = Some(value_generator(value, comment)),
372            BLSKey::MachineId => self.machine_id = Some(value_generator(value, comment)),
373            BLSKey::SortKey => self.sort_key = Some(value_generator(value, comment)),
374            BLSKey::Linux => self.linux = value_generator(value, comment),
375            BLSKey::Efi => self.efi = Some(value_generator(value, comment)),
376            BLSKey::Devicetree => self.devicetree = Some(value_generator(value, comment)),
377            BLSKey::DevicetreeOverlay => {
378                self.devicetree_overlay = Some(value_generator(value, comment))
379            }
380            BLSKey::Architecture => self.architecture = Some(value_generator(value, comment)),
381            BLSKey::GrubHotkey => self.grub_hotkey = Some(value_generator(value, comment)),
382            BLSKey::GrubUsers => self.grub_users = Some(value_generator(value, comment)),
383            BLSKey::GrubArg => self.grub_arg = Some(value_generator(value, comment)),
384
385            BLSKey::Initrd => push_value(
386                &mut self.initrd,
387                value_generator(value, comment),
388                set_policy,
389            ),
390            BLSKey::Options => push_value(
391                &mut self.options,
392                value_generator(value, comment),
393                set_policy,
394            ),
395            BLSKey::GrubClass => push_value(
396                &mut self.grub_class,
397                value_generator(value, comment),
398                set_policy,
399            ),
400        }
401    }
402
403    /// Clears the given key. For `BLSKey::Linux`, sets the value to an empty string (key remains present). Other keys become `None` or empty.
404    pub fn clear(&mut self, key: BLSKey) {
405        match key {
406            BLSKey::Linux => self.linux = BLSValue::Value(String::from("")),
407            BLSKey::Title => self.title = None,
408            BLSKey::Version => self.version = None,
409            BLSKey::MachineId => self.machine_id = None,
410            BLSKey::SortKey => self.sort_key = None,
411            BLSKey::Efi => self.efi = None,
412            BLSKey::Devicetree => self.devicetree = None,
413            BLSKey::DevicetreeOverlay => self.devicetree_overlay = None,
414            BLSKey::Architecture => self.architecture = None,
415            BLSKey::GrubHotkey => self.grub_hotkey = None,
416            BLSKey::GrubUsers => self.grub_users = None,
417            BLSKey::GrubArg => self.grub_arg = None,
418
419            BLSKey::Initrd => self.initrd.clear(),
420            BLSKey::Options => self.options.clear(),
421            BLSKey::GrubClass => self.grub_class.clear(),
422        }
423    }
424}
425
426#[cfg(test)]
427mod bls_tests {
428    use core::str::FromStr;
429
430    #[cfg(not(feature = "std"))]
431    use alloc::string::String;
432    #[cfg(not(feature = "std"))]
433    use alloc::vec;
434
435    use super::BLSEntry;
436    use super::BLSKey;
437    use super::BLSValue;
438    use super::ValueSetPolicy;
439
440    #[test]
441    fn bls_key_from_str() {
442        assert!(BLSKey::from_str("linux").is_ok());
443        assert!(BLSKey::from_str("title").is_ok());
444        assert!(BLSKey::from_str("version").is_ok());
445        assert!(BLSKey::from_str("machine_id").is_ok());
446        assert!(BLSKey::from_str("machine-id").is_ok());
447        assert!(BLSKey::from_str("sort_key").is_ok());
448        assert!(BLSKey::from_str("sort-key").is_ok());
449        assert!(BLSKey::from_str("efi").is_ok());
450        assert!(BLSKey::from_str("initrd").is_ok());
451        assert!(BLSKey::from_str("options").is_ok());
452        assert!(BLSKey::from_str("devicetree").is_ok());
453        assert!(BLSKey::from_str("devicetree_overlay").is_ok());
454        assert!(BLSKey::from_str("devicetree-overlay").is_ok());
455        assert!(BLSKey::from_str("architecture").is_ok());
456        assert!(BLSKey::from_str("grub_hotkey").is_ok());
457        assert!(BLSKey::from_str("grub_users").is_ok());
458        assert!(BLSKey::from_str("grub_class").is_ok());
459        assert!(BLSKey::from_str("grub_arg").is_ok());
460        assert!(BLSKey::from_str("invalid_key").is_err());
461    }
462
463    #[test]
464    fn new_entry() {
465        let entry = BLSEntry::new();
466        match &entry.linux {
467            BLSValue::Value(linux) => assert_eq!(linux, ""),
468            _ => panic!("Invalid 'linux' value {:?}", entry.linux),
469        }
470        assert!(entry.title.is_none());
471        assert!(entry.version.is_none());
472        assert!(entry.machine_id.is_none());
473        assert!(entry.sort_key.is_none());
474        assert!(entry.efi.is_none());
475        assert_eq!(entry.initrd.len(), 0);
476        assert_eq!(entry.options.len(), 0);
477        assert!(entry.devicetree.is_none());
478        assert!(entry.devicetree_overlay.is_none());
479        assert!(entry.architecture.is_none());
480        assert!(entry.grub_hotkey.is_none());
481        assert!(entry.grub_users.is_none());
482        assert_eq!(entry.grub_class.len(), 0);
483        assert!(entry.grub_arg.is_none());
484        assert!(entry.comments.is_empty());
485    }
486
487    #[test]
488    fn parse_entry() {
489        let entry_txt = "#Comment\n\
490                     linux foobar-2.4\n\
491                     options foo=bar #Another Comment";
492        let entry = BLSEntry::parse(entry_txt);
493
494        assert!(entry.is_ok());
495        let entry = entry.unwrap();
496        assert_eq!(entry.comments.len(), 1);
497        assert_eq!(entry.comments[0], "Comment");
498
499        if let BLSValue::Value(linux) = entry.linux {
500            assert_eq!(linux, "foobar-2.4");
501        }
502
503        assert_eq!(entry.options.len(), 1);
504        match &entry.options[0] {
505            BLSValue::ValueWithComment(option, comment) => {
506                assert_eq!(option, "foo=bar");
507                assert_eq!(comment, "Another Comment");
508            }
509            _ => {
510                panic!("Invalid 'options' value {:?}", entry.options[0])
511            }
512        }
513    }
514
515    #[test]
516    fn parse_errors() {
517        // Missing both 'linux' and 'efi'
518        let entry_txt = "options foo=bar";
519        let entry = BLSEntry::parse(entry_txt);
520        assert!(entry.is_err());
521
522        // Invalid command
523        let entry_txt = "linux asdasdasdas\n\
524                     invalid_command foo=bar";
525        let entry = BLSEntry::parse(entry_txt);
526        assert!(entry.is_err());
527    }
528
529    #[test]
530    fn parse_efi_only() {
531        let entry_txt = "title EFI App\nefi /EFI/app.efi";
532        let entry = BLSEntry::parse(entry_txt).expect("efi-only entry should parse");
533        assert!(entry.efi.is_some());
534        if let Some(BLSValue::Value(ref path)) = entry.efi {
535            assert_eq!(path, "/EFI/app.efi");
536        }
537    }
538
539    #[test]
540    fn parse_hyphenated_keys() {
541        let entry_txt = "title Fedora\nmachine-id 6a9857a393724b7a981ebb5b8495b9ea\nsort-key fedora\ndevicetree-overlay /overlay.dtbo\nlinux /vmlinuz";
542        let entry = BLSEntry::parse(entry_txt).expect("hyphenated keys should parse");
543        assert_eq!(
544            entry.title.as_ref().map(|v| match v {
545                BLSValue::Value(s) => s.as_str(),
546                _ => "",
547            }),
548            Some("Fedora")
549        );
550        assert!(entry.machine_id.is_some());
551        assert!(entry.sort_key.is_some());
552        assert!(entry.devicetree_overlay.is_some());
553    }
554
555    #[test]
556    fn parse_multiple_initrd_options() {
557        let entry_txt =
558            "linux /vmlinuz\ninitrd /initrd1\ninitrd /initrd2\noptions a=1\noptions b=2";
559        let entry = BLSEntry::parse(entry_txt).unwrap();
560        assert_eq!(entry.initrd.len(), 2);
561        assert_eq!(entry.options.len(), 2);
562    }
563
564    #[test]
565    fn parse_round_trip() {
566        let entry_txt = "title Fedora 19\nsort-key fedora\nmachine-id 6a9857a393724b7a981ebb5b8495b9ea\nversion 3.8.0-2.fc19.x86_64\noptions root=UUID=abc quiet\narchitecture x64\nlinux /6a9857a393724b7a981ebb5b8495b9ea/3.8.0-2.fc19.x86_64/linux\ninitrd /6a9857a393724b7a981ebb5b8495b9ea/3.8.0-2.fc19.x86_64/initrd";
567        let entry = BLSEntry::parse(entry_txt).unwrap();
568        let rendered = entry.render();
569        let entry2 = BLSEntry::parse(&rendered).unwrap();
570        assert_eq!(entry.title, entry2.title);
571        assert_eq!(entry.version, entry2.version);
572        assert_eq!(entry.machine_id, entry2.machine_id);
573        assert_eq!(entry.sort_key, entry2.sort_key);
574        assert_eq!(entry.linux, entry2.linux);
575        assert_eq!(entry.initrd, entry2.initrd);
576        assert_eq!(entry.options, entry2.options);
577        assert_eq!(entry.architecture, entry2.architecture);
578    }
579
580    #[test]
581    fn render_all_keys_including_grub() {
582        let mut entry = BLSEntry::new();
583        entry.set(
584            BLSKey::Linux,
585            String::from("/vmlinuz"),
586            None,
587            ValueSetPolicy::ReplaceAll,
588        );
589        entry.set(
590            BLSKey::Title,
591            String::from("Test"),
592            None,
593            ValueSetPolicy::ReplaceAll,
594        );
595        entry.set(
596            BLSKey::GrubHotkey,
597            String::from("t"),
598            None,
599            ValueSetPolicy::ReplaceAll,
600        );
601        entry.set(
602            BLSKey::GrubUsers,
603            String::from("root"),
604            None,
605            ValueSetPolicy::ReplaceAll,
606        );
607        entry.set(
608            BLSKey::GrubArg,
609            String::from("--debug"),
610            None,
611            ValueSetPolicy::ReplaceAll,
612        );
613        entry.set(
614            BLSKey::GrubClass,
615            String::from("recovery"),
616            None,
617            ValueSetPolicy::Append,
618        );
619        let out = entry.render();
620        assert!(out.contains("grub_hotkey t"));
621        assert!(out.contains("grub_users root"));
622        assert!(out.contains("grub_arg --debug"));
623        assert!(out.contains("grub_class recovery"));
624    }
625
626    #[test]
627    fn set_every_key() {
628        let mut entry = BLSEntry::new();
629        entry.set(
630            BLSKey::Linux,
631            String::from("/vmlinuz"),
632            None,
633            ValueSetPolicy::ReplaceAll,
634        );
635        entry.set(
636            BLSKey::Title,
637            String::from("T"),
638            None,
639            ValueSetPolicy::ReplaceAll,
640        );
641        entry.set(
642            BLSKey::Version,
643            String::from("1.0"),
644            None,
645            ValueSetPolicy::ReplaceAll,
646        );
647        entry.set(
648            BLSKey::MachineId,
649            String::from("abc"),
650            None,
651            ValueSetPolicy::ReplaceAll,
652        );
653        entry.set(
654            BLSKey::SortKey,
655            String::from("x"),
656            None,
657            ValueSetPolicy::ReplaceAll,
658        );
659        entry.set(
660            BLSKey::Efi,
661            String::from("/efi.efi"),
662            None,
663            ValueSetPolicy::ReplaceAll,
664        );
665        entry.set(
666            BLSKey::Initrd,
667            String::from("/i1"),
668            None,
669            ValueSetPolicy::Append,
670        );
671        entry.set(
672            BLSKey::Options,
673            String::from("opt"),
674            None,
675            ValueSetPolicy::Append,
676        );
677        entry.set(
678            BLSKey::Devicetree,
679            String::from("/dtb"),
680            None,
681            ValueSetPolicy::ReplaceAll,
682        );
683        entry.set(
684            BLSKey::DevicetreeOverlay,
685            String::from("/overlay"),
686            None,
687            ValueSetPolicy::ReplaceAll,
688        );
689        entry.set(
690            BLSKey::Architecture,
691            String::from("x64"),
692            None,
693            ValueSetPolicy::ReplaceAll,
694        );
695        entry.set(
696            BLSKey::GrubHotkey,
697            String::from("h"),
698            None,
699            ValueSetPolicy::ReplaceAll,
700        );
701        entry.set(
702            BLSKey::GrubUsers,
703            String::from("u"),
704            None,
705            ValueSetPolicy::ReplaceAll,
706        );
707        entry.set(
708            BLSKey::GrubClass,
709            String::from("c"),
710            None,
711            ValueSetPolicy::Append,
712        );
713        entry.set(
714            BLSKey::GrubArg,
715            String::from("a"),
716            None,
717            ValueSetPolicy::ReplaceAll,
718        );
719        assert!(entry.title.is_some());
720        assert!(entry.version.is_some());
721        assert!(entry.machine_id.is_some());
722        assert!(entry.sort_key.is_some());
723        assert!(entry.efi.is_some());
724        assert_eq!(entry.initrd.len(), 1);
725        assert_eq!(entry.options.len(), 1);
726        assert!(entry.devicetree.is_some());
727        assert!(entry.devicetree_overlay.is_some());
728        assert!(entry.architecture.is_some());
729        assert!(entry.grub_hotkey.is_some());
730        assert!(entry.grub_users.is_some());
731        assert_eq!(entry.grub_class.len(), 1);
732        assert!(entry.grub_arg.is_some());
733    }
734
735    #[test]
736    fn set_value_policies_initrd_grub_class() {
737        let mut entry = BLSEntry::new();
738        entry.set(
739            BLSKey::Linux,
740            String::from("/vmlinuz"),
741            None,
742            ValueSetPolicy::ReplaceAll,
743        );
744        entry.set(
745            BLSKey::Initrd,
746            String::from("a"),
747            None,
748            ValueSetPolicy::Append,
749        );
750        entry.set(
751            BLSKey::Initrd,
752            String::from("b"),
753            None,
754            ValueSetPolicy::Append,
755        );
756        entry.set(
757            BLSKey::Initrd,
758            String::from("mid"),
759            None,
760            ValueSetPolicy::InsertAt(1),
761        );
762        assert_eq!(entry.initrd.len(), 3);
763        entry.set(
764            BLSKey::Initrd,
765            String::from("first"),
766            None,
767            ValueSetPolicy::Prepend,
768        );
769        assert!(matches!(entry.initrd.first(), Some(BLSValue::Value(s)) if s == "first"));
770        entry.set(
771            BLSKey::Initrd,
772            String::from("only"),
773            None,
774            ValueSetPolicy::ReplaceAll,
775        );
776        assert_eq!(entry.initrd.len(), 1);
777        entry.set(
778            BLSKey::GrubClass,
779            String::from("class1"),
780            None,
781            ValueSetPolicy::Append,
782        );
783        entry.set(
784            BLSKey::GrubClass,
785            String::from("class2"),
786            None,
787            ValueSetPolicy::Append,
788        );
789        assert_eq!(entry.grub_class.len(), 2);
790    }
791
792    #[test]
793    fn clear_every_key() {
794        let mut entry = BLSEntry::new();
795        entry.set(
796            BLSKey::Linux,
797            String::from("/vmlinuz"),
798            None,
799            ValueSetPolicy::ReplaceAll,
800        );
801        entry.set(
802            BLSKey::Title,
803            String::from("T"),
804            None,
805            ValueSetPolicy::ReplaceAll,
806        );
807        entry.set(
808            BLSKey::Version,
809            String::from("1"),
810            None,
811            ValueSetPolicy::ReplaceAll,
812        );
813        entry.set(
814            BLSKey::MachineId,
815            String::from("m"),
816            None,
817            ValueSetPolicy::ReplaceAll,
818        );
819        entry.set(
820            BLSKey::SortKey,
821            String::from("s"),
822            None,
823            ValueSetPolicy::ReplaceAll,
824        );
825        entry.set(
826            BLSKey::Efi,
827            String::from("e"),
828            None,
829            ValueSetPolicy::ReplaceAll,
830        );
831        entry.set(
832            BLSKey::Initrd,
833            String::from("i"),
834            None,
835            ValueSetPolicy::Append,
836        );
837        entry.set(
838            BLSKey::Options,
839            String::from("o"),
840            None,
841            ValueSetPolicy::Append,
842        );
843        entry.set(
844            BLSKey::Devicetree,
845            String::from("d"),
846            None,
847            ValueSetPolicy::ReplaceAll,
848        );
849        entry.set(
850            BLSKey::DevicetreeOverlay,
851            String::from("do"),
852            None,
853            ValueSetPolicy::ReplaceAll,
854        );
855        entry.set(
856            BLSKey::Architecture,
857            String::from("a"),
858            None,
859            ValueSetPolicy::ReplaceAll,
860        );
861        entry.set(
862            BLSKey::GrubHotkey,
863            String::from("g"),
864            None,
865            ValueSetPolicy::ReplaceAll,
866        );
867        entry.set(
868            BLSKey::GrubUsers,
869            String::from("u"),
870            None,
871            ValueSetPolicy::ReplaceAll,
872        );
873        entry.set(
874            BLSKey::GrubClass,
875            String::from("c"),
876            None,
877            ValueSetPolicy::Append,
878        );
879        entry.set(
880            BLSKey::GrubArg,
881            String::from("ga"),
882            None,
883            ValueSetPolicy::ReplaceAll,
884        );
885        entry.clear(BLSKey::Title);
886        entry.clear(BLSKey::Version);
887        entry.clear(BLSKey::MachineId);
888        entry.clear(BLSKey::SortKey);
889        entry.clear(BLSKey::Efi);
890        entry.clear(BLSKey::Initrd);
891        entry.clear(BLSKey::Options);
892        entry.clear(BLSKey::Devicetree);
893        entry.clear(BLSKey::DevicetreeOverlay);
894        entry.clear(BLSKey::Architecture);
895        entry.clear(BLSKey::GrubHotkey);
896        entry.clear(BLSKey::GrubUsers);
897        entry.clear(BLSKey::GrubClass);
898        entry.clear(BLSKey::GrubArg);
899        entry.clear(BLSKey::Linux);
900        assert!(entry.title.is_none());
901        assert!(entry.version.is_none());
902        assert!(entry.machine_id.is_none());
903        assert!(entry.sort_key.is_none());
904        assert!(entry.efi.is_none());
905        assert!(entry.initrd.is_empty());
906        assert!(entry.options.is_empty());
907        assert!(entry.devicetree.is_none());
908        assert!(entry.devicetree_overlay.is_none());
909        assert!(entry.architecture.is_none());
910        assert!(entry.grub_hotkey.is_none());
911        assert!(entry.grub_users.is_none());
912        assert!(entry.grub_class.is_empty());
913        assert!(entry.grub_arg.is_none());
914        assert!(matches!(&entry.linux, BLSValue::Value(s) if s.is_empty()));
915    }
916
917    #[test]
918    fn bls_value_value_with_comment() {
919        let v = BLSValue::Value(String::from("arg"));
920        let vc = BLSValue::ValueWithComment(String::from("arg"), String::from("comment"));
921        assert!(matches!(&v, BLSValue::Value(s) if s == "arg"));
922        assert!(matches!(&vc, BLSValue::ValueWithComment(a, c) if a == "arg" && c == "comment"));
923    }
924
925    #[test]
926    fn set_value_policies() {
927        // Append
928        let mut entry = BLSEntry::new();
929        let _ = entry.set(
930            BLSKey::Options,
931            String::from("foo"),
932            None,
933            ValueSetPolicy::Append,
934        );
935        let _ = entry.set(
936            BLSKey::Options,
937            String::from("bar"),
938            None,
939            ValueSetPolicy::Append,
940        );
941        let _ = entry.set(
942            BLSKey::Options,
943            String::from("baz"),
944            None,
945            ValueSetPolicy::Append,
946        );
947
948        assert_eq!(
949            entry.options,
950            vec![
951                BLSValue::Value(String::from("foo")),
952                BLSValue::Value(String::from("bar")),
953                BLSValue::Value(String::from("baz"))
954            ]
955        );
956
957        // InsertAt
958        let _ = entry.set(
959            BLSKey::Options,
960            String::from("lol"),
961            None,
962            ValueSetPolicy::InsertAt(1),
963        );
964        assert_eq!(
965            entry.options,
966            vec![
967                BLSValue::Value(String::from("foo")),
968                BLSValue::Value(String::from("lol")),
969                BLSValue::Value(String::from("bar")),
970                BLSValue::Value(String::from("baz"))
971            ]
972        );
973
974        // ReplaceAll
975        let _ = entry.set(
976            BLSKey::Options,
977            String::from("wtf"),
978            None,
979            ValueSetPolicy::ReplaceAll,
980        );
981        assert_eq!(entry.options, vec![BLSValue::Value(String::from("wtf"))]);
982
983        // Prepend
984        let _ = entry.set(
985            BLSKey::Options,
986            String::from("uwu"),
987            None,
988            ValueSetPolicy::Prepend,
989        );
990        assert_eq!(
991            entry.options,
992            vec![
993                BLSValue::Value(String::from("uwu")),
994                BLSValue::Value(String::from("wtf"))
995            ]
996        );
997
998        // Clear
999        entry.clear(BLSKey::Options);
1000        assert_eq!(entry.options, vec![]);
1001
1002        entry.set(
1003            BLSKey::Title,
1004            String::from("foobar"),
1005            None,
1006            ValueSetPolicy::Append,
1007        );
1008        assert_eq!(entry.title, Some(BLSValue::Value(String::from("foobar"))));
1009
1010        entry.clear(BLSKey::Title);
1011        assert_eq!(entry.title, None);
1012    }
1013}