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//! Implements systemd's [Boot Loader Specification](https://systemd.io/BOOT_LOADER_SPECIFICATION/)
12//!
13//! This API can parse and modify a Boot Loader Spec entry file.
14//! It supports the [Fedora/GRUB specific commands](https://fedoraproject.org/wiki/Changes/BootLoaderSpecByDefault).
15//!
16//! This library can be used in a no_std environment that
17//! supports dynamic memory allocation by disabling
18//! the "std" feature using --no-default-features.
19//!
20//! NOTE: At the moment, if you parse a BLSEntry with full-line
21//! comments and write it back, all comment lines will be consolidated
22//! in the header.
23
24#![cfg_attr(not(feature = "std"), no_std)]
25#![cfg(not(feature = "std"))]
26extern crate alloc;
27
28#[cfg(not(feature = "std"))]
29use alloc::format;
30#[cfg(not(feature = "std"))]
31use alloc::string::String;
32#[cfg(not(feature = "std"))]
33use alloc::vec::Vec;
34
35#[cfg(not(feature = "std"))]
36use alloc::str::FromStr;
37#[cfg(feature = "std")]
38use std::str::FromStr;
39
40/// BLSValue generalizes a value that may have an inline comment
41#[derive(Debug, PartialEq)]
42pub enum BLSValue {
43    /// The string represent the argument string for the BLS command
44    Value(String),
45    /// Holds string that represents the BLS command argument as well as a comment string at the end of the line
46    ValueWithComment(String, String),
47}
48
49/// This enum is used as an argument in the ```BLSEntry::set()``` method.
50/// Some keys like ```initrd``` can be specified multiple times in a BLS entry file.
51#[derive(Debug, PartialEq)]
52pub enum ValueSetPolicy {
53    /// This replaces all existing values for the given key
54    ReplaceAll,
55    /// This appends a value to the last position in the BLSEntry
56    Append,
57    /// This inserts a value in the first position in the BLSEntry
58    Prepend,
59    /// This inserts a value in the given index. Note that his may cause a panic as per ```std::vec::Vec::insert()```
60    InsertAt(usize),
61}
62
63#[derive(Debug, PartialEq)]
64pub enum BLSKey {
65    Title,
66    Version,
67    MachineId,
68    SortKey,
69    Linux,
70    Efi,
71    Initrd,
72    Options,
73    Devicetree,
74    DevicetreeOverlay,
75    Architecture,
76    GrubHotkey,
77    GrubUsers,
78    GrubClass,
79    GrubArg,
80}
81
82impl FromStr for BLSKey {
83    type Err = String;
84
85    fn from_str(key: &str) -> Result<Self, Self::Err> {
86        match key {
87            "linux" => Ok(BLSKey::Linux),
88            "title" => Ok(BLSKey::Title),
89            "version" => Ok(BLSKey::Version),
90            "machine_id" => Ok(BLSKey::MachineId),
91            "sort_key" => Ok(BLSKey::SortKey),
92            "efi" => Ok(BLSKey::Efi),
93            "initrd" => Ok(BLSKey::Initrd),
94            "options" => Ok(BLSKey::Options),
95            "devicetree" => Ok(BLSKey::Devicetree),
96            "devicetree_overlay" => Ok(BLSKey::DevicetreeOverlay),
97            "architecture" => Ok(BLSKey::Architecture),
98            "grub_hotkey" => Ok(BLSKey::GrubHotkey),
99            "grub_users" => Ok(BLSKey::GrubUsers),
100            "grub_class" => Ok(BLSKey::GrubClass),
101            "grub_arg" => Ok(BLSKey::GrubArg),
102            _ => Err(format!("Invalid key {}", key)),
103        }
104    }
105}
106
107/// BLSEntry represents the contents of a BLS entry file
108#[derive(Debug)]
109pub struct BLSEntry {
110    pub title: Option<BLSValue>,
111    pub version: Option<BLSValue>,
112    pub machine_id: Option<BLSValue>,
113    pub sort_key: Option<BLSValue>,
114    pub linux: BLSValue,
115    pub efi: Option<BLSValue>,
116    pub initrd: Vec<BLSValue>,
117    pub options: Vec<BLSValue>,
118    pub devicetree: Option<BLSValue>,
119    pub devicetree_overlay: Option<BLSValue>,
120    pub architecture: Option<BLSValue>,
121    pub grub_hotkey: Option<BLSValue>,
122    pub grub_users: Option<BLSValue>,
123    pub grub_class: Vec<BLSValue>,
124    pub grub_arg: Option<BLSValue>,
125    // NOTE: All comments are moved to the header of the file upon rendering back the content
126    pub comments: Vec<String>,
127}
128
129impl BLSEntry {
130    /// Allocates a new instance of BLSEntry, all optional members are initialized to None and ```linux``` is set with an empty string
131    pub fn new() -> BLSEntry {
132        BLSEntry {
133            title: None,
134            version: None,
135            machine_id: None,
136            sort_key: None,
137            linux: BLSValue::Value(String::new()),
138            efi: None,
139            initrd: Vec::new(),
140            options: Vec::new(),
141            devicetree: None,
142            devicetree_overlay: None,
143            architecture: None,
144            grub_hotkey: None,
145            grub_users: None,
146            grub_class: Vec::new(),
147            grub_arg: None,
148            comments: Vec::new(),
149        }
150    }
151
152    /// Parses a Boot Loader Spec entry UTF-8 buffer, returns a BLSEntry instance if successful, an error String if there was an error
153    /// Note that any comment lines that are then rendered using BLSEntry::parse() will be pushed to the header of the file as the
154    /// order commands and comments are not preserved.
155    pub fn parse(buffer: &str) -> Result<BLSEntry, String> {
156        let mut entry = BLSEntry::new();
157        let mut has_linux = false;
158
159        for line in buffer.lines() {
160            let mut comment = None;
161            // Extract the comment string from the line
162            let line = if line.contains("#") {
163                let split: Vec<_> = line.splitn(2, "#").collect();
164                comment = Some(String::from(split[1]));
165                split[0]
166            } else {
167                line
168            };
169
170            // NOTE: For now we put all comment lines in the header
171            if line.trim().contains(" ") {
172                let key_value: Vec<&str> = line.trim().splitn(2, " ").collect();
173
174                let key = BLSKey::from_str(key_value[0])?;
175                if key == BLSKey::Linux {
176                    has_linux = true;
177                }
178                entry.set(
179                    key,
180                    String::from(key_value[1]),
181                    comment,
182                    ValueSetPolicy::Append,
183                );
184            } else {
185                match comment {
186                    Some(comment) => {
187                        entry.comments.push(comment);
188                    }
189                    None => {}
190                }
191            }
192        }
193
194        if has_linux {
195            Ok(entry)
196        } else {
197            Err(String::from("No 'linux' command found."))
198        }
199    }
200
201    /// Renders the BLSEntry content into a UTF-8 String
202    pub fn render(&self) -> String {
203        let mut content = String::new();
204
205        fn render_value(content: &mut String, key: &str, value: &BLSValue) {
206            content.push_str(key);
207            content.push(' ');
208            match value {
209                BLSValue::Value(value) => content.push_str(&value),
210                BLSValue::ValueWithComment(value, comment) => {
211                    content.push_str(&value);
212                    content.push_str(" #");
213                    content.push_str(&comment);
214                }
215            }
216        }
217
218        fn render_single_value(content: &mut String, key: &str, value: &Option<BLSValue>) {
219            if let Some(value) = value {
220                render_value(content, key, &value)
221            }
222        }
223
224        fn render_multiple_values(content: &mut String, key: &str, values: &Vec<BLSValue>) {
225            for val in values {
226                render_value(content, key, &val)
227            }
228        }
229
230        // We push all comments in the header
231        for comment in &self.comments {
232            content.push_str("#");
233            content.push_str(&comment)
234        }
235
236        // Mandatory commands
237        render_value(&mut content, "linux", &self.linux);
238
239        // Optional commands
240        render_single_value(&mut content, "title", &self.title);
241        render_single_value(&mut content, "version", &self.version);
242        render_single_value(&mut content, "machine-id", &self.machine_id);
243        render_single_value(&mut content, "sort-key", &self.sort_key);
244        render_single_value(&mut content, "efi", &self.efi);
245        render_single_value(&mut content, "devicetree", &self.devicetree);
246        render_single_value(&mut content, "devicetree-overlay", &self.devicetree_overlay);
247        render_single_value(&mut content, "architecture", &self.architecture);
248        render_single_value(&mut content, "grub_hotkey", &self.devicetree_overlay);
249        render_single_value(&mut content, "grub_users", &self.devicetree_overlay);
250        render_single_value(&mut content, "grub_arg", &self.devicetree_overlay);
251
252        // Commands with multiple values
253        render_multiple_values(&mut content, "initrd", &self.initrd);
254        render_multiple_values(&mut content, "options", &self.options);
255        render_multiple_values(&mut content, "grub_class", &self.grub_class);
256
257        content
258    }
259
260    /// Sets a value for a given key
261    /// # Arguments
262    ///
263    /// - ```key```: a &str representing the key to be set
264    /// - ```value```: a String representing the value for the key
265    /// - ```comment```: an optional String reprensenting an inline comment for the value
266    /// - ```set_policy```: Some keys can hold multiple values, the ```ValueSetPolicy``` enum specifies the policy for keys that can be specified multiple times.
267    ///
268    /// # Panics
269    ///
270    /// If ```ValueSetPolicy::InsertAt(usize)``` is used as ```set_policy``` it may cause a panic if the index is out of bound
271    pub fn set(
272        &mut self,
273        key: BLSKey,
274        value: String,
275        comment: Option<String>,
276        set_policy: ValueSetPolicy,
277    ) {
278        fn value_generator(value: String, comment: Option<String>) -> BLSValue {
279            match comment {
280                Some(comment) => BLSValue::ValueWithComment(value, comment),
281                None => BLSValue::Value(value),
282            }
283        }
284
285        fn push_value(values: &mut Vec<BLSValue>, val: BLSValue, policy: ValueSetPolicy) {
286            match policy {
287                ValueSetPolicy::Append => values.push(val),
288                ValueSetPolicy::InsertAt(i) => values.insert(i, val),
289                ValueSetPolicy::Prepend => values.insert(0, val),
290                ValueSetPolicy::ReplaceAll => {
291                    values.clear();
292                    values.push(val);
293                }
294            }
295        }
296
297        match key {
298            BLSKey::Title => self.title = Some(value_generator(value, comment)),
299            BLSKey::Version => self.version = Some(value_generator(value, comment)),
300            BLSKey::MachineId => self.machine_id = Some(value_generator(value, comment)),
301            BLSKey::SortKey => self.sort_key = Some(value_generator(value, comment)),
302            BLSKey::Linux => self.linux = value_generator(value, comment),
303            BLSKey::Efi => self.efi = Some(value_generator(value, comment)),
304            BLSKey::Devicetree => self.devicetree = Some(value_generator(value, comment)),
305            BLSKey::DevicetreeOverlay => {
306                self.devicetree_overlay = Some(value_generator(value, comment))
307            }
308            BLSKey::Architecture => self.architecture = Some(value_generator(value, comment)),
309            BLSKey::GrubHotkey => self.grub_hotkey = Some(value_generator(value, comment)),
310            BLSKey::GrubUsers => self.grub_users = Some(value_generator(value, comment)),
311            BLSKey::GrubArg => self.grub_arg = Some(value_generator(value, comment)),
312
313            BLSKey::Initrd => push_value(
314                &mut self.initrd,
315                value_generator(value, comment),
316                set_policy,
317            ),
318            BLSKey::Options => push_value(
319                &mut self.options,
320                value_generator(value, comment),
321                set_policy,
322            ),
323            BLSKey::GrubClass => push_value(
324                &mut self.grub_class,
325                value_generator(value, comment),
326                set_policy,
327            ),
328        }
329    }
330
331    /// Clears a field in the BLSEntry
332    /// 
333    /// # Arguments
334    /// - key: The ```BLSKey``` to clear. Note that if ```BLSKey::linux``` is used it will set it to an empty string as this key is always supposed to be present. Sets None or empty vector otherwise.
335    pub fn clear(&mut self, key: BLSKey) {
336        match key {
337            BLSKey::Linux => self.linux = BLSValue::Value(String::from("")),
338            BLSKey::Title => self.title = None,
339            BLSKey::Version => self.version = None,
340            BLSKey::MachineId => self.machine_id = None,
341            BLSKey::SortKey => self.sort_key = None,
342            BLSKey::Efi => self.efi = None,
343            BLSKey::Devicetree => self.devicetree = None,
344            BLSKey::DevicetreeOverlay => self.devicetree_overlay = None,
345            BLSKey::Architecture => self.architecture = None,
346            BLSKey::GrubHotkey => self.grub_hotkey = None,
347            BLSKey::GrubUsers => self.grub_users = None,
348            BLSKey::GrubArg => self.grub_arg = None,
349
350            BLSKey::Initrd => self.initrd.clear(),
351            BLSKey::Options => self.options.clear(),
352            BLSKey::GrubClass => self.grub_class.clear(),
353        }
354    }
355}
356
357#[cfg(test)]
358mod bls_tests {
359    use super::String;
360
361    #[cfg(not(feature = "std"))]
362    use alloc::vec;
363
364    use super::BLSEntry;
365    use super::BLSKey;
366    use super::BLSValue;
367    use super::ValueSetPolicy;
368
369    #[test]
370    fn new_entry() {
371        let entry = BLSEntry::new();
372        match entry.linux {
373            BLSValue::Value(linux) => {
374                assert_eq!(linux, "");
375            }
376            _ => {
377                panic!("Invalid 'linux' value {:?}", entry.linux);
378            }
379        }
380        assert_eq!(entry.initrd.len(), 0);
381    }
382
383    #[test]
384    fn parse_entry() {
385        let entry_txt = "#Comment\n\
386                     linux foobar-2.4\n\
387                     options foo=bar #Another Comment";
388        let entry = BLSEntry::parse(entry_txt);
389
390        assert!(entry.is_ok());
391        let entry = entry.unwrap();
392        assert_eq!(entry.comments.len(), 1);
393        assert_eq!(entry.comments[0], "Comment");
394
395        if let BLSValue::Value(linux) = entry.linux {
396            assert_eq!(linux, "foobar-2.4");
397        }
398
399        assert_eq!(entry.options.len(), 1);
400        match &entry.options[0] {
401            BLSValue::ValueWithComment(option, comment) => {
402                assert_eq!(option, "foo=bar");
403                assert_eq!(comment, "Another Comment");
404            }
405            _ => {
406                panic!("Invalid 'options' value {:?}", entry.options[0])
407            }
408        }
409    }
410
411    #[test]
412    fn parse_errors() {
413        // Missing 'linux' command
414        let entry_txt = "options foo=bar";
415        let entry = BLSEntry::parse(entry_txt);
416        assert!(entry.is_err());
417
418        // Invalid command
419        let entry_txt = "linux asdasdasdas\n\
420                     invalid_command foo=bar";
421        let entry = BLSEntry::parse(entry_txt);
422        assert!(entry.is_err());
423    }
424
425    #[test]
426    fn set_value_policies() {
427        // Append
428        let mut entry = BLSEntry::new();
429        let _ = entry.set(
430            BLSKey::Options,
431            String::from("foo"),
432            None,
433            ValueSetPolicy::Append,
434        );
435        let _ = entry.set(
436            BLSKey::Options,
437            String::from("bar"),
438            None,
439            ValueSetPolicy::Append,
440        );
441        let _ = entry.set(
442            BLSKey::Options,
443            String::from("baz"),
444            None,
445            ValueSetPolicy::Append,
446        );
447
448        assert_eq!(
449            entry.options,
450            vec![
451                BLSValue::Value(String::from("foo")),
452                BLSValue::Value(String::from("bar")),
453                BLSValue::Value(String::from("baz"))
454            ]
455        );
456
457        // InsertAt
458        let _ = entry.set(
459            BLSKey::Options,
460            String::from("lol"),
461            None,
462            ValueSetPolicy::InsertAt(1),
463        );
464        assert_eq!(
465            entry.options,
466            vec![
467                BLSValue::Value(String::from("foo")),
468                BLSValue::Value(String::from("lol")),
469                BLSValue::Value(String::from("bar")),
470                BLSValue::Value(String::from("baz"))
471            ]
472        );
473
474        // ReplaceAll
475        let _ = entry.set(
476            BLSKey::Options,
477            String::from("wtf"),
478            None,
479            ValueSetPolicy::ReplaceAll,
480        );
481        assert_eq!(entry.options, vec![BLSValue::Value(String::from("wtf"))]);
482
483        // Prepend
484        let _ = entry.set(
485            BLSKey::Options,
486            String::from("uwu"),
487            None,
488            ValueSetPolicy::Prepend,
489        );
490        assert_eq!(
491            entry.options,
492            vec![
493                BLSValue::Value(String::from("uwu")),
494                BLSValue::Value(String::from("wtf"))
495            ]
496        );
497
498        // Clear
499        entry.clear(BLSKey::Options);
500        assert_eq!(entry.options, vec![]);
501
502        entry.set(
503            BLSKey::Title,
504            String::from("foobar"),
505            None,
506            ValueSetPolicy::Append,
507        );
508        assert_eq!(entry.title, Some(BLSValue::Value(String::from("foobar"))));
509
510        entry.clear(BLSKey::Title);
511        assert_eq!(entry.title, None);
512    }
513}