systemd_boot_conf/
entry.rs

1use itertools::Itertools;
2use std::fs::File;
3use std::io::{self, BufRead, BufReader};
4use std::path::Path;
5
6#[derive(Debug, Error)]
7pub enum EntryError {
8    #[error("error reading line in entry file")]
9    Line(#[source] io::Error),
10    #[error("title field is missing")]
11    MisisngTitle,
12    #[error("entry is not a file")]
13    NotAFile,
14    #[error("entry does not have a file name")]
15    NoFilename,
16    #[error("initrd was defined without a value")]
17    NoValueForInitrd,
18    #[error("linux was defined without a value")]
19    NoValueForLinux,
20    #[error("error opening entry file")]
21    Open(#[source] io::Error),
22    #[error("entry has a file name that is not UTF-8")]
23    Utf8Filename,
24}
25
26#[derive(Debug, Default, Clone)]
27pub struct Entry {
28    pub id: Box<str>,
29    pub initrd: Option<Box<str>>,
30    pub linux: Box<str>,
31    pub options: Vec<Box<str>>,
32    pub title: Box<str>,
33}
34
35impl Entry {
36    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, EntryError> {
37        let path = path.as_ref();
38
39        if !path.is_file() {
40            return Err(EntryError::NotAFile);
41        }
42
43        let file_name = match path.file_stem() {
44            Some(file_name) => match file_name.to_str() {
45                Some(file_name) => file_name.to_owned(),
46                None => return Err(EntryError::Utf8Filename),
47            },
48            None => return Err(EntryError::NoFilename),
49        };
50
51        let file = File::open(path).map_err(EntryError::Open)?;
52
53        let mut entry = Entry::default();
54        entry.id = file_name.into();
55
56        for line in BufReader::new(file).lines() {
57            let line = line.map_err(EntryError::Line)?;
58            let mut fields = line.split_whitespace();
59            match fields.next() {
60                Some("title") => entry.title = fields.join(" ").into(),
61                Some("linux") => match fields.next() {
62                    Some(value) => entry.linux = value.into(),
63                    None => return Err(EntryError::NoValueForLinux),
64                },
65                Some("initrd") => match fields.next() {
66                    Some(value) => entry.initrd = Some(value.into()),
67                    None => return Err(EntryError::NoValueForInitrd),
68                },
69                Some("options") => entry.options = fields.map(Box::from).collect(),
70                _ => (),
71            }
72        }
73
74        if entry.title.is_empty() {
75            return Err(EntryError::MisisngTitle);
76        }
77
78        Ok(entry)
79    }
80
81    /// Determines if this boot entry is the current boot entry
82    ///
83    /// # Implementation
84    ///
85    /// This is determined by a matching the entry's initd and options to `/proc/cmdline`.
86    pub fn is_current(&self) -> bool {
87        let initrd = self
88            .initrd
89            .as_ref()
90            .map(|x| ["initrd=", &x.replace('/', "\\")].concat());
91
92        let initrd = initrd.as_ref().map(String::as_str);
93        let options = self.options.iter().map(Box::as_ref);
94
95        let expected_cmdline = initrd.iter().cloned().chain(options);
96
97        crate::kernel_cmdline()
98            .iter()
99            .cloned()
100            .zip(expected_cmdline)
101            .all(|(a, b)| a == b)
102    }
103}