systemd_boot_conf/
lib.rs

1//! Rust crate for managing the systemd-boot loader configuration.
2
3#[macro_use]
4extern crate thiserror;
5
6pub mod entry;
7pub mod loader;
8
9use self::entry::*;
10use self::loader::*;
11
12use once_cell::sync::OnceCell;
13
14use std::fs;
15use std::fs::File;
16use std::io::prelude::*;
17use std::io::{self, BufWriter};
18use std::path::{Path, PathBuf};
19
20#[derive(Debug, Error)]
21pub enum Error {
22    #[error("error reading loader enrties directory")]
23    EntriesDir(#[source] io::Error),
24    #[error("error parsing entry at {:?}", path)]
25    Entry { path: PathBuf, source: EntryError },
26    #[error("error writing entry file")]
27    EntryWrite(#[source] io::Error),
28    #[error("error reading entry in loader entries directory")]
29    FileEntry(#[source] io::Error),
30    #[error("error parsing loader conf at {:?}", path)]
31    Loader { path: PathBuf, source: LoaderError },
32    #[error("error writing loader file")]
33    LoaderWrite(#[source] io::Error),
34    #[error("entry not found in data structure")]
35    NotFound,
36}
37
38#[derive(Debug, Clone)]
39pub struct SystemdBootConf {
40    pub efi_mount: Box<Path>,
41    pub entries_path: Box<Path>,
42    pub loader_path: Box<Path>,
43    pub entries: Vec<Entry>,
44    pub loader_conf: LoaderConf,
45}
46
47impl SystemdBootConf {
48    pub fn new<P: Into<PathBuf>>(efi_mount: P) -> Result<Self, Error> {
49        let efi_mount = efi_mount.into();
50        let entries_path = efi_mount.join("loader/entries").into();
51        let loader_path = efi_mount.join("loader/loader.conf").into();
52
53        let mut manager = Self {
54            efi_mount: efi_mount.into(),
55            entries_path,
56            loader_path,
57            entries: Vec::default(),
58            loader_conf: LoaderConf::default(),
59        };
60
61        manager.load_conf()?;
62        manager.load_entries()?;
63
64        Ok(manager)
65    }
66
67    /// Find the boot entry which matches the current boot
68    ///
69    /// # Implementation
70    ///
71    /// The current boot option is determined by a matching the entry's initd and options
72    /// to `/proc/cmdline`.
73    pub fn current_entry(&self) -> Option<&Entry> {
74        self.entries.iter().find(|e| e.is_current())
75    }
76
77    /// Validate that the default entry exists.
78    pub fn default_entry_exists(&self) -> DefaultState {
79        match self.loader_conf.default {
80            Some(ref default) => {
81                if self.entry_exists(default) {
82                    DefaultState::Exists
83                } else {
84                    DefaultState::DoesNotExist
85                }
86            }
87            None => DefaultState::NotDefined,
88        }
89    }
90
91    /// Validates that an entry exists with this name.
92    pub fn entry_exists(&self, entry: &str) -> bool {
93        self.entries.iter().any(|e| e.id.as_ref() == entry)
94    }
95
96    /// Get the entry that corresponds to the given name.
97    pub fn get(&self, entry: &str) -> Option<&Entry> {
98        self.entries.iter().find(|e| e.id.as_ref() == entry)
99    }
100
101    /// Get a mutable entry that corresponds to the given name.
102    pub fn get_mut(&mut self, entry: &str) -> Option<&mut Entry> {
103        self.entries.iter_mut().find(|e| e.id.as_ref() == entry)
104    }
105
106    /// Attempt to re-read the loader configuration.
107    pub fn load_conf(&mut self) -> Result<(), Error> {
108        let &mut SystemdBootConf {
109            ref mut loader_conf,
110            ref loader_path,
111            ..
112        } = self;
113
114        *loader_conf = LoaderConf::from_path(loader_path).map_err(move |source| Error::Loader {
115            path: loader_path.to_path_buf(),
116            source,
117        })?;
118
119        Ok(())
120    }
121
122    /// Attempt to load all of the available entries in the system.
123    pub fn load_entries(&mut self) -> Result<(), Error> {
124        let &mut SystemdBootConf {
125            ref mut entries,
126            ref entries_path,
127            ..
128        } = self;
129        let dir_entries = fs::read_dir(entries_path).map_err(Error::EntriesDir)?;
130
131        entries.clear();
132        for entry in dir_entries {
133            let entry = entry.map_err(Error::FileEntry)?;
134            let path = entry.path();
135
136            // Only consider conf files in the directory.
137            if !path.is_file() || path.extension().map_or(true, |ext| ext != "conf") {
138                continue;
139            }
140
141            let entry = Entry::from_path(&path).map_err(move |source| Error::Entry {
142                path: path.to_path_buf(),
143                source,
144            })?;
145
146            entries.push(entry);
147        }
148
149        Ok(())
150    }
151
152    /// Overwrite the conf file with stored values.
153    pub fn overwrite_loader_conf(&self) -> Result<(), Error> {
154        let result = Self::try_io(&self.loader_path, |file| {
155            if let Some(ref default) = self.loader_conf.default {
156                writeln!(file, "default {}", default)?;
157            }
158
159            if let Some(timeout) = self.loader_conf.timeout {
160                writeln!(file, "timeout {}", timeout)?;
161            }
162
163            Ok(())
164        });
165
166        result.map_err(Error::LoaderWrite)
167    }
168
169    /// Overwrite the entry conf for the given entry.
170    pub fn overwrite_entry_conf(&self, entry: &str) -> Result<(), Error> {
171        let entry = match self.get(entry) {
172            Some(entry) => entry,
173            None => return Err(Error::NotFound),
174        };
175
176        let result = Self::try_io(
177            &self.entries_path.join(format!("{}.conf", entry.id)),
178            move |file| {
179                writeln!(file, "title {}", entry.title)?;
180                writeln!(file, "linux {}", entry.linux)?;
181
182                if let Some(ref initrd) = entry.initrd {
183                    writeln!(file, "initrd {}", initrd)?;
184                }
185
186                if !entry.options.is_empty() {
187                    writeln!(file, "options: {}", entry.options.join(" "))?;
188                }
189
190                Ok(())
191            },
192        );
193
194        result.map_err(Error::EntryWrite)
195    }
196
197    fn try_io<F: FnMut(&mut BufWriter<File>) -> io::Result<()>>(
198        path: &Path,
199        mut instructions: F,
200    ) -> io::Result<()> {
201        instructions(&mut BufWriter::new(File::create(path)?))
202    }
203}
204
205#[derive(Debug, Copy, Clone)]
206pub enum DefaultState {
207    NotDefined,
208    Exists,
209    DoesNotExist,
210}
211
212/// Fetches the kernel command line, and lazily initialize it if it has not been fetched.
213pub fn kernel_cmdline() -> &'static [&'static str] {
214    static CMDLINE_BUF: OnceCell<Box<str>> = OnceCell::new();
215    static CMDLINE: OnceCell<Box<[&'static str]>> = OnceCell::new();
216
217    CMDLINE.get_or_init(|| {
218        let cmdline = CMDLINE_BUF.get_or_init(|| {
219            fs::read_to_string("/proc/cmdline")
220                .unwrap_or_default()
221                .into()
222        });
223
224        cmdline
225            .split_ascii_whitespace()
226            .collect::<Vec<&'static str>>()
227            .into()
228    })
229}