pub const BLS_ENTRIES: &str = "/boot/loader/entries";
use std::path::{Path, PathBuf};
use std::io;
use std::io::{Read, Write, ErrorKind};
extern crate ini;
use cmdline::*;
pub struct Environment {
pub clamped: Option<usize>,
pub envpath: PathBuf
}
macro_rules! parse_ini {
($envpath:expr) => {
match ini::Ini::load_from_file($envpath) {
Ok(val) => val,
Err(_) => {
return Err(io::Error::new(ErrorKind::InvalidData,
"Could not parse environment data"));
}
}
}
}
#[allow(dead_code)]
pub fn get_os_id () -> Option<String> {
let ini = match ini::Ini::load_from_file("/etc/os-release") {
Ok(ini) => ini,
Err(_) => { return None; }
};
match ini.general_section().get("ID") {
Some(val) => {
let val = val.trim();
if val == "rhel" {
Some(String::from("redhat"))
} else {
Some(String::from(val))
}
},
_ => None
}
}
#[allow(dead_code)]
pub fn get_env_path () -> String {
match std::env::var_os("BOOTLOADER_ENV_PATH") {
Some(path) => match path.into_string() {
Ok(path) => { return path; },
_ => {}
}
_ => {}
};
if std::path::Path::new("/sys/firmware/efi").is_dir() {
let id = get_os_id();
if id.is_some () {
let id = id.unwrap();
let esp = "/boot/efi/EFI";
let efipath_legacy = format!("{}/{}/grubenv", esp, id);
if std::path::Path::new(&efipath_legacy).is_file() {
return efipath_legacy;
}
return format!("{}/{}/env", esp, id);
};
};
String::from("/boot/grub2/grubenv")
}
impl Environment {
pub fn new (clamped: Option<usize>, filepath: &str) -> io::Result<Environment> {
let path = Path::new(filepath);
let exists = path.exists();
let clamp_size;
let clamped = match clamped {
None | Some(0) => { clamp_size = 0; None }
Some(clamped) => { clamp_size = 512 * clamped as u64 ; Some(clamped) }
};
let mut grubenv = std::fs::OpenOptions::new().write(true)
.read(true)
.truncate(false)
.create(true)
.open(filepath)?;
let file_size = std::fs::metadata(path)?.len();
if exists {
if clamped.is_some() && { file_size != clamp_size } {
return Err(io::Error::new(io::ErrorKind::InvalidData, format!("Tried to open a file padded at {} bytes but was {}", clamp_size, file_size)));
};
let mut buffer = Vec::<u8>::new();
buffer.resize("# GRUB Environment Block\n".len(), b'0');
grubenv.read_exact(buffer.as_mut_slice())?;
if String::from("# GRUB Environment Block\n").into_bytes() == buffer {
Ok(Environment{envpath: path.to_path_buf(), clamped: clamped})
} else {
Err(io::Error::new(io::ErrorKind::InvalidData, "Invalid GRUB environment file"))
}
} else {
let _ = grubenv.write("# GRUB Environment Block\n".as_bytes());
Ok(Environment{envpath: path.to_path_buf(), clamped: clamped})
}
}
fn commit_env (&self, env_ini: ini::Ini) -> std::io::Result<()> {
let mut content: Vec<u8> = Vec::new();
for byte in "# GRUB Environment Block\n".as_bytes() {
content.push(byte.clone());
}
env_ini.write_to(&mut content)?;
if self.clamped.is_some() {
let max = self.clamped.unwrap() * 512;
if content.len() > max {
return Err(io::Error::new(io::ErrorKind::InvalidData,
format!("Could only write {} bytes but environment is {}", max, content.len())));
}
content.resize(max, b'#');
}
std::fs::write(self.envpath.as_path(), content)?;
Ok(())
}
pub fn get (&self, key: &str) -> std::io::Result<String> {
let env_ini = parse_ini!(&self.envpath);
let settings = match env_ini.section(None::<String>) {
Some(section) => section,
_ => { return Ok(String::new()); }
};
match settings.get(key) {
Some(val) => Ok(val.clone()),
_ => Err(io::Error::new(io::ErrorKind::InvalidInput,
format!("'{}' variable not present in environment file", key)))
}
}
pub fn show(&self) -> std::io::Result<String> {
let mut output: Vec<u8> = Vec::new();
let env_ini = parse_ini!(&self.envpath);
let _ = env_ini.write_to(&mut output);
unsafe {
Ok(String::from_utf8_unchecked(output))
}
}
pub fn set (&self, key: &str, value: &str) -> std::io::Result<()> {
let mut env_ini: ini::Ini = parse_ini!(&self.envpath.as_path());
{
env_ini.with_section(None::<String>).set(String::from(key), String::from(value));
}
self.commit_env(env_ini)
}
pub fn unset (&self, key: &str) -> std::io::Result<()> {
let mut env_ini: ini::Ini = parse_ini!(&self.envpath.as_path());
{
env_ini.general_section_mut().remove(&String::from(key));
}
self.commit_env(env_ini)
}
}
impl CmdlineStore for Environment {
fn cmdline_store(&mut self, cmdline: &Cmdline) -> std::io::Result<()> {
match Cmdline::render(&cmdline) {
Ok(cmdline) => {
match self.set("kernelopts", &cmdline) {
Ok(_) => Ok(()),
Err(error) => {
Err(io::Error::new(io::ErrorKind::InvalidData,
format!("{}", error)))
}
}
},
Err(error) => {
Err(io::Error::new(io::ErrorKind::InvalidData,
format!("{}", error)))
}
}
}
fn cmdline (&self) -> std::io::Result<Cmdline> {
let cmdline = match self.get("kernelopts") {
Ok(cmdline) => cmdline,
_ => String::new()
};
cmdline_parse(cmdline.as_str())
}
}
#[cfg(test)]
mod blenv_tests {
use std::fs;
use blenv;
extern crate tempfile;
extern crate serial_test_derive;
use self::serial_test_derive::serial;
fn tests_init () -> (tempfile::TempDir, std::path::PathBuf) {
let tmpdir = tempfile::tempdir().expect("Could not create temp dir");
let mut envpath = tmpdir.path().to_path_buf();
envpath.push(std::path::Path::new("grubenv"));
(tmpdir, envpath)
}
#[test]
#[serial]
fn set () {
let (_tmpdir, envpath) = tests_init();
blenv::Environment::new(None, &envpath.to_string_lossy()).expect("Could not create grubenv file")
.set("foo", "asd").expect("Could not set foo=asd in grubenv file");
assert_eq!("# GRUB Environment Block\nfoo=asd\n", fs::read_to_string(envpath.as_path()).unwrap().as_str());
}
#[test]
#[serial]
fn get () {
let (_tmpdir, envpath) = tests_init();
fs::write(envpath.as_path(), "# GRUB Environment Block\nfoo=asd\n").expect("Could not write Environment file");
let ret = blenv::Environment::new(None, &envpath.to_string_lossy()).expect("Could not create grubenv file").get("foo").expect("Could not get foo property");
assert_eq!("asd", ret.as_str());
}
#[test]
#[serial]
fn show () {
let (_tmpdir, envpath) = tests_init();
fs::write(envpath.as_path(), "# GRUB Environment Block\nfoo=asd\n").expect("Could not write Environment file");
let ret = blenv::Environment::new(None, &envpath.to_string_lossy()).expect("Could not read grubenv file");
assert_eq!("foo=asd\n", ret.show().expect("Could not call Environment::show").as_str());
}
#[test]
#[serial]
fn invalid_header () {
let (_tmpdir, envpath) = tests_init();
fs::write(envpath.as_path(), "# Bogus header Block\nfoo=asd\n").expect("Could not write Environment file");
let ret = blenv::Environment::new(None, &envpath.to_string_lossy());
assert!(ret.is_err());
}
#[test]
#[serial]
fn clamp () {
let (_tmpdir, envpath) = tests_init();
for (clamp, size) in &[(None, 39), (Some(0), 39), (Some(1), 512), (Some(2), 1024)] {
if size != &39 { let mut buffer: Vec<u8> = "# GRUB Environment Block\n".bytes().collect();
buffer.resize(size.clone() as usize, 35 as u8); let _ = fs::write(&envpath, buffer);
};
let env = blenv::Environment::new(clamp.clone(), &envpath.to_string_lossy()).expect("Could not create grubenv file");
if let Err(e) = env.set("somekey", "value") {
panic!("Could not set key=value {:?}", e);
};
assert_eq!(&fs::metadata(&envpath).expect("Could not get file metadata").len(),
size);
};
let _ = fs::remove_file(&envpath);
let env = blenv::Environment::new(Some(1), &envpath.to_string_lossy()).expect("Could not create grubenv file");
let _ = env.set("foo", "bar");
assert_eq!(env.get("foo").expect("Could not get 'foo' property"), "bar");
assert_eq!(fs::metadata(&envpath).expect("Could not get grubenv metadata").len(), 512);
}
}