use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec};
use log::error;
use thiserror::Error;
use uefi::{
CStr16, CString16, Char16, Guid, Handle, Status,
boot::{self, ScopedProtocol},
fs::{CHARACTER_DENY_LIST, COMMON_SKIP_DIRS, UefiDirectoryIter},
guid,
proto::media::{
file::{
Directory, File, FileAttribute, FileInfo, FileMode, FileSystemVolumeLabel, RegularFile,
},
fs::SimpleFileSystem,
partition::{GptPartitionType, PartitionInfo},
},
};
use crate::{
BootResult,
system::helper::{cstr_ends_with, str_to_cstr},
};
pub(crate) const ONE_GIGABYTE: usize = 1024 * 1024 * 1024;
const XBOOTLDR_PARTITION: uefi::Guid = guid!("bc13c2ff-59e6-4262-a352-b275fd6f7172");
const APFS_PARTITION: uefi::Guid = guid!("7c3457ef-0000-11aa-aa11-00306543ecac");
const HFS_PARTITION: uefi::Guid = guid!("48465300-0000-11aa-aa11-00306543ecac");
const HFS_BOOT_PARTITION: uefi::Guid = guid!("426f6f74-0000-11aa-aa11-00306543ecac");
const TARGET_PARTITION_GUIDS: &[uefi::Guid] = &[
GptPartitionType::EFI_SYSTEM_PARTITION.0,
XBOOTLDR_PARTITION,
APFS_PARTITION,
HFS_PARTITION,
HFS_BOOT_PARTITION,
];
#[derive(Error, Debug)]
pub enum FsError {
#[error("Buffer too small (require {0} bytes)")]
BufTooSmall(usize),
#[error("Could not write to file: returned status {status} ({bytes} bytes written)")]
WriteErr {
status: Status,
bytes: usize,
},
#[error("Failed to open file")]
OpenErr(Status),
#[error("Failed to read file")]
ReadErr(Status),
#[error("Failed to delete file")]
DeleteErr(Status),
#[error("Failed to flush file")]
FlushErr(Status),
#[error("Could not set position of a deleted file")]
SeekErr,
#[error("Could not get volume label of a partition")]
VolumeLabelErr,
}
pub struct UefiFileSystem(ScopedProtocol<SimpleFileSystem>);
impl UefiFileSystem {
#[must_use = "Has no effect if the result is unused"]
pub const fn new(fs: ScopedProtocol<SimpleFileSystem>) -> Self {
Self(fs)
}
pub fn from_handle(handle: Handle) -> BootResult<Self> {
let fs = boot::open_protocol_exclusive(handle)?;
Ok(Self(fs))
}
pub fn from_image_fs() -> BootResult<Self> {
let fs = boot::get_image_file_system(boot::image_handle())?;
Ok(Self(fs))
}
pub fn get_volume_label(&mut self) -> Result<CString16, FsError> {
let mut root = self
.0
.open_volume()
.map_err(|x| FsError::OpenErr(x.status()))?;
let info = root
.get_boxed_info::<FileSystemVolumeLabel>()
.map_err(|_| FsError::VolumeLabelErr)?;
Ok(info.volume_label().to_owned())
}
pub fn exists(&mut self, path: &CStr16) -> bool {
let Ok(mut root) = self.0.open_volume() else {
return false;
};
root.open(path, FileMode::Read, FileAttribute::empty())
.is_ok()
}
pub fn exists_str(&mut self, path: &str) -> BootResult<bool> {
Ok(self.exists(&str_to_cstr(path)?))
}
pub fn read_dir(&mut self, path: &CStr16) -> Result<UefiDirectoryIter, FsError> {
Ok(UefiDirectoryIter::new(self.get_directory(path)?))
}
pub fn read_filtered_dir(
&mut self,
path: &CStr16,
ext: &'static str,
) -> impl Iterator<Item = Box<FileInfo>> + use<> {
self.read_dir(path)
.into_iter()
.flatten()
.filter_map(Result::ok)
.filter(|x| !COMMON_SKIP_DIRS.contains(&x.file_name())) .filter(move |x| cstr_ends_with(x.file_name(), ext))
.filter(|x| x.file_size() > 0)
}
pub fn read_into(&mut self, path: &CStr16, buf: &mut [u8]) -> Result<usize, FsError> {
let mut file = self.get_regular_file(path)?;
let info = file
.get_boxed_info::<FileInfo>()
.map_err(|e| FsError::ReadErr(e.status()))?;
let size = usize::try_from(info.file_size()).unwrap_or(ONE_GIGABYTE);
let read = file.read(buf).map_err(|e| FsError::ReadErr(e.status()))?;
if read != size {
return Err(FsError::BufTooSmall(size));
}
Ok(read)
}
pub fn read(&mut self, path: &CStr16) -> Result<Vec<u8>, FsError> {
let mut file = self.get_regular_file(path)?;
let info = file
.get_boxed_info::<FileInfo>()
.map_err(|e| FsError::ReadErr(e.status()))?;
let size = usize::try_from(info.file_size()).unwrap_or(ONE_GIGABYTE);
let mut buf = vec![0; size];
file.read(&mut buf)
.map_err(|e| FsError::ReadErr(e.status()))?;
Ok(buf)
}
pub fn copy(&mut self, src: &CStr16, dst: &CStr16) -> Result<(), FsError> {
const CHUNK_SIZE: usize = 4 * 1024;
let _ = self.delete(dst);
let _ = self.create(dst);
let mut src = self.get_mut_file(src)?;
let mut dst = self.get_mut_file(dst)?;
let mut chunk = [0; CHUNK_SIZE];
let src_info = src
.get_boxed_info::<FileInfo>()
.map_err(|e| FsError::ReadErr(e.status()))?;
let mut remaining = src_info.file_size();
while remaining > 0 {
let bytes = src
.read(&mut chunk)
.map_err(|e| FsError::ReadErr(e.status()))?;
if bytes == 0 {
return Err(FsError::ReadErr(Status::ABORTED));
}
dst.write(&chunk[..bytes]).map_err(|e| FsError::WriteErr {
status: e.status(),
bytes: *e.data(),
})?;
remaining -= u64::try_from(bytes).unwrap_or(CHUNK_SIZE as u64);
}
dst.flush().map_err(|e| FsError::FlushErr(e.status()))?;
Ok(())
}
pub fn rename(&mut self, src: &CStr16, dst: &CStr16) -> Result<(), FsError> {
if src == dst {
return Ok(());
}
self.copy(src, dst)?;
self.delete(src)?;
Ok(())
}
pub fn create(&mut self, path: &CStr16) -> Result<(), FsError> {
let mut root = self
.0
.open_volume()
.map_err(|x| FsError::OpenErr(x.status()))?;
let f = root
.open(path, FileMode::CreateReadWrite, FileAttribute::empty())
.map_err(|e| FsError::OpenErr(e.status()))?;
if let Some(mut f) = f.into_regular_file() {
let buf = [0; 0];
let _ = f.write(&buf);
}
Ok(())
}
pub fn write(&mut self, path: &CStr16, buffer: &[u8]) -> Result<(), FsError> {
let mut file = self.get_mut_file(path)?;
file.write(buffer).map_err(|e| FsError::WriteErr {
status: e.status(),
bytes: *e.data(),
})?;
Ok(())
}
pub fn append(&mut self, path: &CStr16, buffer: &[u8]) -> BootResult<()> {
let mut file = self.get_mut_file(path)?;
file.set_position(RegularFile::END_OF_FILE)
.map_err(|_| FsError::SeekErr)?;
file.write(buffer).map_err(|e| FsError::WriteErr {
status: e.status(),
bytes: *e.data(),
})?;
Ok(())
}
pub fn delete(&mut self, path: &CStr16) -> Result<(), FsError> {
let file = self.get_mut_file(path)?;
file.delete().map_err(|e| FsError::DeleteErr(e.status()))?;
Ok(())
}
fn get_regular_file(&mut self, path: &CStr16) -> Result<RegularFile, FsError> {
let mut root = self
.0
.open_volume()
.map_err(|e| FsError::OpenErr(e.status()))?;
root.open(path, FileMode::Read, FileAttribute::empty())
.map_err(|e| FsError::OpenErr(e.status()))?
.into_regular_file()
.ok_or(FsError::OpenErr(Status::INVALID_PARAMETER))
}
fn get_mut_file(&mut self, path: &CStr16) -> Result<RegularFile, FsError> {
let mut root = self
.0
.open_volume()
.map_err(|e| FsError::OpenErr(e.status()))?;
root.open(path, FileMode::ReadWrite, FileAttribute::empty())
.map_err(|e| FsError::OpenErr(e.status()))?
.into_regular_file()
.ok_or(FsError::OpenErr(Status::INVALID_PARAMETER))
}
fn get_directory(&mut self, path: &CStr16) -> Result<Directory, FsError> {
let mut root = self
.0
.open_volume()
.map_err(|e| FsError::OpenErr(e.status()))?;
root.open(path, FileMode::ReadWrite, FileAttribute::empty())
.map_err(|e| FsError::OpenErr(e.status()))?
.into_directory()
.ok_or(FsError::OpenErr(Status::INVALID_PARAMETER))
}
}
#[must_use = "Has no effect if the result is unused"]
pub(crate) fn is_target_partition(handle: Handle) -> bool {
if let Ok(info) = boot::open_protocol_exclusive::<PartitionInfo>(handle) {
let Some(entry) = info.gpt_partition_entry() else {
return false;
};
let guid = entry.partition_type_guid.0;
if !TARGET_PARTITION_GUIDS.contains(&guid) {
return false;
}
}
true
}
pub(crate) fn get_partition_guid(handle: Handle) -> Option<Guid> {
if let Ok(info) = boot::open_protocol_exclusive::<PartitionInfo>(handle) {
let entry = info.gpt_partition_entry()?;
Some(entry.unique_partition_guid)
} else {
None
}
}
#[must_use = "Has no effect if the result is unused"]
pub(crate) fn check_path_valid(path: &str) -> bool {
path.chars()
.all(|x| Char16::try_from(x).is_ok_and(|x| !CHARACTER_DENY_LIST.contains(&x) || x == '\\'))
&& path != ".."
&& path != "."
}