use alloc::{
borrow::ToOwned,
string::{String, ToString},
vec::Vec,
};
use log::{error, warn};
use thiserror::Error;
use tinyvec::ArrayVec;
use uefi::{
boot::{self, SearchType},
proto::media::fs::SimpleFileSystem,
};
use crate::{
BootResult,
boot::action::BootAction,
config::{
parsers::{Parsers, parse_all_configs},
types::{Architecture, DevicetreePath, EfiPath, FsHandle, MachineId, SortKey},
},
system::{
fs::{UefiFileSystem, is_target_partition},
helper::get_arch,
},
};
pub mod builder;
pub mod editor;
pub mod parsers;
pub mod types;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Config \"{0}\" missing handle")]
ConfigMissingHandle(String),
#[error("Config \"{0}\" missing EFI")]
ConfigMissingEfi(String),
#[error("Config \"{0}\" has non-matching architecture")]
NonMatchingArch(String),
#[error("\"{0}\" does not exist at path \"{1}\"")]
NotExist(&'static str, String),
}
#[derive(Clone, Debug, Default)]
pub struct Config {
pub title: Option<String>,
pub version: Option<String>,
pub machine_id: Option<MachineId>,
pub sort_key: Option<SortKey>,
pub options: Option<String>,
pub devicetree_path: Option<DevicetreePath>,
pub architecture: Option<Architecture>,
pub efi_path: Option<EfiPath>,
pub action: BootAction,
pub bad: bool,
pub fs_handle: Option<FsHandle>,
pub origin: Option<Parsers>,
pub filename: String,
pub suffix: String,
}
impl Config {
#[must_use = "Has no effect if the result is unused"]
pub fn get_str_fields(&self) -> impl Iterator<Item = (&'static str, Option<&str>)> {
let vec: ArrayVec<[_; 8]> = ArrayVec::from([
("title", self.title.as_deref()),
("version", self.version.as_deref()),
("machine_id", self.machine_id.as_deref().map(|x| &**x)),
("sort_key", self.sort_key.as_deref().map(|x| &**x)),
("options", self.options.as_deref()),
("devicetree", self.devicetree_path.as_deref().map(|x| &**x)),
("architecture", self.architecture.as_deref().map(|x| &**x)),
("efi", self.efi_path.as_deref().map(|x| &**x)),
]);
vec.into_iter()
}
#[must_use = "Has no effect if the result is unused"]
pub fn is_good(&self) -> bool {
self.lint();
self.validate().map_err(|e| error!("{e}")).is_ok()
}
fn validate(&self) -> Result<(), ConfigError> {
self.validate_arch()?;
self.validate_efi()?;
self.validate_paths()?;
Ok(())
}
fn lint(&self) {
if self.title.as_ref().is_none_or(|x| x.trim().is_empty()) {
if self.filename.is_empty() {
warn!(
"Config found with no filename or title, assigning a title of its boot index"
);
} else {
warn!("Config {} does not have a title", self.filename);
}
}
}
#[must_use = "Has no effect if the result is unused"]
pub fn get_preferred_title(&self, option: Option<usize>) -> String {
let mut title = self.title.clone().unwrap_or_else(|| {
if self.filename.is_empty() {
option.map_or_else(|| "Unknown".to_owned(), |x| x.to_string())
} else {
self.filename.clone()
}
});
if self.bad {
title.push_str(" (Bad)");
}
title
}
fn validate_arch(&self) -> Result<(), ConfigError> {
if let Some(target) = &self.architecture
&& let Some(arch) = get_arch()
&& target != &arch
{
return Err(ConfigError::NonMatchingArch((**target).clone()));
}
Ok(())
}
fn validate_efi(&self) -> Result<(), ConfigError> {
if matches!(self.action, BootAction::BootEfi | BootAction::BootTftp)
&& self.efi_path.is_none()
{
return Err(ConfigError::ConfigMissingEfi(self.filename.clone()));
}
Ok(())
}
fn validate_paths(&self) -> Result<(), ConfigError> {
if let Some(handle) = self.fs_handle {
let mut fs = UefiFileSystem::from_handle(*handle)
.expect("FsHandle should always support SimpleFileSystem");
if let Some(efi_path) = &self.efi_path
&& !fs.exists_str(efi_path).unwrap_or(false)
{
return Err(ConfigError::NotExist("EFI", (**efi_path).clone()));
}
if let Some(devicetree_path) = &self.devicetree_path
&& !fs.exists_str(devicetree_path).unwrap_or(false)
{
return Err(ConfigError::NotExist(
"Devicetree",
(**devicetree_path).clone(),
));
}
} else if self.action == BootAction::BootEfi {
return Err(ConfigError::ConfigMissingHandle(self.filename.clone()));
}
Ok(())
}
}
pub(crate) fn scan_configs() -> BootResult<Vec<Config>> {
let mut configs = Vec::with_capacity(4); let handles = boot::locate_handle_buffer(SearchType::from_proto::<SimpleFileSystem>())?;
for &handle in handles.iter() {
if !is_target_partition(handle) {
continue;
}
let mut fs = UefiFileSystem::from_handle(handle)?;
parse_all_configs(&mut fs, handle, &mut configs);
}
configs.retain(Config::is_good);
configs.sort_unstable_by(|a, b| {
a.bad
.cmp(&b.bad) .then_with(|| b.sort_key.is_some().cmp(&a.sort_key.is_some())) .then_with(|| a.sort_key.cmp(&b.sort_key)) .then_with(|| a.machine_id.cmp(&b.machine_id)) .then_with(|| b.version.cmp(&a.version)) .then_with(|| {
b.filename
.strip_suffix(&b.suffix)
.cmp(&a.filename.strip_suffix(&a.suffix))
}) });
Ok(configs)
}
#[cfg(test)]
mod tests {
use crate::config::types::TypeError;
use super::*;
use alloc::borrow::ToOwned;
#[test]
fn test_non_efi_config() -> Result<(), TypeError> {
let machine_id = MachineId::new("1234567890abcdef1234567890abcdef")?;
let sort_key = SortKey::new("linux")?;
let efi_path = EfiPath::new("\\vmlinuz-linux")?;
let config = Config {
title: Some("Linux".to_owned()),
version: Some("6.10.0".to_owned()),
machine_id: Some(machine_id),
sort_key: Some(sort_key),
options: Some("root=PARTUUID=1234abcd-56ef-78gh-90ij-klmnopqrstuv ro".to_owned()),
efi_path: Some(efi_path),
filename: "linux.conf".to_owned(),
suffix: ".conf".to_owned(),
action: BootAction::BootTftp,
..Config::default()
};
assert!(config.is_good());
Ok(())
}
}