1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
//! Rust crate for managing the systemd-boot loader configuration.

#[macro_use]
extern crate thiserror;

pub mod entry;
pub mod loader;

use self::entry::*;
use self::loader::*;

use once_cell::sync::OnceCell;

use std::fs;
use std::fs::File;
use std::io::prelude::*;
use std::io::{self, BufWriter};
use std::path::{Path, PathBuf};

#[derive(Debug, Error)]
pub enum Error {
    #[error("error reading loader enrties directory")]
    EntriesDir(#[source] io::Error),
    #[error("error parsing entry at {:?}", path)]
    Entry { path: PathBuf, source: EntryError },
    #[error("error writing entry file")]
    EntryWrite(#[source] io::Error),
    #[error("error reading entry in loader entries directory")]
    FileEntry(#[source] io::Error),
    #[error("error parsing loader conf at {:?}", path)]
    Loader { path: PathBuf, source: LoaderError },
    #[error("error writing loader file")]
    LoaderWrite(#[source] io::Error),
    #[error("entry not found in data structure")]
    NotFound,
}

#[derive(Debug, Clone)]
pub struct SystemdBootConf {
    pub efi_mount: Box<Path>,
    pub entries_path: Box<Path>,
    pub loader_path: Box<Path>,
    pub entries: Vec<Entry>,
    pub loader_conf: LoaderConf,
}

impl SystemdBootConf {
    pub fn new<P: Into<PathBuf>>(efi_mount: P) -> Result<Self, Error> {
        let efi_mount = efi_mount.into();
        let entries_path = efi_mount.join("loader/entries").into();
        let loader_path = efi_mount.join("loader/loader.conf").into();

        let mut manager = Self {
            efi_mount: efi_mount.into(),
            entries_path,
            loader_path,
            entries: Vec::default(),
            loader_conf: LoaderConf::default(),
        };

        manager.load_conf()?;
        manager.load_entries()?;

        Ok(manager)
    }

    /// Find the boot entry which matches the current boot
    ///
    /// # Implementation
    ///
    /// The current boot option is determined by a matching the entry's initd and options
    /// to `/proc/cmdline`.
    pub fn current_entry(&self) -> Option<&Entry> {
        self.entries.iter().find(|e| e.is_current())
    }

    /// Validate that the default entry exists.
    pub fn default_entry_exists(&self) -> DefaultState {
        match self.loader_conf.default {
            Some(ref default) => {
                if self.entry_exists(default) {
                    DefaultState::Exists
                } else {
                    DefaultState::DoesNotExist
                }
            }
            None => DefaultState::NotDefined,
        }
    }

    /// Validates that an entry exists with this name.
    pub fn entry_exists(&self, entry: &str) -> bool {
        self.entries.iter().any(|e| e.id.as_ref() == entry)
    }

    /// Get the entry that corresponds to the given name.
    pub fn get(&self, entry: &str) -> Option<&Entry> {
        self.entries.iter().find(|e| e.id.as_ref() == entry)
    }

    /// Get a mutable entry that corresponds to the given name.
    pub fn get_mut(&mut self, entry: &str) -> Option<&mut Entry> {
        self.entries.iter_mut().find(|e| e.id.as_ref() == entry)
    }

    /// Attempt to re-read the loader configuration.
    pub fn load_conf(&mut self) -> Result<(), Error> {
        let &mut SystemdBootConf {
            ref mut loader_conf,
            ref loader_path,
            ..
        } = self;

        *loader_conf = LoaderConf::from_path(loader_path).map_err(move |source| Error::Loader {
            path: loader_path.to_path_buf(),
            source,
        })?;

        Ok(())
    }

    /// Attempt to load all of the available entries in the system.
    pub fn load_entries(&mut self) -> Result<(), Error> {
        let &mut SystemdBootConf {
            ref mut entries,
            ref entries_path,
            ..
        } = self;
        let dir_entries = fs::read_dir(entries_path).map_err(Error::EntriesDir)?;

        entries.clear();
        for entry in dir_entries {
            let entry = entry.map_err(Error::FileEntry)?;
            let path = entry.path();

            // Only consider conf files in the directory.
            if !path.is_file() || path.extension().map_or(true, |ext| ext != "conf") {
                continue;
            }

            let entry = Entry::from_path(&path).map_err(move |source| Error::Entry {
                path: path.to_path_buf(),
                source,
            })?;

            entries.push(entry);
        }

        Ok(())
    }

    /// Overwrite the conf file with stored values.
    pub fn overwrite_loader_conf(&self) -> Result<(), Error> {
        let result = Self::try_io(&self.loader_path, |file| {
            if let Some(ref default) = self.loader_conf.default {
                writeln!(file, "default {}", default)?;
            }

            if let Some(timeout) = self.loader_conf.timeout {
                writeln!(file, "timeout {}", timeout)?;
            }

            Ok(())
        });

        result.map_err(Error::LoaderWrite)
    }

    /// Overwrite the entry conf for the given entry.
    pub fn overwrite_entry_conf(&self, entry: &str) -> Result<(), Error> {
        let entry = match self.get(entry) {
            Some(entry) => entry,
            None => return Err(Error::NotFound),
        };

        let result = Self::try_io(
            &self.entries_path.join(format!("{}.conf", entry.id)),
            move |file| {
                writeln!(file, "title {}", entry.title)?;
                writeln!(file, "linux {}", entry.linux)?;

                if let Some(ref initrd) = entry.initrd {
                    writeln!(file, "initrd {}", initrd)?;
                }

                if !entry.options.is_empty() {
                    writeln!(file, "options: {}", entry.options.join(" "))?;
                }

                Ok(())
            },
        );

        result.map_err(Error::EntryWrite)
    }

    fn try_io<F: FnMut(&mut BufWriter<File>) -> io::Result<()>>(
        path: &Path,
        mut instructions: F,
    ) -> io::Result<()> {
        instructions(&mut BufWriter::new(File::create(path)?))
    }
}

#[derive(Debug, Copy, Clone)]
pub enum DefaultState {
    NotDefined,
    Exists,
    DoesNotExist,
}

/// Fetches the kernel command line, and lazily initialize it if it has not been fetched.
pub fn kernel_cmdline() -> &'static [&'static str] {
    static CMDLINE_BUF: OnceCell<Box<str>> = OnceCell::new();
    static CMDLINE: OnceCell<Box<[&'static str]>> = OnceCell::new();

    CMDLINE.get_or_init(|| {
        let cmdline = CMDLINE_BUF.get_or_init(|| {
            fs::read_to_string("/proc/cmdline")
                .unwrap_or_default()
                .into()
        });

        cmdline
            .split_ascii_whitespace()
            .collect::<Vec<&'static str>>()
            .into()
    })
}