use alloc::{borrow::ToOwned, format, string::String, vec::Vec};
use log::warn;
use object::{Architecture, Object, ObjectSection, Section};
use thiserror::Error;
use uefi::{CStr16, Handle, Status, cstr16, proto::media::file::FileInfo};
use crate::{
BootResult,
config::{
Config,
builder::ConfigBuilder,
parsers::{ConfigParser, Parsers},
},
system::{
fs::{FsError, UefiFileSystem},
helper::get_path_cstr,
},
};
const UKI_PREFIX: &CStr16 = cstr16!("\\EFI\\Linux");
const UKI_SUFFIX: &str = ".efi";
#[derive(Error, Debug)]
pub enum UkiError {
#[error("Error while parsing PE binary: {0}")]
Object(#[from] object::Error),
}
#[derive(Default)]
struct Osrel {
name: Option<String>,
id: Option<String>,
image_id: Option<String>,
image_version: Option<String>,
pretty_name: Option<String>,
version: Option<String>,
version_id: Option<String>,
build_id: Option<String>,
}
impl Osrel {
fn new(content: Option<Section<'_, '_>>) -> Result<Self, UkiError> {
let mut osrel = Self::default();
if let Some(content) = content {
let content_bytes = content.data()?;
let content_str = String::from_utf8_lossy(content_bytes).replace('"', "");
for line in content_str.lines() {
let line = line.trim();
if let Some((key, value)) = line.split_once('=') {
let value = value.trim().to_owned();
match key {
"NAME" => osrel.name = Some(value),
"ID" => osrel.id = Some(value),
"IMAGE_ID" => osrel.image_id = Some(value),
"IMAGE_VERSION" => osrel.image_version = Some(value),
"PRETTY_NAME" => osrel.pretty_name = Some(value),
"VERSION" => osrel.version = Some(value),
"VERSION_ID" => osrel.version_id = Some(value),
"BUILD_ID" => osrel.build_id = Some(value),
_ => (),
}
}
}
}
Ok(osrel)
}
}
pub struct UkiConfig {
title: String,
sort_key: String,
version: Option<String>,
architecture: Option<String>,
}
impl UkiConfig {
pub fn new(content: &[u8]) -> Result<Self, UkiError> {
let pe = object::File::parse(content)?;
let section = pe.section_by_name(".osrel");
let osrel = match Osrel::new(section) {
Ok(osrel) => osrel,
Err(e) => {
warn!("{e}"); Osrel::default()
}
};
let architecture = match pe.architecture() {
Architecture::X86_64 => Some("x64"),
Architecture::I386 => Some("x86"),
Architecture::Aarch64 => Some("aa64"),
Architecture::Arm => Some("arm"),
_ => None,
}
.map(ToOwned::to_owned);
Ok(Self {
title: osrel
.pretty_name
.as_ref()
.or(osrel.image_id.as_ref())
.or(osrel.name.as_ref())
.or(osrel.id.as_ref())
.map_or("Linux", |v| v)
.to_owned(),
sort_key: osrel
.image_id
.as_ref()
.or(osrel.id.as_ref())
.map_or("linux", |v| v)
.to_owned(),
version: osrel
.image_version
.or(osrel.version)
.or(osrel.version_id)
.or(osrel.build_id),
architecture,
})
}
}
impl ConfigParser for UkiConfig {
fn parse_configs(fs: &mut UefiFileSystem, handle: Handle, configs: &mut Vec<Config>) {
let dir = fs.read_filtered_dir(UKI_PREFIX, UKI_SUFFIX);
for file in dir {
match get_uki_config(&file, fs, handle) {
Ok(Some(config)) => configs.push(config),
Err(e) => warn!("{e}"),
_ => (),
}
}
}
}
fn get_uki_config(
file: &FileInfo,
fs: &mut UefiFileSystem,
handle: Handle,
) -> BootResult<Option<Config>> {
let content = match fs.read(&get_path_cstr(UKI_PREFIX, file.file_name())?) {
Ok(content) => content,
Err(FsError::OpenErr(Status::NOT_FOUND)) => return Ok(None),
Err(e) => return Err(e.into()),
};
let uki_config = UkiConfig::new(&content)?;
let efi_path = format!("{UKI_PREFIX}\\{}", file.file_name());
let config = ConfigBuilder::new(file.file_name(), UKI_SUFFIX)
.efi_path(efi_path)
.title(uki_config.title)
.sort_key(uki_config.sort_key)
.fs_handle(handle)
.origin(Parsers::Uki)
.assign_if_some(uki_config.version, ConfigBuilder::version)
.assign_if_some(uki_config.architecture, ConfigBuilder::architecture);
Ok(Some(config.build()))
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn doesnt_panic(x in any::<Vec<u8>>()) {
let _ = UkiConfig::new(&x);
}
}
}