diff --git a/CHANGELOG.md b/CHANGELOG.md index c8601cf23..e41fab760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## uefi - [Unreleased] +### Added + +- There is a new `fs` module that provides a high-level API for file-system + access. The API is close to the `std::fs` module. + ### Changed - The `global_allocator` module has been renamed to `allocator`, and is now @@ -9,6 +14,8 @@ `global_allocator` feature now only controls whether `allocator::Allocator` is set as Rust's global allocator. - `Error::new` and `Error::from` now panic if the status is `SUCCESS`. +- `Image::get_image_file_system` now returns a `fs::FileSystem` instead of the + protocol. ## uefi-macros - [Unreleased] diff --git a/Cargo.lock b/Cargo.lock index 38305e1dd..59432ae1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,25 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.103", +] + [[package]] name = "either" version = "1.8.0" @@ -363,6 +382,15 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.37.4" @@ -383,6 +411,12 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + [[package]] name = "serde" version = "1.0.147" @@ -537,6 +571,7 @@ name = "uefi" version = "0.20.0" dependencies = [ "bitflags", + "derive_more", "log", "ptr_meta", "ucs2", diff --git a/uefi-test-runner/src/fs/mod.rs b/uefi-test-runner/src/fs/mod.rs new file mode 100644 index 000000000..8ff38ac64 --- /dev/null +++ b/uefi-test-runner/src/fs/mod.rs @@ -0,0 +1,57 @@ +//! Tests functionality from the `uefi::fs` module. See function [`test`]. + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use uefi::fs::{FileSystem, FileSystemError}; +use uefi::proto::media::fs::SimpleFileSystem; +use uefi::table::boot::ScopedProtocol; + +/// Tests functionality from the `uefi::fs` module. This test relies on a +/// working File System Protocol, which is tested at a dedicated place. +pub fn test(sfs: ScopedProtocol) -> Result<(), FileSystemError> { + let mut fs = FileSystem::new(sfs); + + fs.create_dir("test_file_system_abs")?; + + // slash is transparently transformed to backslash + fs.write("test_file_system_abs/foo", "hello")?; + // absolute or relative paths are supported; ./ is ignored + fs.copy("\\test_file_system_abs/foo", "\\test_file_system_abs/./bar")?; + let read = fs.read("\\test_file_system_abs\\bar")?; + let read = String::from_utf8(read).expect("Should be valid utf8"); + assert_eq!(read, "hello"); + + assert_eq!( + fs.try_exists("test_file_system_abs\\barfoo"), + Err(FileSystemError::OpenError( + "\\test_file_system_abs\\barfoo".to_string() + )) + ); + fs.rename("test_file_system_abs\\bar", "test_file_system_abs\\barfoo")?; + assert!(fs.try_exists("test_file_system_abs\\barfoo").is_ok()); + + let entries = fs + .read_dir("test_file_system_abs")? + .map(|e| { + e.expect("Should return boxed file info") + .file_name() + .to_string() + }) + .collect::>(); + assert_eq!(&[".", "..", "foo", "barfoo"], entries.as_slice()); + + fs.create_dir("/deeply_nested_test")?; + fs.create_dir("/deeply_nested_test/1")?; + fs.create_dir("/deeply_nested_test/1/2")?; + fs.create_dir("/deeply_nested_test/1/2/3")?; + fs.create_dir("/deeply_nested_test/1/2/3/4")?; + fs.create_dir_all("/deeply_nested_test/1/2/3/4/5/6/7")?; + fs.try_exists("/deeply_nested_test/1/2/3/4/5/6/7")?; + // TODO + // fs.remove_dir_all("/deeply_nested_test/1/2/3/4/5/6/7")?; + fs.remove_dir("/deeply_nested_test/1/2/3/4/5/6/7")?; + let exists = matches!(fs.try_exists("/deeply_nested_test/1/2/3/4/5/6/7"), Ok(_)); + assert!(!exists); + + Ok(()) +} diff --git a/uefi-test-runner/src/main.rs b/uefi-test-runner/src/main.rs index c6acd77a6..7bf594306 100644 --- a/uefi-test-runner/src/main.rs +++ b/uefi-test-runner/src/main.rs @@ -13,6 +13,7 @@ use uefi::Result; use uefi_services::{print, println}; mod boot; +mod fs; mod proto; mod runtime; diff --git a/uefi-test-runner/src/proto/media.rs b/uefi-test-runner/src/proto/media.rs index 0e763feca..0d2585742 100644 --- a/uefi-test-runner/src/proto/media.rs +++ b/uefi-test-runner/src/proto/media.rs @@ -433,8 +433,12 @@ pub fn test(bt: &BootServices) { test_partition_info(bt, handle); } - // Close the `SimpleFileSystem` protocol so that the raw disk tests work. - drop(sfs); + // Invoke the fs test after the basic low-level file system protocol + // tests succeeded. + + // This will also drop the `SimpleFileSystem` protocol so that the raw disk + // tests work. + crate::fs::test(sfs).unwrap(); test_raw_disk_io(handle, bt); test_raw_disk_io2(handle, bt); diff --git a/uefi/Cargo.toml b/uefi/Cargo.toml index 40e0124b4..9359a94f3 100644 --- a/uefi/Cargo.toml +++ b/uefi/Cargo.toml @@ -25,6 +25,7 @@ unstable = [] [dependencies] bitflags = "1.3.1" +derive_more = { version = "0.99.17", features = ["display"] } log = { version = "0.4.5", default-features = false } ptr_meta = { version = "0.2.0", default-features = false } ucs2 = "0.3.2" diff --git a/uefi/src/fs/dir_entry_iter.rs b/uefi/src/fs/dir_entry_iter.rs new file mode 100644 index 000000000..2ae618840 --- /dev/null +++ b/uefi/src/fs/dir_entry_iter.rs @@ -0,0 +1,31 @@ +//! Module for directory iteration. See [`UefiDirectoryIter`]. + +use super::*; +use alloc::boxed::Box; +use uefi::Result; + +/// Iterates over the entries of an UEFI directory. It returns boxed values of +/// type [`UefiFileInfo`]. +#[derive(Debug)] +pub struct UefiDirectoryIter(UefiDirectoryHandle); + +impl UefiDirectoryIter { + /// Constructor. + pub fn new(handle: UefiDirectoryHandle) -> Self { + Self(handle) + } +} + +impl Iterator for UefiDirectoryIter { + type Item = Result, ()>; + + fn next(&mut self) -> Option { + let e = self.0.read_entry_boxed(); + match e { + // no more entries + Ok(None) => None, + Ok(Some(e)) => Some(Ok(e)), + Err(e) => Some(Err(e)), + } + } +} diff --git a/uefi/src/fs/file_system.rs b/uefi/src/fs/file_system.rs new file mode 100644 index 000000000..8a3ffb32b --- /dev/null +++ b/uefi/src/fs/file_system.rs @@ -0,0 +1,303 @@ +//! Module for [`FileSystem`]. + +use super::*; +use alloc::boxed::Box; +use alloc::string::{FromUtf8Error, String, ToString}; +use alloc::vec::Vec; +use alloc::{format, vec}; +use core::fmt; +use core::fmt::{Debug, Formatter}; +use core::ops::Deref; +use derive_more::Display; +use log::info; +use uefi::proto::media::file::{FileAttribute, FileInfo, FileType}; +use uefi::table::boot::ScopedProtocol; + +/// All errors that can happen when working with the [`FileSystem`]. +#[derive(Debug, Clone, Display, PartialEq, Eq)] +pub enum FileSystemError { + /// Can't open the root directory of the underlying volume. + CantOpenVolume, + /// The path is invalid because of the underlying [`PathError`]. + IllegalPath(PathError), + /// The file or directory was not found in the underlying volume. + FileNotFound(String), + /// The path is existent but does not correspond to a directory when a + /// directory was expected. + NotADirectory(String), + /// The path is existent but does not correspond to a file when a file was + /// expected. + NotAFile(String), + /// Can't delete the file. + CantDeleteFile(String), + /// Can't delete the directory. + CantDeleteDirectory(String), + /// Error writing bytes. + WriteFailure, + /// Error flushing file. + FlushFailure, + /// Error reading file. + ReadFailure, + /// Can't parse file content as UTF-8. + Utf8Error(FromUtf8Error), + /// Could not open the given path. + OpenError(String), +} + +#[cfg(feature = "unstable")] +impl core::error::Error for FileSystemError {} + +impl From for FileSystemError { + fn from(err: PathError) -> Self { + Self::IllegalPath(err) + } +} + +/// Return type for public [`FileSystem`] operations. +pub type FileSystemResult = Result; + +/// High-level file-system abstraction for UEFI volumes with an API that is +/// close to `std::fs`. It acts as convenient accessor around the +/// [`SimpleFileSystemProtocol`]. +pub struct FileSystem<'a>(ScopedProtocol<'a, SimpleFileSystemProtocol>); + +impl<'a> FileSystem<'a> { + /// Constructor. + #[must_use] + pub fn new(proto: ScopedProtocol<'a, SimpleFileSystemProtocol>) -> Self { + Self(proto) + } + + /// Tests if the underlying file exists. If this returns `Ok`, the file + /// exists. + pub fn try_exists(&mut self, path: impl AsRef) -> FileSystemResult<()> { + self.metadata(path).map(|_| ()) + } + + /// Copies the contents of one file to another. Creates the destination file + /// if it doesn't exist and overwrites any content, if it exists. + pub fn copy( + &mut self, + src_path: impl AsRef, + dest_path: impl AsRef, + ) -> FileSystemResult<()> { + let read = self.read(src_path)?; + self.write(dest_path, read) + } + + /// Creates a new, empty directory at the provided path + pub fn create_dir(&mut self, path: impl AsRef) -> FileSystemResult<()> { + let path = path.as_ref(); + self.open(path, UefiFileMode::CreateReadWrite, true) + .map(|_| ()) + .map_err(|err| { + log::debug!("failed to fetch file info: {err:#?}"); + FileSystemError::OpenError(path.to_string()) + }) + } + + /// Recursively create a directory and all of its parent components if they + /// are missing. + pub fn create_dir_all(&mut self, path: impl AsRef) -> FileSystemResult<()> { + let path = path.as_ref(); + + let normalized_path = NormalizedPath::new("\\", path)?; + let normalized_path_string = normalized_path.to_string(); + let normalized_path_pathref = Path::new(&normalized_path_string); + + let iter = || normalized_path_pathref.components(SEPARATOR); + iter() + .scan(String::new(), |path_acc, component| { + if component != Component::RootDir { + *path_acc += SEPARATOR_STR; + *path_acc += format!("{component}").as_str(); + } + info!("path_acc: {path_acc}, component: {component}"); + Some((component, path_acc.clone())) + }) + .try_for_each(|(_component, full_path)| self.create_dir(full_path.as_str())) + } + + /// Given a path, query the file system to get information about a file, + /// directory, etc. Returns [`UefiFileInfo`]. + pub fn metadata(&mut self, path: impl AsRef) -> FileSystemResult> { + let path = path.as_ref(); + let file = self.open(path, UefiFileMode::Read, false)?; + log::debug!("{:#?}", &file.into_type().unwrap()); + let mut file = self.open(path, UefiFileMode::Read, false)?; + file.get_boxed_info().map_err(|err| { + log::debug!("failed to fetch file info: {err:#?}"); + FileSystemError::OpenError(path.to_string()) + }) + } + + /// Read the entire contents of a file into a bytes vector. + pub fn read(&mut self, path: impl AsRef) -> FileSystemResult> { + let path = path.as_ref(); + + let mut file = self + .open(path, UefiFileMode::Read, false)? + .into_regular_file() + .ok_or(FileSystemError::NotAFile(path.as_str().to_string()))?; + let info = file.get_boxed_info::().map_err(|e| { + log::error!("get info failed: {e:?}"); + FileSystemError::OpenError(path.as_str().to_string()) + })?; + + let mut vec = vec![0; info.file_size() as usize]; + let read_bytes = file.read(vec.as_mut_slice()).map_err(|e| { + log::error!("reading failed: {e:?}"); + FileSystemError::ReadFailure + })?; + + // we read the whole file at once! + if read_bytes != info.file_size() as usize { + log::error!("Did only read {}/{} bytes", info.file_size(), read_bytes); + } + + Ok(vec) + } + + /// Returns an iterator over the entries within a directory. + pub fn read_dir(&mut self, path: impl AsRef) -> FileSystemResult { + let path = path.as_ref(); + let dir = self + .open(path, UefiFileMode::Read, false)? + .into_directory() + .ok_or(FileSystemError::NotADirectory(path.as_str().to_string()))?; + Ok(UefiDirectoryIter::new(dir)) + } + + /// Read the entire contents of a file into a string. + pub fn read_to_string(&mut self, path: impl AsRef) -> FileSystemResult { + String::from_utf8(self.read(path)?).map_err(FileSystemError::Utf8Error) + } + + /// Removes an empty directory. + pub fn remove_dir(&mut self, path: impl AsRef) -> FileSystemResult<()> { + let path = path.as_ref(); + + let file = self + .open(path, UefiFileMode::ReadWrite, false)? + .into_type() + .unwrap(); + + match file { + FileType::Dir(dir) => dir.delete().map_err(|e| { + log::error!("error removing dir: {e:?}"); + FileSystemError::CantDeleteDirectory(path.as_str().to_string()) + }), + FileType::Regular(_) => Err(FileSystemError::NotADirectory(path.as_str().to_string())), + } + } + + /*/// Removes a directory at this path, after removing all its contents. Use + /// carefully! + pub fn remove_dir_all(&mut self, _path: impl AsRef) -> FileSystemResult<()> { + todo!() + }*/ + + /// Removes a file from the filesystem. + pub fn remove_file(&mut self, path: impl AsRef) -> FileSystemResult<()> { + let path = path.as_ref(); + + let file = self + .open(path, UefiFileMode::ReadWrite, false)? + .into_type() + .unwrap(); + + match file { + FileType::Regular(file) => file.delete().map_err(|e| { + log::error!("error removing file: {e:?}"); + FileSystemError::CantDeleteFile(path.as_str().to_string()) + }), + FileType::Dir(_) => Err(FileSystemError::NotAFile(path.as_str().to_string())), + } + } + + /// Rename a file or directory to a new name, replacing the original file if + /// it already exists. + pub fn rename( + &mut self, + src_path: impl AsRef, + dest_path: impl AsRef, + ) -> FileSystemResult<()> { + self.copy(&src_path, dest_path)?; + self.remove_file(src_path) + } + + /// Write a slice as the entire contents of a file. This function will + /// create a file if it does not exist, and will entirely replace its + /// contents if it does. + pub fn write( + &mut self, + path: impl AsRef, + content: impl AsRef<[u8]>, + ) -> FileSystemResult<()> { + let path = path.as_ref(); + + // since there is no .truncate() in UEFI, we delete the file first it it + // exists. + if self.try_exists(path).is_ok() { + self.remove_file(path)?; + } + + let mut handle = self + .open(path, UefiFileMode::CreateReadWrite, false)? + .into_regular_file() + .unwrap(); + + handle.write(content.as_ref()).map_err(|e| { + log::error!("only wrote {e:?} bytes"); + FileSystemError::WriteFailure + })?; + handle.flush().map_err(|e| { + log::error!("flush failure: {e:?}"); + FileSystemError::FlushFailure + })?; + Ok(()) + } + + /// Opens a fresh handle to the root directory of the volume. + fn open_root(&mut self) -> FileSystemResult { + self.0.open_volume().map_err(|e| { + log::error!("Can't open root volume: {e:?}"); + FileSystemError::CantOpenVolume + }) + } + + /// Wrapper around [`Self::open_root`] that opens the provided path as + /// absolute path. + /// + /// May create a file if [`UefiFileMode::CreateReadWrite`] is set. May + /// create a directory if [`UefiFileMode::CreateReadWrite`] and `is_dir` + /// is set. + fn open( + &mut self, + path: &Path, + mode: UefiFileMode, + is_dir: bool, + ) -> FileSystemResult { + let path = NormalizedPath::new("\\", path)?; + log::debug!("normalized path: {path}"); + + let attr = if mode == UefiFileMode::CreateReadWrite && is_dir { + FileAttribute::DIRECTORY + } else { + FileAttribute::empty() + }; + + self.open_root()?.open(&path, mode, attr).map_err(|x| { + log::trace!("Can't open file {path}: {x:?}"); + FileSystemError::OpenError(path.to_string()) + }) + } +} + +impl<'a> Debug for FileSystem<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("FileSystem(<>))") + .field(&(self.0.deref() as *const _)) + .finish() + } +} diff --git a/uefi/src/fs/mod.rs b/uefi/src/fs/mod.rs new file mode 100644 index 000000000..c22d4e95f --- /dev/null +++ b/uefi/src/fs/mod.rs @@ -0,0 +1,48 @@ +//! A high-level file system API for UEFI applications close to the `fs` module +//! from Rust's standard library. +//! +//! ## Difference to typical File System Abstractions +//! Users perform actions on dedicated volumes: For example, the boot volume, +//! such as a CD-rom, USB-stick, or any other storage device. +//! +//! ### UNIX +//! Unlike in the API of typical UNIX file system abstractions, there is no +//! virtual file system. +//! +//! ### Windows +//! Unlike in Windows, there is no way to access volumes by a dedicated name. +//! +//! ## Paths +//! All paths are absolute and follow the FAT-like file system conventions for +//! paths. Thus, there is no current working directory and path components +//! like `.` and `.."`are not supported. In other words, the current working +//! directory is always `/`, i.e., the root, of the opened volume. This may +//! change in the future but is currently sufficient. +//! +//! Symlinks or hard-links are not supported but only directories and regular +//! files with plain linear paths to them. +//! +//! ## API Hints +//! There are no `File` and `Path` abstractions similar to those from `std` that +//! are publicly exported. Instead, paths to files are provided as `&str`, and +//! will be validated and transformed internally to the correct type. +//! Furthermore, there are no `File` objects that are exposed to users. Instead, +//! it is intended to work with the file system as in `std::fs`. +//! +//! ### Synchronization +//! There is no automatic synchronization of the file system for concurrent +//! accesses. This is in the responsibility of the user. + +mod dir_entry_iter; +mod file_system; +mod normalized_path; +mod path; +mod uefi_types; + +pub use file_system::{FileSystem, FileSystemError, FileSystemResult}; +pub use normalized_path::{PathError, SEPARATOR, SEPARATOR_STR}; + +use dir_entry_iter::*; +use normalized_path::*; +use path::*; +use uefi_types::*; diff --git a/uefi/src/fs/normalized_path.rs b/uefi/src/fs/normalized_path.rs new file mode 100644 index 000000000..c6fa251cd --- /dev/null +++ b/uefi/src/fs/normalized_path.rs @@ -0,0 +1,239 @@ +//! Module for path normalization. See [`NormalizedPath`]. + +use super::*; +use alloc::format; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::ops::Deref; +use derive_more::Display; +use uefi::data_types::FromStrError; +use uefi::CString16; + +/// The default separator for paths. +pub const SEPARATOR: char = '\\'; + +/// Stringifyed version of [`SEPARATOR`]. +pub const SEPARATOR_STR: &str = "\\"; + +/// Errors that may happen during path normalization.. +#[derive(Debug, Clone, Eq, PartialEq, Display)] +pub enum PathError { + /// The specified present working directory is not absolute. + PwdNotAbsolute, + /// The path is empty. + Empty, + /// There are illegal characters in the path. + IllegalCharacters(CharactersError), +} + +#[cfg(feature = "unstable")] +impl core::error::Error for PathError {} + +#[derive(Debug, Clone, Eq, PartialEq, Display)] +pub enum CharactersError { + ProhibitedSymbols, + NonUCS2Compatible(FromStrError), +} + +#[cfg(feature = "unstable")] +impl core::error::Error for CharactersError {} + +/// **Internal API (so far).** +/// +/// Unlike a [`Path`], which is close to the implementation of the Rust +/// standard library, this abstraction is an absolute path that is valid in +/// FAT-like file systems (which are supported by UEFI and can be accessed via +/// the file system protocol). +/// +/// Hence, it is called normalized path. Another term might be canonicalized +/// path. +/// +/// For compatibility with the UEFI file-system protocol, this is a +/// [`CString16`]. The separator is `\`. For convenience, all occurrences of `/` +/// are transparently replaced by `\`. +/// +/// A normalized path is always absolute, i.e., starts at the root directory. +#[derive(Debug, Eq, PartialEq, Display)] +pub struct NormalizedPath(CString16); + +impl NormalizedPath { + /// Deny list of characters for path components. UEFI supports FAT-like file + /// systems. According to , + /// paths should not contain the following symbols. + pub const CHARACTER_DENY_LIST: [char; 10] = + ['\0', '"', '*', '/', ':', '<', '>', '?', '\\', '|']; + + /// Constructor. Combines the path with the present working directory (pwd) + /// if the `path` is relative. The resulting path is technically valid so + /// that it can be passed to the underlying file-system protocol. The + /// resulting path doesn't contain `.` or `..`. + /// + /// `pwd` is expected to be valid. + pub fn new(pwd: impl AsRef, path: impl AsRef) -> Result { + let pwd = pwd.as_ref(); + let path = path.as_ref(); + + let path = Self::normalize_separator(path); + let path = Path::new(path.as_str()); + + Self::check_pwd_absolute(pwd)?; + Self::check_prohibited_chars(path)?; + + let path = Self::combine_path_with_pwd(pwd, path); + + Self::build_normalized_path(path.as_str().as_ref()) + } + + /// Checks if the pwd is an absolute path. + fn check_pwd_absolute(pwd: &Path) -> Result<(), PathError> { + if !pwd.as_str().starts_with(SEPARATOR) { + return Err(PathError::PwdNotAbsolute); + } + Ok(()) + } + + /// Replaces all occurrences of `/` with [`SEPARATOR`]. + fn normalize_separator(path: &Path) -> String { + path.as_str().replace('/', SEPARATOR_STR) + } + + /// Checks that each component of type [`Component::Normal`] doesn't contain + /// any of the prohibited characters specified in + /// [`Self::CHARACTER_DENY_LIST`]. + fn check_prohibited_chars(path: &Path) -> Result<(), PathError> { + let prohibited_character_found = path + .components(SEPARATOR) + .filter_map(|c| match c { + Component::Normal(n) => Some(n), + _ => None, + }) + .flat_map(|c| c.chars()) + .any(|c| Self::CHARACTER_DENY_LIST.contains(&c)); + + (!prohibited_character_found) + .then_some(()) + .ok_or(PathError::IllegalCharacters( + CharactersError::ProhibitedSymbols, + )) + } + + /// Merges `pwd` and `path`, if `path` is not absolute. + fn combine_path_with_pwd(pwd: &Path, path: &Path) -> String { + let path_is_absolute = path.as_str().starts_with(SEPARATOR); + if path_is_absolute { + path.as_str().to_string() + } else { + // This concatenation is fine as pwd is an absolute path. + if pwd.as_str() == SEPARATOR_STR { + format!("{separator}{path}", separator = SEPARATOR) + } else { + format!("{pwd}{separator}{path}", separator = SEPARATOR) + } + } + } + + /// Consumes an absolute path and builds a `Self` from it. At this point, + /// the path is expected to have passed all sanity checks. The last step + /// is only relevant to resolve `.` and `..`. + fn build_normalized_path(path: &Path) -> Result { + let component_count = path.components(SEPARATOR).count(); + let mut normalized_components = Vec::with_capacity(component_count); + + for component in path.components(SEPARATOR) { + match component { + Component::RootDir => { + normalized_components.push(SEPARATOR_STR); + } + Component::CurDir => continue, + Component::ParentDir => { + normalized_components.remove(normalized_components.len() - 1); + } + Component::Normal(n) => { + let prev_has_sep = normalized_components + .last() + .map(|x| x.eq(&SEPARATOR_STR)) + .unwrap_or(false); + if !prev_has_sep { + normalized_components.push(SEPARATOR_STR); + } + normalized_components.push(n); + } + } + } + + let normalized_string: String = normalized_components.concat(); + CString16::try_from(normalized_string.as_str()) + .map(Self) + .map_err(|x| PathError::IllegalCharacters(CharactersError::NonUCS2Compatible(x))) + } +} + +impl Deref for NormalizedPath { + type Target = CString16; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn pwd_must_be_absolute() { + let path = NormalizedPath::new("", ""); + assert_eq!(Err(PathError::PwdNotAbsolute), path); + + let path = NormalizedPath::new(".", ""); + assert_eq!(Err(PathError::PwdNotAbsolute), path); + + let path = NormalizedPath::new("/", ""); + assert_eq!(Err(PathError::PwdNotAbsolute), path); + } + + #[test] + fn normalized_path() { + let path = NormalizedPath::new("\\foo", "/bar/barfoo").map(|x| x.0); + assert_eq!(path, Ok(CString16::try_from("\\bar\\barfoo").unwrap())); + + let path = NormalizedPath::new("\\foo", "bar/barfoo").map(|x| x.0); + assert_eq!(path, Ok(CString16::try_from("\\foo\\bar\\barfoo").unwrap())); + + let path = NormalizedPath::new("\\foo", "./bar/barfoo").map(|x| x.0); + assert_eq!(path, Ok(CString16::try_from("\\foo\\bar\\barfoo").unwrap())); + + let path = NormalizedPath::new("\\foo", "./bar/.././././barfoo").map(|x| x.0); + assert_eq!(path, Ok(CString16::try_from("\\foo\\barfoo").unwrap())); + + let path = NormalizedPath::new("\\", "foo").map(|x| x.0); + assert_eq!(path, Ok(CString16::try_from("\\foo").unwrap())); + } + + #[test] + fn check_components_for_allowed_chars() { + fn check_fail(path: impl AsRef) { + assert_eq!( + NormalizedPath::check_prohibited_chars(path.as_ref()), + Err(PathError::IllegalCharacters( + CharactersError::ProhibitedSymbols + )) + ); + } + + assert_eq!( + NormalizedPath::check_prohibited_chars("\\foo".as_ref()), + Ok(()) + ); + + check_fail("\\foo\0"); + check_fail("\\foo:"); + check_fail("\\foo*"); + check_fail("\\foo/"); + check_fail("\\foo<"); + check_fail("\\foo>"); + check_fail("\\foo?"); + check_fail("\\foo|"); + check_fail("\\foo\""); + } +} diff --git a/uefi/src/fs/path.rs b/uefi/src/fs/path.rs new file mode 100644 index 000000000..36f850241 --- /dev/null +++ b/uefi/src/fs/path.rs @@ -0,0 +1,154 @@ +//! Module for handling file-system paths in [`super::FileSystem`]. +//! See [`Path`]. + +use alloc::string::String; +use core::fmt::{Display, Formatter}; + +/// Path abstraction similar to `std::path::Path` but adapted to the platform- +/// agnostic `no_std` use case. It is up to the file-system implementation to +/// verify if a path is valid. +#[repr(transparent)] +#[derive(Debug)] +pub struct Path(str); + +impl Path { + /// Directly wraps a string slice as a `Path` slice. + pub fn new + ?Sized>(str: &S) -> &Self { + unsafe { &*(str.as_ref() as *const str as *const Path) } + } + + /// Returns the underlying `str`. + pub fn as_str(&self) -> &str { + self.as_ref() + } + + /// Returns an Iterator of type [`Components`]. + pub fn components(&self, separator: char) -> Components<'_> { + let split = self.0.split(separator); + Components::new(self, split) + } +} + +impl AsRef for str { + fn as_ref(&self) -> &Path { + Path::new(self) + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &Path { + self + } +} + +impl AsRef for Path { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl AsRef for String { + fn as_ref(&self) -> &Path { + self.as_str().as_ref() + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", &self.0) + } +} + +/// [`Iterator`] over the [`Component`]s of a [`Path`]. +#[derive(Debug)] +pub struct Components<'a> { + path: &'a Path, + split: core::str::Split<'a, char>, + i: usize, +} + +impl<'a> Components<'a> { + fn new(path: &'a Path, split: core::str::Split<'a, char>) -> Self { + Self { path, split, i: 0 } + } +} + +impl<'a> Iterator for Components<'a> { + type Item = Component<'a>; + + fn next(&mut self) -> Option { + if self.path.0.is_empty() { + return None; + }; + + self.split.next().map(|str| match str { + "." => Component::CurDir, + ".." => Component::ParentDir, + "" if self.i == 0 => Component::RootDir, + normal => Component::Normal(normal), + }) + } +} + +/// Components of a [`Path`]. +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq)] +pub enum Component<'a> { + /// Current dir: `.` + CurDir, + /// Parent dir: `..` + ParentDir, + /// Root directory: `/` + RootDir, + /// Normal directory or filename. + Normal(&'a str), +} + +impl<'a> Display for Component<'a> { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Component::CurDir => f.write_str(".\\"), + Component::ParentDir => f.write_str("..\\"), + Component::RootDir => f.write_str("\\"), + Component::Normal(normal) => f.write_str(normal), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + + #[test] + fn path_creation() { + let path_str = "/foo/bar/foobar"; + let _path = Path::new(path_str); + let path: &Path = path_str.as_ref(); + let _path: &Path = path.as_ref(); + } + + #[test] + fn path_components() { + let path_str = "/foo/./../bar/foobar"; + let path = Path::new(path_str); + assert_eq!(path_str, path.as_str()); + let components = path.components('/').collect::>(); + let expected = [ + Component::RootDir, + Component::Normal("foo"), + Component::CurDir, + Component::ParentDir, + Component::Normal("bar"), + Component::Normal("foobar"), + ]; + assert_eq!(components.as_slice(), expected.as_slice()); + + let path = Path::new("./foo"); + let components = path.components('/').collect::>(); + let expected = [Component::CurDir, Component::Normal("foo")]; + assert_eq!(components.as_slice(), expected.as_slice()); + + let path = Path::new(""); + assert_eq!(path.components('/').count(), 0); + } +} diff --git a/uefi/src/fs/uefi_types.rs b/uefi/src/fs/uefi_types.rs new file mode 100644 index 000000000..302badeac --- /dev/null +++ b/uefi/src/fs/uefi_types.rs @@ -0,0 +1,10 @@ +//! Re-export of low-level UEFI types but prefixed with `Uefi`. This simplifies +//! to differ between high-level and low-level types and interfaces in this +//! module. + +pub use uefi::proto::media::file::{ + Directory as UefiDirectoryHandle, File as UefiFileTrait, FileAttribute as UefiFileAttribute, + FileHandle as UefiFileHandle, FileInfo as UefiFileInfo, FileMode as UefiFileMode, + FileType as UefiFileType, RegularFile as UefiRegularFileHandle, +}; +pub use uefi::proto::media::fs::SimpleFileSystem as SimpleFileSystemProtocol; diff --git a/uefi/src/lib.rs b/uefi/src/lib.rs index 8d47249d0..6dd831110 100644 --- a/uefi/src/lib.rs +++ b/uefi/src/lib.rs @@ -113,6 +113,9 @@ pub mod allocator; #[cfg(feature = "logger")] pub mod logger; +#[cfg(feature = "alloc")] +pub mod fs; + // As long as this is behind "alloc", we can simplify cfg-feature attributes in this module. #[cfg(feature = "alloc")] pub(crate) mod mem; diff --git a/uefi/src/table/boot.rs b/uefi/src/table/boot.rs index c3eb024a6..af3c18ebf 100644 --- a/uefi/src/table/boot.rs +++ b/uefi/src/table/boot.rs @@ -3,12 +3,8 @@ use super::{Header, Revision}; use crate::data_types::{Align, PhysicalAddress, VirtualAddress}; use crate::proto::device_path::{DevicePath, FfiDevicePath}; -#[cfg(feature = "alloc")] -use crate::proto::{loaded_image::LoadedImage, media::fs::SimpleFileSystem}; use crate::proto::{Protocol, ProtocolPointer}; use crate::{Char16, Event, Guid, Handle, Result, Status}; -#[cfg(feature = "alloc")] -use ::alloc::vec::Vec; use bitflags::bitflags; use core::cell::UnsafeCell; use core::ffi::c_void; @@ -17,6 +13,12 @@ use core::mem::{self, MaybeUninit}; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; use core::{ptr, slice}; +#[cfg(feature = "alloc")] +use { + crate::fs::FileSystem, + crate::proto::{loaded_image::LoadedImage, media::fs::SimpleFileSystem}, + ::alloc::vec::Vec, +}; // TODO: this similar to `SyncUnsafeCell`. Once that is stabilized we // can use it instead. @@ -1478,36 +1480,32 @@ impl BootServices { Ok(handles) } - /// Retrieves the `SimpleFileSystem` protocol associated with - /// the device the given image was loaded from. - /// - /// You can retrieve the SFS protocol associated with the boot partition - /// by passing the image handle received by the UEFI entry point to this function. + /// Retrieves a [`FileSystem`] protocol associated with the device the given + /// image was loaded from. /// /// # Errors /// - /// This function can return errors from [`open_protocol_exclusive`] and [`locate_device_path`]. - /// See those functions for more details. + /// This function can return errors from [`open_protocol_exclusive`] and + /// [`locate_device_path`]. See those functions for more details. /// /// [`open_protocol_exclusive`]: Self::open_protocol_exclusive /// [`locate_device_path`]: Self::locate_device_path + /// [`FileSystem`]: uefi::fs::FileSystem /// /// * [`uefi::Status::INVALID_PARAMETER`] /// * [`uefi::Status::UNSUPPORTED`] /// * [`uefi::Status::ACCESS_DENIED`] /// * [`uefi::Status::ALREADY_STARTED`] /// * [`uefi::Status::NOT_FOUND`] - pub fn get_image_file_system( - &self, - image_handle: Handle, - ) -> Result> { + pub fn get_image_file_system(&self, image_handle: Handle) -> Result { let loaded_image = self.open_protocol_exclusive::(image_handle)?; let device_path = self.open_protocol_exclusive::(loaded_image.device())?; let device_handle = self.locate_device_path::(&mut &*device_path)?; - self.open_protocol_exclusive(device_handle) + let protocol = self.open_protocol_exclusive(device_handle)?; + Ok(FileSystem::new(protocol)) } }