use crate::cmdline::*;
use std::fmt;
use std::fs::File;
use std::io;
use std::io::{Read, Write};
use std::os::raw::{c_char,c_int};
use std::path::Path;
pub type KeyValue = Vec<String>;
pub const BLS_ENTRIES: &str = "/boot/loader/entries";
#[link(name = "rpmio")]
extern {
fn rpmvercmp(a: *const c_char, b: *const c_char) -> c_int;
}
fn compare_kernel_string (a: &std::ffi::CString, b: &std::ffi::CString) -> std::cmp::Ordering {
match unsafe { rpmvercmp(a.as_ptr(), b.as_ptr()) } {
1 => { std::cmp::Ordering::Greater },
0 => { std::cmp::Ordering::Equal },
-1 => { std::cmp::Ordering::Less },
_ => { panic!("rpmvercmp() returned an unexpected value"); }
}
}
#[derive(Debug)]
enum BLSEntryLine {
Comment(String),
Key(String,String),
KeyWithComment(String,String,String),
Empty
}
type BLSEntryData = Vec<BLSEntryLine>;
type BLSEntryList = Vec<String>;
pub struct BLSEntry {
bls_dir: String,
pub name: String
}
impl BLSEntry {
fn get_full_path(&self) -> String {
format!("{}/{}", self.bls_dir, self.name)
}
fn get_bls_dir() -> String {
match std::env::var("BLSCTL_BLS_DIR") {
Ok(env_dir) => env_dir,
_ => { String::from(BLS_ENTRIES) }
}
}
pub fn new (entry: &str) -> Result<BLSEntry, String> {
let index = entry.parse::<usize>();
let bls_dir = Self::get_bls_dir();
let entries = match Self::get_bls_entries() {
Ok(entries) => entries,
Err(e) => { return Err(format!("could not read bootloader entries from {}: {}", bls_dir, e)); }
};
if index.is_ok() {
let index = index.unwrap();
if index >= entries.len() {
return Err(format!("Invalid bootloader index {}, there are only {} entries", index, entries.len()));
}
let entry = &entries[index];
return Ok(BLSEntry {name: entry.to_string().clone(), bls_dir: bls_dir});
}
for e in &entries {
if e == entry || e == &format!("{}.conf", entry) {
return Ok(BLSEntry {name: e.clone(), bls_dir: bls_dir});
}
}
let path = std::path::Path::new(&entry);
if let Ok(abs_path) = path.canonicalize() {
if abs_path.exists() {
if abs_path.parent() != Some(Path::new(bls_dir.as_str())) {
return Err(format!("entry argument {} is not in bootloader entry directory {}", entry, bls_dir));
}
if !abs_path.ends_with(".conf") {
return Err(format!("entry argument {:?} does not have .conf suffix", abs_path));
}
if let Some(file_stem) = abs_path.file_stem() {
if let Some(file_stem) = file_stem.to_str() {
return Ok(BLSEntry {name: String::from(file_stem), bls_dir: bls_dir});
}
}
}
}
let mut candidates: Vec<String> = Vec::new();
for e in entries {
match std::fs::File::open(&format!("{}/{}", bls_dir, e)) {
Ok(file) => {
let mut content = String::new();
let mut reader = std::io::BufReader::new(file);
let _ = reader.read_to_string(&mut content);
for line in content.lines() {
let line = line.split("#").collect::<Vec<_>>()[0].trim();
if line.starts_with("version ") && line.ends_with(entry) {
candidates.push(String::from(e.as_str()));
}
}
}
_ => {}
};
};
if candidates.len() == 1 {
return Ok(BLSEntry {name: entry.to_string().clone(), bls_dir: bls_dir});
}
if candidates.len() > 2 {
return Err(format!("ERROR: kernel version {} was found in multiple boot entries: {}", entry, candidates.join(" ")));
}
Err(format!("ERROR: invalid entry argument {}", entry))
}
pub fn get_bls_entries () -> std::io::Result<BLSEntryList> {
let bls_dir = Self::get_bls_dir();
let dir = std::fs::read_dir(std::path::Path::new(bls_dir.as_str()))?;
let mut entries: Vec<std::ffi::CString> = dir.filter(|entry| {entry.is_ok()})
.map(|entry| {entry.unwrap().file_name().into_string()})
.filter(|entry| {entry.is_ok()})
.map(|entry| {entry.unwrap()})
.filter(|entry| {entry.ends_with(".conf")})
.filter_map(|entry| {
if let Ok(entry) = std::ffi::CString::new(entry) {
Some(entry)
} else {
None
}
})
.collect();
entries.as_mut_slice().sort_by(|a,b| {compare_kernel_string(b,a)});
let entries = entries.drain(..)
.map(|c_entry| { c_entry.into_string() })
.filter_map(|entry| {
if let Ok(entry) = entry {
Some(entry)
} else {
None
}
}).collect();
Ok(entries)
}
fn parse (&self) -> std::io::Result<BLSEntryData> {
let mut result = Vec::new();
let mut content = String::new();
let path = self.get_full_path();
let mut file = std::fs::File::open(&path)?;
file.read_to_string(&mut content)?;
for line in content.lines() {
let mut comment = None;
let line = if line.contains("#") {
let split: Vec<_> = line.splitn(2, "#").collect();
comment = Some(String::from(split[1]));
split[0]
} else {
line
};
if ! line.trim().contains(" ") {
match comment {
Some(comment) => { result.push(BLSEntryLine::Comment(comment)); }
None => { result.push(BLSEntryLine::Empty); }
}
} else {
let line: Vec<&str> = line.trim().splitn(2, " ").collect();
let pair = (String::from(line[0]), String::from(line[1]));
match comment {
Some(comment) => {
result.push(BLSEntryLine::KeyWithComment(pair.0, pair.1, comment));
}
None => { result.push(BLSEntryLine::Key(pair.0, pair.1)); }
}
}
}
Ok(result)
}
fn create_content(&self, data: &BLSEntryData) -> String {
let mut content = String::new();
for line in data {
match line {
BLSEntryLine::Key(key,value) => {
content.push_str(key);
content.push_str(" ");
content.push_str(value);
content.push_str("\n");
},
BLSEntryLine::Comment(comment) => {
content.push_str("#");
content.push_str(comment);
content.push_str("\n");
},
BLSEntryLine::KeyWithComment(key,value,comment) => {
content.push_str(key);
content.push_str(" ");
content.push_str(value);
content.push_str(" #");
content.push_str(comment);
content.push_str("\n");
}
_ => { content.push_str("\n"); }
}
}
content
}
fn commit (&self, data: &BLSEntryData) -> std::io::Result<()> {
let path = self.get_full_path();
let mut file = std::fs::File::create(&path)?;
let content = self.create_content(data);
file.write_all(content.as_bytes())
}
pub fn get(&self, key: &str) -> std::io::Result<KeyValue> {
let data = self.parse()?;
let matches: Vec<_> = data.iter()
.filter(|x| match x {
BLSEntryLine::KeyWithComment(c,_,_) |
BLSEntryLine::Key(c,_) => { c.as_str() == key }
_ => { false }
}).map(|x| match x {
BLSEntryLine::KeyWithComment(_,value,_) |
BLSEntryLine::Key(_,value) => {
String::from(value)
}
_ => { panic!("unreachable code"); }
})
.collect();
match matches.len() {
0 => { Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' does not contain a {} key", self.get_full_path(), key))) },
_ => { Ok(matches) }
}
}
pub fn set(&self, key: &str, value: &[String]) -> std::io::Result<()> {
let mut data = self.parse()?;
let key = String::from(key);
let mut candidates = Vec::new();
let mut i: usize = 0;
let mut count: usize = 0;
for line in data.iter() {
match line {
BLSEntryLine::Key(c,_) => {
if c == key.as_str() {
candidates.push(i);
};
},
BLSEntryLine::KeyWithComment(c,_,_) => {
if c == key.as_str() {
candidates.push(i);
};
},
_ => {}
};
i += 1;
};
for i in &candidates {
if count == value.len() {
break;
}
let candidate = match &data[*i] {
BLSEntryLine::Key(_,_) => {
Some(BLSEntryLine::Key(key.clone(), value[count].clone()))
}
BLSEntryLine::KeyWithComment(_,_,comment) => {
Some(BLSEntryLine::KeyWithComment(key.clone(), value[count].clone(),
comment.clone()))
}
_ => { None }
};
if let Some(line) = candidate {
data[*i] = line;
}
count += 1;
};
if candidates.len() > value.len() {
for i in value.len()..candidates.len() {
data.remove(candidates[i]);
}
}
if count < value.len() {
for i in count..value.len() {
data.push(BLSEntryLine::Key(key.clone(), value[i].clone()));
}
}
self.commit(&data)?;
Ok(())
}
pub fn remove(&self, key: &str) -> std::io::Result<()> {
let mut data = self.parse()?;
let mut i: usize = 0;
let mut candidates = Vec::new();
let mut commit = false;
for line in data.iter() {
match line {
BLSEntryLine::Key(c,_) => {
if c == key {
candidates.push(i);
};
},
BLSEntryLine::KeyWithComment(c,_,_) => {
if c == key {
candidates.push(i);
};
},
_ => {}
};
i += 1;
};
for i in candidates {
if ! commit { commit = true };
match data.remove(i) {
BLSEntryLine::KeyWithComment(_,_,cmnt) => {
data.insert(i, BLSEntryLine::Comment(cmnt));
}
_ => {}
};
};
if commit {
self.commit(&data)
} else {
Ok(())
}
}
pub fn create (entry: &str) -> Result<BLSEntry, String> {
let mut entry = entry.to_string();
if !entry.ends_with(".conf") {
entry = format!("{}.conf", &entry);
}
let path = format!("{}/{}", Self::get_bls_dir(), &entry);
let path = std::path::Path::new(&path);
if path.exists() {
return Err(format!("bootloader entry {} already exists", &entry));
}
if let Err(e) = File::create(&path) {
return Err(e.to_string());
};
BLSEntry::new(&entry)
}
pub fn delete (&self) -> std::io::Result<()> {
let path = self.get_full_path();
std::fs::remove_file(&path)?;
Ok(())
}
}
impl fmt::Display for BLSEntry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let data = self.parse().unwrap();
let content = self.create_content(&data);
write!(f, "{}", content)
}
}
impl CmdlineStore for BLSEntry {
fn cmdline_store(&mut self, cmdline: &Cmdline) -> std::io::Result<()> {
let cmdline_string = match Cmdline::render(cmdline) {
Ok(cmdline_string) => cmdline_string,
Err(error) => {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("could not store kernel cmdline in '{}': {}", self.name, error)));
}
};
self.set("options", &[cmdline_string])
}
fn cmdline (&self) -> std::io::Result<Cmdline> {
match Cmdline::parse(self.get("options")?.join(" ").as_str()) {
Ok(c) => Ok(c),
Err(e) => {
Err(io::Error::new(io::ErrorKind::InvalidData,
format!("could not read kernel cmdline from '{}': {}", self.name, e.1)))
}
}
}
}
fn entry_batch_run (entries: &Vec<String>,
params: &[String],
func: &dyn Fn (&mut BLSEntry, &[String]) -> std::io::Result<()>) -> std::io::Result<()> {
let errors = entries.iter()
.map(|e| {
let mut entry = BLSEntry{name: e.clone(), bls_dir: BLSEntry::get_bls_dir()};
(func(&mut entry, params), e)
})
.filter(|(result, _)| { result.is_err() })
.map(|(result, entry)| { format!("there was an error trying to modify cmdline parameters in {}: {}",
entry, result.err().unwrap()) })
.collect::<Vec<String>>()
.join("\n");
if errors.len() > 0 {
Err(io::Error::new(io::ErrorKind::Interrupted,
errors))
} else {
Ok(())
}
}
impl CmdlineHandler for Vec<String> {
fn cmdline_render(&self) -> std::io::Result<String> {
panic!("not implemented");
}
fn cmdline_get (&self, _: &str) -> std::io::Result<CmdlineParam> {
panic!("not implemented");
}
fn cmdline_set (&mut self, params: &[String]) -> std::io::Result<()> {
entry_batch_run(self, params, &BLSEntry::cmdline_set)
}
fn cmdline_add (&mut self, params: &[String]) -> std::io::Result<()> {
entry_batch_run(self, params, &BLSEntry::cmdline_add)
}
fn cmdline_remove(&mut self, params: &[String]) -> std::io::Result<()> {
entry_batch_run(self, params, &BLSEntry::cmdline_remove)
}
fn cmdline_clear (&mut self, params: &[String]) -> std::io::Result<()> {
entry_batch_run(self, params, &BLSEntry::cmdline_clear)
}
}
#[cfg(test)]
mod bls_tests {
use std::fs;
use std::env;
use super::BLSEntry;
extern crate tempfile;
extern crate serial_test_derive;
use self::serial_test_derive::serial;
fn tests_init () -> tempfile::TempDir {
let tmpdir = tempfile::tempdir().expect("Could not create temp dir");
env::set_var("BLSCTL_BLS_DIR", tmpdir.path().as_os_str());
tmpdir
}
fn tests_finalize () {
env::remove_var("BLSCTL_BLS_DIR");
}
#[test]
#[serial]
fn all_entries() {
let tmpdir = tests_init();
let all = BLSEntry::get_bls_entries().expect("Could not list all entries");
assert_eq!(all.len(), 0);
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
let all = BLSEntry::get_bls_entries().expect("Could not list all entries");
assert_eq!(all, vec!["A.conf"]);
fs::write(format!("{}/B.conf", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
fs::write(format!("{}/C.conf", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
let all = BLSEntry::get_bls_entries().expect("Could not list all entries");
assert_eq!(all, vec!["C.conf", "B.conf", "A.conf"]);
fs::write(format!("{}/C.conf.false", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
let all = BLSEntry::get_bls_entries().expect("Could not list all entries");
assert_eq!(all, vec!["C.conf", "B.conf", "A.conf"]);
tests_finalize();
}
#[test]
#[serial]
fn new () {
let tmpdir = tests_init();
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
let a = BLSEntry::new("A.conf");
assert!(a.is_ok());
let b = BLSEntry::new("B.conf");
assert!(b.is_err());
let zero = BLSEntry::new("0").expect("Get BLSEntry by index");
assert_eq!(zero.name, "A.conf");
let one = BLSEntry::new("1");
assert!(one.is_err());
tests_finalize();
}
#[test]
#[serial]
fn get () {
let tmpdir = tests_init();
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "kernel a=1").expect("Could not write test BLS entry");
let a = BLSEntry::new("A").expect("Could not create BLSEntry instance for existing BLS entry");
let cmdline = a.get("kernel").expect("Could not get value for kernel key in BLS entry");
assert_eq!(cmdline.join(" "), "a=1");
tests_finalize();
}
#[test]
#[serial]
fn set () {
let tmpdir = tests_init();
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "").expect("Could not write test BLS entry");
let a = BLSEntry::new("A").expect("Could not create BLSEntry instance for existing BLS entry");
let _ = a.set("kernel", &[String::from("a=1")]).expect("Could not set value for kernel key in BLS entry");
let cmdline = a.get("kernel").expect("Could not get value for kernel key in BLS entry");
assert_eq!(cmdline.join(" "), String::from("a=1"));
let values = [String::from("foo"), String::from("bar")];
let _ = a.set("initrd", &values);
let initrd = a.get("initrd").expect("Could not get value for initrd key in BLS entry");
assert_eq!(initrd.join(" "), String::from("foo bar"));
let values = [String::from("A"), String::from("B"), String::from("C")];
let _ = a.set("initrd", &values);
let initrd = a.get("initrd").expect("Could not get value for initrd key in BLS entry");
assert_eq!(initrd.join(" "), String::from("A B C"));
let values = [String::from("X"), String::from("Y")];
let _ = a.set("initrd", &values);
let initrd = a.get("initrd").expect("Could not get value for initrd key in BLS entry");
assert_eq!(initrd.join(" "), String::from("X Y"));
tests_finalize();
}
#[test]
#[serial]
fn remove () {
let tmpdir = tests_init();
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "kernel a=1").expect("Could not write test BLS entry");
let a = BLSEntry::new("A").expect("Could not create BLSEntry instance for existing BLS entry");
let _ = a.remove("kernel").expect("Could not remove kernel key in BLS entry");
assert!(a.get("kernel").is_err());
tests_finalize();
}
#[test]
#[serial]
fn create () {
let _tmpdir = tests_init();
let a = BLSEntry::create("A").expect("Could not create BLSEntry");
assert!(BLSEntry::create("A").is_err());
assert!(BLSEntry::create("A.conf").is_err());
let _ = a.delete();
let _ = BLSEntry::create("A.conf").expect("Could not create BLSEntry after delete");
assert!(BLSEntry::create("A").is_err());
assert!(BLSEntry::create("A.conf").is_err());
tests_finalize();
}
#[test]
#[serial]
fn delete () {
let tmpdir = tests_init();
fs::write(format!("{}/A.conf", tmpdir.path().to_str().unwrap()), "kernel a=1").expect("Could not write test BLS entry");
let a = BLSEntry::new("A").expect("Could not create BLSEntry instance for existing BLS entry");
let _ = a.delete();
assert!(BLSEntry::new("A").is_err());
tests_finalize();
}
}