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
use itertools::Itertools;
use std::fs::File;
use std::io::{self, BufRead, BufReader};
use std::path::Path;

#[derive(Debug, Error)]
pub enum EntryError {
    #[error("error reading line in entry file")]
    Line(#[source] io::Error),
    #[error("title field is missing")]
    MisisngTitle,
    #[error("entry is not a file")]
    NotAFile,
    #[error("entry does not have a file name")]
    NoFilename,
    #[error("initrd was defined without a value")]
    NoValueForInitrd,
    #[error("linux was defined without a value")]
    NoValueForLinux,
    #[error("error opening entry file")]
    Open(#[source] io::Error),
    #[error("entry has a file name that is not UTF-8")]
    Utf8Filename,
}

#[derive(Debug, Default, Clone)]
pub struct Entry {
    pub id: Box<str>,
    pub initrd: Option<Box<str>>,
    pub linux: Box<str>,
    pub options: Vec<Box<str>>,
    pub title: Box<str>,
}

impl Entry {
    pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, EntryError> {
        let path = path.as_ref();

        if !path.is_file() {
            return Err(EntryError::NotAFile);
        }

        let file_name = match path.file_stem() {
            Some(file_name) => match file_name.to_str() {
                Some(file_name) => file_name.to_owned(),
                None => return Err(EntryError::Utf8Filename),
            },
            None => return Err(EntryError::NoFilename),
        };

        let file = File::open(path).map_err(EntryError::Open)?;

        let mut entry = Entry::default();
        entry.id = file_name.into();

        for line in BufReader::new(file).lines() {
            let line = line.map_err(EntryError::Line)?;
            let mut fields = line.split_whitespace();
            match fields.next() {
                Some("title") => entry.title = fields.join(" ").into(),
                Some("linux") => match fields.next() {
                    Some(value) => entry.linux = value.into(),
                    None => return Err(EntryError::NoValueForLinux),
                },
                Some("initrd") => match fields.next() {
                    Some(value) => entry.initrd = Some(value.into()),
                    None => return Err(EntryError::NoValueForInitrd),
                },
                Some("options") => entry.options = fields.map(Box::from).collect(),
                _ => (),
            }
        }

        if entry.title.is_empty() {
            return Err(EntryError::MisisngTitle);
        }

        Ok(entry)
    }

    /// Determines if this boot entry is the current boot entry
    ///
    /// # Implementation
    ///
    /// This is determined by a matching the entry's initd and options to `/proc/cmdline`.
    pub fn is_current(&self) -> bool {
        let initrd = self
            .initrd
            .as_ref()
            .map(|x| ["initrd=", &x.replace('/', "\\")].concat());

        let initrd = initrd.as_ref().map(String::as_str);
        let options = self.options.iter().map(Box::as_ref);

        let expected_cmdline = initrd.iter().cloned().chain(options);

        crate::kernel_cmdline()
            .iter()
            .cloned()
            .zip(expected_cmdline)
            .all(|(a, b)| a == b)
    }
}