blob: 74cacd6caff7f9ee7b7981e808748f726cc9b4f7 [file] [log] [blame] [edit]
use r_efi::protocols::file;
use crate::ffi::OsString;
use crate::fmt;
use crate::fs::TryLockError;
use crate::hash::Hash;
use crate::io::{self, BorrowedCursor, IoSlice, IoSliceMut, SeekFrom};
use crate::path::{Path, PathBuf};
pub use crate::sys::fs::common::Dir;
use crate::sys::pal::{helpers, unsupported};
use crate::sys::time::SystemTime;
const FILE_PERMISSIONS_MASK: u64 = r_efi::protocols::file::READ_ONLY;
pub struct File(!);
#[derive(Clone)]
pub struct FileAttr {
attr: u64,
size: u64,
file_time: FileTimes,
created: Option<SystemTime>,
}
pub struct ReadDir(uefi_fs::File);
pub struct DirEntry {
attr: FileAttr,
file_name: OsString,
path: PathBuf,
}
#[derive(Clone, Debug)]
pub struct OpenOptions {
mode: u64,
append: bool,
truncate: bool,
create_new: bool,
}
#[derive(Copy, Clone, Debug, Default)]
pub struct FileTimes {
accessed: Option<SystemTime>,
modified: Option<SystemTime>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
// Bool indicates if file is readonly
pub struct FilePermissions(bool);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
// Bool indicates if directory
pub struct FileType(bool);
#[derive(Debug)]
pub struct DirBuilder;
impl FileAttr {
pub fn size(&self) -> u64 {
self.size
}
pub fn perm(&self) -> FilePermissions {
FilePermissions::from_attr(self.attr)
}
pub fn file_type(&self) -> FileType {
FileType::from_attr(self.attr)
}
pub fn modified(&self) -> io::Result<SystemTime> {
self.file_time
.modified
.ok_or(io::const_error!(io::ErrorKind::InvalidData, "modification time is not valid"))
}
pub fn accessed(&self) -> io::Result<SystemTime> {
self.file_time
.accessed
.ok_or(io::const_error!(io::ErrorKind::InvalidData, "last access time is not valid"))
}
pub fn created(&self) -> io::Result<SystemTime> {
self.created
.ok_or(io::const_error!(io::ErrorKind::InvalidData, "creation time is not valid"))
}
fn from_uefi(info: helpers::UefiBox<file::Info>) -> Self {
unsafe {
Self {
attr: (*info.as_ptr()).attribute,
size: (*info.as_ptr()).file_size,
file_time: FileTimes {
modified: uefi_fs::uefi_to_systemtime((*info.as_ptr()).modification_time),
accessed: uefi_fs::uefi_to_systemtime((*info.as_ptr()).last_access_time),
},
created: uefi_fs::uefi_to_systemtime((*info.as_ptr()).create_time),
}
}
}
}
impl FilePermissions {
pub fn readonly(&self) -> bool {
self.0
}
pub fn set_readonly(&mut self, readonly: bool) {
self.0 = readonly
}
const fn from_attr(attr: u64) -> Self {
Self(attr & r_efi::protocols::file::READ_ONLY != 0)
}
const fn to_attr(&self) -> u64 {
if self.0 { r_efi::protocols::file::READ_ONLY } else { 0 }
}
}
impl FileTimes {
pub fn set_accessed(&mut self, t: SystemTime) {
self.accessed = Some(t);
}
pub fn set_modified(&mut self, t: SystemTime) {
self.modified = Some(t);
}
}
impl FileType {
pub fn is_dir(&self) -> bool {
self.0
}
pub fn is_file(&self) -> bool {
!self.is_dir()
}
// Symlinks are not supported in UEFI
pub fn is_symlink(&self) -> bool {
false
}
const fn from_attr(attr: u64) -> Self {
Self(attr & r_efi::protocols::file::DIRECTORY != 0)
}
}
impl fmt::Debug for ReadDir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut b = f.debug_struct("ReadDir");
b.field("path", &self.0.path());
b.finish()
}
}
impl Iterator for ReadDir {
type Item = io::Result<DirEntry>;
fn next(&mut self) -> Option<io::Result<DirEntry>> {
match self.0.read_dir_entry() {
Ok(None) => None,
Ok(Some(x)) => {
let temp = DirEntry::from_uefi(x, self.0.path());
// Ignore "." and "..". This is how ReadDir behaves in Unix.
if temp.file_name == "." || temp.file_name == ".." {
self.next()
} else {
Some(Ok(temp))
}
}
Err(e) => Some(Err(e)),
}
}
}
impl DirEntry {
pub fn path(&self) -> PathBuf {
self.path.clone()
}
pub fn file_name(&self) -> OsString {
self.file_name.clone()
}
pub fn metadata(&self) -> io::Result<FileAttr> {
Ok(self.attr.clone())
}
pub fn file_type(&self) -> io::Result<FileType> {
Ok(self.attr.file_type())
}
fn from_uefi(info: helpers::UefiBox<file::Info>, parent: &Path) -> Self {
let file_name = uefi_fs::file_name_from_uefi(&info);
let path = parent.join(&file_name);
Self { file_name, path, attr: FileAttr::from_uefi(info) }
}
}
impl OpenOptions {
pub fn new() -> OpenOptions {
OpenOptions { mode: 0, append: false, create_new: false, truncate: false }
}
pub fn read(&mut self, read: bool) {
if read {
self.mode |= file::MODE_READ;
} else {
self.mode &= !file::MODE_READ;
}
}
pub fn write(&mut self, write: bool) {
if write {
// Valid Combinations: Read, Read/Write, Read/Write/Create
self.read(true);
self.mode |= file::MODE_WRITE;
} else {
self.mode &= !file::MODE_WRITE;
}
}
pub fn append(&mut self, append: bool) {
// Docs state that `.write(true).append(true)` has the same effect as `.append(true)`
if append {
self.write(true);
}
self.append = append;
}
pub fn truncate(&mut self, truncate: bool) {
self.truncate = truncate;
}
pub fn create(&mut self, create: bool) {
if create {
self.mode |= file::MODE_CREATE;
} else {
self.mode &= !file::MODE_CREATE;
}
}
pub fn create_new(&mut self, create_new: bool) {
self.create_new = create_new;
}
#[expect(dead_code)]
const fn is_mode_valid(&self) -> bool {
// Valid Combinations: Read, Read/Write, Read/Write/Create
self.mode == file::MODE_READ
|| self.mode == (file::MODE_READ | file::MODE_WRITE)
|| self.mode == (file::MODE_READ | file::MODE_WRITE | file::MODE_CREATE)
}
}
impl File {
pub fn open(_path: &Path, _opts: &OpenOptions) -> io::Result<File> {
unsupported()
}
pub fn file_attr(&self) -> io::Result<FileAttr> {
self.0
}
pub fn fsync(&self) -> io::Result<()> {
self.0
}
pub fn datasync(&self) -> io::Result<()> {
self.0
}
pub fn lock(&self) -> io::Result<()> {
self.0
}
pub fn lock_shared(&self) -> io::Result<()> {
self.0
}
pub fn try_lock(&self) -> Result<(), TryLockError> {
self.0
}
pub fn try_lock_shared(&self) -> Result<(), TryLockError> {
self.0
}
pub fn unlock(&self) -> io::Result<()> {
self.0
}
pub fn truncate(&self, _size: u64) -> io::Result<()> {
self.0
}
pub fn read(&self, _buf: &mut [u8]) -> io::Result<usize> {
self.0
}
pub fn read_vectored(&self, _bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
self.0
}
pub fn is_read_vectored(&self) -> bool {
self.0
}
pub fn read_buf(&self, _cursor: BorrowedCursor<'_>) -> io::Result<()> {
self.0
}
pub fn write(&self, _buf: &[u8]) -> io::Result<usize> {
self.0
}
pub fn write_vectored(&self, _bufs: &[IoSlice<'_>]) -> io::Result<usize> {
self.0
}
pub fn is_write_vectored(&self) -> bool {
self.0
}
pub fn flush(&self) -> io::Result<()> {
self.0
}
pub fn seek(&self, _pos: SeekFrom) -> io::Result<u64> {
self.0
}
pub fn size(&self) -> Option<io::Result<u64>> {
self.0
}
pub fn tell(&self) -> io::Result<u64> {
self.0
}
pub fn duplicate(&self) -> io::Result<File> {
self.0
}
pub fn set_permissions(&self, _perm: FilePermissions) -> io::Result<()> {
self.0
}
pub fn set_times(&self, _times: FileTimes) -> io::Result<()> {
self.0
}
}
impl DirBuilder {
pub fn new() -> DirBuilder {
DirBuilder
}
pub fn mkdir(&self, p: &Path) -> io::Result<()> {
uefi_fs::mkdir(p)
}
}
impl fmt::Debug for File {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0
}
}
pub fn readdir(p: &Path) -> io::Result<ReadDir> {
let path = crate::path::absolute(p)?;
let f = uefi_fs::File::from_path(&path, file::MODE_READ, 0)?;
let file_info = f.file_info()?;
let file_attr = FileAttr::from_uefi(file_info);
if file_attr.file_type().is_dir() {
Ok(ReadDir(f))
} else {
Err(io::const_error!(io::ErrorKind::NotADirectory, "expected a directory but got a file"))
}
}
pub fn unlink(p: &Path) -> io::Result<()> {
let f = uefi_fs::File::from_path(p, file::MODE_READ | file::MODE_WRITE, 0)?;
let file_info = f.file_info()?;
let file_attr = FileAttr::from_uefi(file_info);
if file_attr.file_type().is_file() {
f.delete()
} else {
Err(io::const_error!(io::ErrorKind::IsADirectory, "expected a file but got a directory"))
}
}
/// The implementation mirrors `mv` implementation in UEFI shell:
/// https://github.com/tianocore/edk2/blob/66346d5edeac2a00d3cf2f2f3b5f66d423c07b3e/ShellPkg/Library/UefiShellLevel2CommandsLib/Mv.c#L455
///
/// In a nutshell we do the following:
/// 1. Convert both old and new paths to absolute paths.
/// 2. Check that both lie in the same disk.
/// 3. Construct the target path relative to the current disk root.
/// 4. Set this target path as the file_name in the file_info structure.
pub fn rename(old: &Path, new: &Path) -> io::Result<()> {
let old_absolute = crate::path::absolute(old)?;
let new_absolute = crate::path::absolute(new)?;
let mut old_components = old_absolute.components();
let mut new_components = new_absolute.components();
let Some(old_disk) = old_components.next() else {
return Err(io::const_error!(io::ErrorKind::InvalidInput, "Old path is not valid"));
};
let Some(new_disk) = new_components.next() else {
return Err(io::const_error!(io::ErrorKind::InvalidInput, "New path is not valid"));
};
// Ensure that paths are on the same device.
if old_disk != new_disk {
return Err(io::const_error!(io::ErrorKind::CrossesDevices, "Cannot rename across device"));
}
// Construct an path relative the current disk root.
let new_relative =
[crate::path::Component::RootDir].into_iter().chain(new_components).collect::<PathBuf>();
let f = uefi_fs::File::from_path(old, file::MODE_READ | file::MODE_WRITE, 0)?;
let file_info = f.file_info()?;
let new_info = file_info.with_file_name(new_relative.as_os_str())?;
f.set_file_info(new_info)
}
pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> {
let f = uefi_fs::File::from_path(p, file::MODE_READ | file::MODE_WRITE, 0)?;
let mut file_info = f.file_info()?;
unsafe {
(*file_info.as_mut_ptr()).attribute =
((*file_info.as_ptr()).attribute & !FILE_PERMISSIONS_MASK) | perm.to_attr()
};
f.set_file_info(file_info)
}
pub fn set_times(p: &Path, times: FileTimes) -> io::Result<()> {
// UEFI does not support symlinks
set_times_nofollow(p, times)
}
pub fn set_times_nofollow(p: &Path, times: FileTimes) -> io::Result<()> {
let f = uefi_fs::File::from_path(p, file::MODE_READ | file::MODE_WRITE, 0)?;
let mut file_info = f.file_info()?;
if let Some(x) = times.accessed {
unsafe {
(*file_info.as_mut_ptr()).last_access_time = uefi_fs::systemtime_to_uefi(x);
}
}
if let Some(x) = times.modified {
unsafe {
(*file_info.as_mut_ptr()).modification_time = uefi_fs::systemtime_to_uefi(x);
}
}
f.set_file_info(file_info)
}
pub fn rmdir(p: &Path) -> io::Result<()> {
let f = uefi_fs::File::from_path(p, file::MODE_READ | file::MODE_WRITE, 0)?;
let file_info = f.file_info()?;
let file_attr = FileAttr::from_uefi(file_info);
if file_attr.file_type().is_dir() {
f.delete()
} else {
Err(io::const_error!(io::ErrorKind::NotADirectory, "expected a directory but got a file"))
}
}
pub fn remove_dir_all(_path: &Path) -> io::Result<()> {
unsupported()
}
pub fn exists(path: &Path) -> io::Result<bool> {
let f = uefi_fs::File::from_path(path, r_efi::protocols::file::MODE_READ, 0);
match f {
Ok(_) => Ok(true),
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e),
}
}
pub fn readlink(_p: &Path) -> io::Result<PathBuf> {
unsupported()
}
pub fn symlink(_original: &Path, _link: &Path) -> io::Result<()> {
unsupported()
}
pub fn link(_src: &Path, _dst: &Path) -> io::Result<()> {
unsupported()
}
pub fn stat(p: &Path) -> io::Result<FileAttr> {
let f = uefi_fs::File::from_path(p, r_efi::protocols::file::MODE_READ, 0)?;
let inf = f.file_info()?;
Ok(FileAttr::from_uefi(inf))
}
pub fn lstat(p: &Path) -> io::Result<FileAttr> {
stat(p)
}
pub fn canonicalize(p: &Path) -> io::Result<PathBuf> {
crate::path::absolute(p)
}
pub fn copy(_from: &Path, _to: &Path) -> io::Result<u64> {
unsupported()
}
mod uefi_fs {
use r_efi::protocols::{device_path, file, simple_file_system};
use crate::boxed::Box;
use crate::ffi::OsString;
use crate::io;
use crate::os::uefi::ffi::OsStringExt;
use crate::path::Path;
use crate::ptr::NonNull;
use crate::sys::pal::helpers::{self, UefiBox};
use crate::sys::time::{self, SystemTime};
pub(crate) struct File {
protocol: NonNull<file::Protocol>,
path: crate::path::PathBuf,
}
impl File {
pub(crate) fn from_path(path: &Path, open_mode: u64, attr: u64) -> io::Result<Self> {
let absolute = crate::path::absolute(path)?;
let p = helpers::OwnedDevicePath::from_text(absolute.as_os_str())?;
let (vol, mut path_remaining) = Self::open_volume_from_device_path(p.borrow())?;
let protocol = Self::open(vol, &mut path_remaining, open_mode, attr)?;
Ok(Self { protocol, path: absolute })
}
/// Open Filesystem volume given a devicepath to the volume, or a file/directory in the
/// volume. The path provided should be absolute UEFI device path, without any UEFI shell
/// mappings.
///
/// Returns
/// 1. The volume as a UEFI File
/// 2. Path relative to the volume.
///
/// For example, given "PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Slave,0x0)/\abc\run.efi",
/// this will open the volume "PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Slave,0x0)"
/// and return the remaining file path "\abc\run.efi".
fn open_volume_from_device_path(
path: helpers::BorrowedDevicePath<'_>,
) -> io::Result<(NonNull<file::Protocol>, Box<[u16]>)> {
let handles = match helpers::locate_handles(simple_file_system::PROTOCOL_GUID) {
Ok(x) => x,
Err(e) => return Err(e),
};
for handle in handles {
let volume_device_path: NonNull<device_path::Protocol> =
match helpers::open_protocol(handle, device_path::PROTOCOL_GUID) {
Ok(x) => x,
Err(_) => continue,
};
let volume_device_path = helpers::BorrowedDevicePath::new(volume_device_path);
if let Some(left_path) = path_best_match(&volume_device_path, &path) {
return Ok((Self::open_volume(handle)?, left_path));
}
}
Err(io::const_error!(io::ErrorKind::NotFound, "Volume Not Found"))
}
// Open volume on device_handle using SIMPLE_FILE_SYSTEM_PROTOCOL
fn open_volume(
device_handle: NonNull<crate::ffi::c_void>,
) -> io::Result<NonNull<file::Protocol>> {
let simple_file_system_protocol = helpers::open_protocol::<simple_file_system::Protocol>(
device_handle,
simple_file_system::PROTOCOL_GUID,
)?;
let mut file_protocol = crate::ptr::null_mut();
let r = unsafe {
((*simple_file_system_protocol.as_ptr()).open_volume)(
simple_file_system_protocol.as_ptr(),
&mut file_protocol,
)
};
if r.is_error() {
return Err(io::Error::from_raw_os_error(r.as_usize()));
}
// Since no error was returned, file protocol should be non-NULL.
let p = NonNull::new(file_protocol).unwrap();
Ok(p)
}
fn open(
protocol: NonNull<file::Protocol>,
path: &mut [u16],
open_mode: u64,
attr: u64,
) -> io::Result<NonNull<file::Protocol>> {
let file_ptr = protocol.as_ptr();
let mut file_opened = crate::ptr::null_mut();
let r = unsafe {
((*file_ptr).open)(file_ptr, &mut file_opened, path.as_mut_ptr(), open_mode, attr)
};
if r.is_error() {
return Err(io::Error::from_raw_os_error(r.as_usize()));
}
// Since no error was returned, file protocol should be non-NULL.
let p = NonNull::new(file_opened).unwrap();
Ok(p)
}
pub(crate) fn read_dir_entry(&self) -> io::Result<Option<UefiBox<file::Info>>> {
let file_ptr = self.protocol.as_ptr();
let mut buf_size = 0;
let r = unsafe { ((*file_ptr).read)(file_ptr, &mut buf_size, crate::ptr::null_mut()) };
if buf_size == 0 {
return Ok(None);
}
assert!(r.is_error());
if r != r_efi::efi::Status::BUFFER_TOO_SMALL {
return Err(io::Error::from_raw_os_error(r.as_usize()));
}
let mut info: UefiBox<file::Info> = UefiBox::new(buf_size)?;
let r =
unsafe { ((*file_ptr).read)(file_ptr, &mut buf_size, info.as_mut_ptr().cast()) };
if r.is_error() {
Err(io::Error::from_raw_os_error(r.as_usize()))
} else {
Ok(Some(info))
}
}
pub(crate) fn file_info(&self) -> io::Result<UefiBox<file::Info>> {
let file_ptr = self.protocol.as_ptr();
let mut info_id = file::INFO_ID;
let mut buf_size = 0;
let r = unsafe {
((*file_ptr).get_info)(
file_ptr,
&mut info_id,
&mut buf_size,
crate::ptr::null_mut(),
)
};
assert!(r.is_error());
if r != r_efi::efi::Status::BUFFER_TOO_SMALL {
return Err(io::Error::from_raw_os_error(r.as_usize()));
}
let mut info: UefiBox<file::Info> = UefiBox::new(buf_size)?;
let r = unsafe {
((*file_ptr).get_info)(
file_ptr,
&mut info_id,
&mut buf_size,
info.as_mut_ptr().cast(),
)
};
if r.is_error() { Err(io::Error::from_raw_os_error(r.as_usize())) } else { Ok(info) }
}
pub(crate) fn set_file_info(&self, mut info: UefiBox<file::Info>) -> io::Result<()> {
let file_ptr = self.protocol.as_ptr();
let mut info_id = file::INFO_ID;
let r = unsafe {
((*file_ptr).set_info)(file_ptr, &mut info_id, info.len(), info.as_mut_ptr().cast())
};
if r.is_error() { Err(io::Error::from_raw_os_error(r.as_usize())) } else { Ok(()) }
}
pub(crate) fn delete(self) -> io::Result<()> {
let file_ptr = self.protocol.as_ptr();
let r = unsafe { ((*file_ptr).delete)(file_ptr) };
// Spec states that even in case of failure, the file handle will be closed.
crate::mem::forget(self);
if r.is_error() { Err(io::Error::from_raw_os_error(r.as_usize())) } else { Ok(()) }
}
pub(crate) fn path(&self) -> &Path {
&self.path
}
}
impl Drop for File {
fn drop(&mut self) {
let file_ptr = self.protocol.as_ptr();
let _ = unsafe { ((*file_ptr).close)(file_ptr) };
}
}
/// A helper to check that target path is a descendent of source. It is expected to be used with
/// absolute UEFI device paths without any UEFI shell mappings.
///
/// Returns the path relative to source
///
/// For example, given "PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Slave,0x0)/" and
/// "PciRoot(0x0)/Pci(0x1,0x1)/Ata(Secondary,Slave,0x0)/\abc\run.efi", this will return
/// "\abc\run.efi"
fn path_best_match(
source: &helpers::BorrowedDevicePath<'_>,
target: &helpers::BorrowedDevicePath<'_>,
) -> Option<Box<[u16]>> {
let mut source_iter = source.iter().take_while(|x| !x.is_end_instance());
let mut target_iter = target.iter().take_while(|x| !x.is_end_instance());
loop {
match (source_iter.next(), target_iter.next()) {
(Some(x), Some(y)) if x == y => continue,
(None, Some(y)) => {
let p = y.to_path().to_text().ok()?;
return helpers::os_string_to_raw(&p);
}
_ => return None,
}
}
}
/// An implementation of mkdir to allow creating new directory without having to open the
/// volume twice (once for checking and once for creating)
pub(crate) fn mkdir(path: &Path) -> io::Result<()> {
let absolute = crate::path::absolute(path)?;
let p = helpers::OwnedDevicePath::from_text(absolute.as_os_str())?;
let (vol, mut path_remaining) = File::open_volume_from_device_path(p.borrow())?;
// Check if file exists
match File::open(vol, &mut path_remaining, file::MODE_READ, 0) {
Ok(_) => {
return Err(io::Error::new(io::ErrorKind::AlreadyExists, "Path already exists"));
}
Err(e) if e.kind() == io::ErrorKind::NotFound => {}
Err(e) => return Err(e),
}
let _ = File::open(
vol,
&mut path_remaining,
file::MODE_READ | file::MODE_WRITE | file::MODE_CREATE,
file::DIRECTORY,
)?;
Ok(())
}
/// EDK2 FAT driver uses EFI_UNSPECIFIED_TIMEZONE to represent localtime. So for proper
/// conversion to SystemTime, we use the current time to get the timezone in such cases.
pub(crate) fn uefi_to_systemtime(mut time: r_efi::efi::Time) -> Option<SystemTime> {
time.timezone = if time.timezone == r_efi::efi::UNSPECIFIED_TIMEZONE {
time::system_time_internal::now().timezone
} else {
time.timezone
};
SystemTime::from_uefi(time)
}
/// Convert to UEFI Time with the current timezone.
pub(crate) fn systemtime_to_uefi(time: SystemTime) -> r_efi::efi::Time {
let now = time::system_time_internal::now();
time.to_uefi_loose(now.timezone, now.daylight)
}
pub(crate) fn file_name_from_uefi(info: &UefiBox<file::Info>) -> OsString {
let fname = info.file_name();
OsString::from_wide(&fname[..fname.len() - 1])
}
}