use alloc::{borrow::ToOwned, format, string::String, vec::Vec};
use log::{error, warn};
use uefi::{CStr16, CString16, Handle, cstr16, proto::media::file::FileInfo};
use crate::{
BootResult,
config::{
Config,
builder::ConfigBuilder,
parsers::{ConfigParser, Parsers},
},
error::BootError,
system::{
fs::{FsError, UefiFileSystem},
helper::{get_path_cstr, str_to_cstr},
},
};
const BLS_PREFIX: &CStr16 = cstr16!("\\loader\\entries");
const BLS_SUFFIX: &str = ".conf";
struct BootCounter {
base_name: String,
left: u32,
done: u32,
}
impl BootCounter {
fn new(filename: impl Into<String>) -> Option<Self> {
let filename = filename.into();
let filename = filename.trim_end_matches(BLS_SUFFIX);
let v: Vec<&str> = filename.rsplitn(2, '+').collect();
if v.len() != 2 {
return None;
}
let counter = v[0];
let (left, done) = match counter.split_once('-') {
Some((l, d)) => (l.parse().ok()?, d.parse().ok()?),
None => (counter.parse().ok()?, 0),
};
Some(Self {
base_name: v[1].to_owned(),
left,
done,
})
}
fn to_filename(&self) -> BootResult<CString16> {
let str = if self.done > 0 {
format!("{}+{}-{}.conf", self.base_name, self.left, self.done)
} else {
format!("{}+{}.conf", self.base_name, self.left)
};
Ok(str_to_cstr(&str)?)
}
const fn decrement(&mut self) {
if self.left > 0 {
self.left -= 1;
self.done += 1;
}
}
const fn is_bad(&self) -> bool {
self.left == 0
}
}
#[derive(Default)]
pub struct BlsConfig {
title: Option<String>,
version: Option<String>,
machine_id: Option<String>,
sort_key: Option<String>,
linux: Option<String>,
initrd: Option<String>,
efi: Option<String>,
options: Option<String>,
devicetree: Option<String>,
devicetree_overlay: Option<String>,
architecture: Option<String>,
}
impl BlsConfig {
#[must_use = "Has no effect if the result is unused"]
pub fn new(content: &[u8], bytes: Option<usize>) -> Self {
let mut config = Self::default();
let slice = &content[0..bytes.unwrap_or(content.len()).min(content.len())];
if let Ok(content) = str::from_utf8(slice) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
config.assign_to_field(line);
}
}
config
}
fn assign_to_field(&mut self, line: &str) {
if let Some((key, value)) = line.split_once(' ') {
let value = value.trim().to_owned();
match &*key.to_ascii_lowercase() {
"title" => self.title = Some(value),
"version" => self.version = Some(value),
"machine_id" => self.machine_id = Some(value),
"sort_key" => self.sort_key = Some(value),
"linux" => self.linux = Some(value),
"initrd" => {
if let Some(initrd) = &mut self.initrd {
initrd.push(' ');
initrd.push_str(&value);
} else {
self.initrd = Some(value);
}
}
"efi" => self.efi = Some(value),
"options" => self.options = Some(value),
"devicetree" => self.devicetree = Some(value),
"devicetree_overlay" => self.devicetree_overlay = Some(value),
"architecture" => self.architecture = Some(value.to_ascii_lowercase()),
_ => warn!("[BLS PARSER]: Found unrecognized key {key} with value {value}"),
}
}
}
#[must_use = "Has no effect if the result is unused"]
fn get_options(&self) -> String {
let mut options = String::new();
if let Some(opts) = &self.options {
options.push_str(opts);
}
self.initrd_options(&mut options);
options
}
fn initrd_options(&self, buffer: &mut String) {
if let Some(initrd) = &self.initrd {
for initrd in initrd.split_ascii_whitespace() {
if !buffer.is_empty() {
buffer.push(' ');
}
buffer.push_str("initrd=");
buffer.push_str(initrd);
}
}
}
}
impl ConfigParser for BlsConfig {
fn parse_configs(fs: &mut UefiFileSystem, handle: Handle, configs: &mut Vec<Config>) {
let dir = fs.read_filtered_dir(BLS_PREFIX, BLS_SUFFIX);
for file in dir {
match get_bls_config(&file, fs, handle) {
Ok(Some(config)) => configs.push(config),
Err(e) => warn!("{e}"),
_ => (),
}
}
}
}
fn get_bls_config(
file: &FileInfo,
fs: &mut UefiFileSystem,
handle: Handle,
) -> BootResult<Option<Config>> {
let mut buf = [0; 4096]; let path = get_path_cstr(BLS_PREFIX, file.file_name())?;
let read_result = fs.read_into(&path, &mut buf);
let (bytes, buf) = match read_result {
Ok(bytes) => (bytes, &buf[..]),
Err(FsError::BufTooSmall(bytes)) => (bytes, &fs.read(&path)?[..]),
Err(e) => return Err(BootError::FsError(e)),
};
let bls_config = BlsConfig::new(buf, Some(bytes));
let options = bls_config.get_options();
let Some(efi_path) = bls_config.linux.or(bls_config.efi) else {
return Ok(None);
};
let config = ConfigBuilder::new(file.file_name(), BLS_SUFFIX)
.efi_path(efi_path)
.options(options)
.set_bad(check_bad(file, fs))
.fs_handle(handle)
.origin(Parsers::Bls)
.assign_if_some(bls_config.title, ConfigBuilder::title)
.assign_if_some(bls_config.version, ConfigBuilder::version)
.assign_if_some(bls_config.machine_id, ConfigBuilder::machine_id)
.assign_if_some(bls_config.sort_key, ConfigBuilder::sort_key)
.assign_if_some(bls_config.devicetree, ConfigBuilder::devicetree_path)
.assign_if_some(bls_config.architecture, ConfigBuilder::architecture);
Ok(Some(config.build()))
}
fn check_bad(file: &FileInfo, fs: &mut UefiFileSystem) -> bool {
let counter = BootCounter::new(file.file_name());
if let Some(mut counter) = counter {
if counter.is_bad() {
return true; }
counter.decrement();
let Ok(counter_name) = counter.to_filename() else {
return false; };
let Ok(src) = get_path_cstr(BLS_PREFIX, file.file_name()) else {
return false;
};
let Ok(dst) = get_path_cstr(BLS_PREFIX, &counter_name) else {
return false;
};
if let Err(e) = fs.rename(&src, &dst) {
error!("{e}");
}
}
false
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[test]
fn test_basic_config() {
let config = b"
title Linux
linux /vmlinuz-linux
initrd /initramfs-linux.img
options root=PARTUUID=1234abcd-56ef-78gh-90ij-klmnopqrstuv rw
";
let bls_config = BlsConfig::new(config, None);
assert_eq!(bls_config.title, Some("Linux".to_owned()));
assert_eq!(bls_config.linux, Some("/vmlinuz-linux".to_owned()));
assert_eq!(bls_config.initrd, Some("/initramfs-linux.img".to_owned()));
assert_eq!(
bls_config.options,
Some("root=PARTUUID=1234abcd-56ef-78gh-90ij-klmnopqrstuv rw".to_owned())
);
assert_eq!(
bls_config.get_options(),
"root=PARTUUID=1234abcd-56ef-78gh-90ij-klmnopqrstuv rw initrd=/initramfs-linux.img"
.to_owned()
);
}
#[test]
fn test_multiple_initrd() {
let config = b"
title Linux
linux /vmlinuz-linux
initrd /intel-ucode.img
initrd /initramfs-linux.img
options root=PARTUUID=dcba4321-fe65-hg87-ji09-vutsrqponmlk ro
";
let bls_config = BlsConfig::new(config, None);
assert_eq!(
bls_config.initrd,
Some("/intel-ucode.img /initramfs-linux.img".to_owned())
);
assert_eq!(bls_config.get_options(), "root=PARTUUID=dcba4321-fe65-hg87-ji09-vutsrqponmlk ro initrd=/intel-ucode.img initrd=/initramfs-linux.img".to_owned());
}
#[test]
fn test_comment() {
let config = b"
# A comment that should be ignored.
title Linux
linux /vmlinuz-linux
";
let bls_config = BlsConfig::new(config, None);
assert_eq!(bls_config.title, Some("Linux".to_owned()));
assert_eq!(bls_config.linux, Some("/vmlinuz-linux".to_owned()));
}
#[test]
fn test_duplicate() {
let config = b"
title Linux
title Linux 2
linux /vmlinuz-linux
";
let bls_config = BlsConfig::new(config, None);
assert_eq!(bls_config.title, Some("Linux 2".to_owned()));
assert_eq!(bls_config.linux, Some("/vmlinuz-linux".to_owned()));
}
#[test]
fn test_invalid_keys() {
let config = b"
title Linux
invalid invalid
someother invalid
";
let bls_config = BlsConfig::new(config, None);
assert_eq!(bls_config.title, Some("Linux".to_owned())); }
#[test]
fn test_boot_counter() {
let filename = "somelinuxconf+3.conf";
let mut ctr = BootCounter::new(filename)
.expect("Failed to create a boot counter from valid filename in test");
ctr.decrement();
assert_eq!(
ctr.to_filename().ok(),
CString16::try_from("somelinuxconf+2-1.conf").ok()
);
ctr.decrement();
assert_eq!(
ctr.to_filename().ok(),
CString16::try_from("somelinuxconf+1-2.conf").ok()
);
ctr.decrement();
assert_eq!(
ctr.to_filename().ok(),
CString16::try_from("somelinuxconf+0-3.conf").ok()
);
assert!(ctr.is_bad());
}
proptest! {
#[test]
fn doesnt_panic(x in any::<Vec<u8>>(), y in any::<usize>()) {
let _ = BlsConfig::new(&x, Some(y));
}
#[test]
fn sets_title(x in any::<String>()) {
let x = x.trim();
let title = format!("title {x}");
let config = BlsConfig::new(title.as_bytes(), None);
if !x.is_empty() {
prop_assert_eq!(config.title, Some(x.to_owned()));
}
}
}
}