diff --git a/CHANGELOG.md b/CHANGELOG.md index be01fc593..8c167a0c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ ### 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. + access. The API is close to the `std::fs` module. The module also provides a + `Path` and a `PathBuf` abstraction that is similar to the ones from + `std::path`. However, they are adapted for UEFI. - Multiple convenience methods for `CString16` and `CStr16`, including: - `CStr16::as_slice()` - `CStr16::num_chars()` diff --git a/uefi-test-runner/src/fs/mod.rs b/uefi-test-runner/src/fs/mod.rs index 8ff38ac64..daf895e32 100644 --- a/uefi-test-runner/src/fs/mod.rs +++ b/uefi-test-runner/src/fs/mod.rs @@ -2,7 +2,8 @@ use alloc::string::{String, ToString}; use alloc::vec::Vec; -use uefi::fs::{FileSystem, FileSystemError}; +use uefi::cstr16; +use uefi::fs::{FileSystem, FileSystemError, PathBuf}; use uefi::proto::media::fs::SimpleFileSystem; use uefi::table::boot::ScopedProtocol; @@ -11,47 +12,50 @@ use uefi::table::boot::ScopedProtocol; pub fn test(sfs: ScopedProtocol) -> Result<(), FileSystemError> { let mut fs = FileSystem::new(sfs); - fs.create_dir("test_file_system_abs")?; + // test create dir + fs.create_dir(cstr16!("foo_dir"))?; - // 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")?; + // test write, copy, and read + let data_to_write = "hello world"; + fs.write(cstr16!("foo_dir\\foo"), data_to_write)?; + // Here, we additionally check that absolute paths work. + fs.copy(cstr16!("\\foo_dir\\foo"), cstr16!("\\foo_dir\\foo_cpy"))?; + let read = fs.read(cstr16!("foo_dir\\foo_cpy"))?; 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()); - + assert_eq!(read.as_str(), data_to_write); + + // test copy from non-existent file + let err = fs.copy(cstr16!("not_found"), cstr16!("abc")); + assert!(matches!(err, Err(FileSystemError::OpenError { .. }))); + + // test rename file + path buf replaces / with \ + fs.rename( + PathBuf::from(cstr16!("/foo_dir/foo_cpy")), + cstr16!("foo_dir\\foo_cpy2"), + )?; + // file should not be available after rename + let err = fs.read(cstr16!("foo_dir\\foo_cpy")); + assert!(matches!(err, Err(FileSystemError::OpenError { .. }))); + + // test read dir on a sub dir let entries = fs - .read_dir("test_file_system_abs")? - .map(|e| { - e.expect("Should return boxed file info") - .file_name() - .to_string() - }) + .read_dir(cstr16!("foo_dir"))? + .map(|entry| entry.expect("Should be valid").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")?; + assert_eq!(&[".", "..", "foo", "foo_cpy2"], entries.as_slice()); + + // test create dir recursively + fs.create_dir_all(cstr16!("foo_dir\\1\\2\\3\\4\\5\\6\\7"))?; + fs.create_dir_all(cstr16!("foo_dir\\1\\2\\3\\4\\5\\6\\7\\8"))?; + fs.write( + cstr16!("foo_dir\\1\\2\\3\\4\\5\\6\\7\\8\\foobar"), + data_to_write, + )?; + let boxinfo = fs.metadata(cstr16!("foo_dir\\1\\2\\3\\4\\5\\6\\7\\8\\foobar"))?; + assert_eq!(boxinfo.file_size(), data_to_write.len() as u64); + + // test remove dir all // 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/src/data_types/owned_strs.rs b/uefi/src/data_types/owned_strs.rs index d47f2b912..3631725f5 100644 --- a/uefi/src/data_types/owned_strs.rs +++ b/uefi/src/data_types/owned_strs.rs @@ -36,8 +36,8 @@ impl core::error::Error for FromStrError {} /// An owned UCS-2 null-terminated string. /// -/// For convenience, a [CString16] is comparable with `&str` and `String` from the standard library -/// through the trait [EqStrUntilNul]. +/// For convenience, a [`CString16`] is comparable with `&str` and `String` from +/// the standard library through the trait [`EqStrUntilNul`]. /// /// # Examples /// diff --git a/uefi/src/data_types/strs.rs b/uefi/src/data_types/strs.rs index 25db6afe9..9167afe88 100644 --- a/uefi/src/data_types/strs.rs +++ b/uefi/src/data_types/strs.rs @@ -68,7 +68,7 @@ pub enum FromStrWithBufError { /// For convenience, a [`CStr8`] is comparable with [`core::str`] and /// `alloc::string::String` from the standard library through the trait [`EqStrUntilNul`]. #[repr(transparent)] -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Ord, PartialOrd)] pub struct CStr8([Char8]); impl CStr8 { @@ -182,14 +182,14 @@ impl<'a> TryFrom<&'a CStr> for &'a CStr8 { } } -/// An UCS-2 null-terminated string. +/// An UCS-2 null-terminated string slice. /// /// This type is largely inspired by [`core::ffi::CStr`] with the exception that all characters are /// guaranteed to be 16 bit long. /// /// For convenience, a [`CStr16`] is comparable with [`core::str`] and /// `alloc::string::String` from the standard library through the trait [`EqStrUntilNul`]. -#[derive(Eq, PartialEq)] +#[derive(Eq, PartialEq, Ord, PartialOrd)] #[repr(transparent)] pub struct CStr16([Char16]); @@ -449,6 +449,12 @@ impl + ?Sized> EqStrUntilNul for CStr16 { } } +impl AsRef for CStr16 { + fn as_ref(&self) -> &CStr16 { + self + } +} + /// An iterator over the [`Char16`]s in a [`CStr16`]. #[derive(Debug)] pub struct CStr16Iter<'a> { diff --git a/uefi/src/fs/file_system.rs b/uefi/src/fs/file_system.rs index 68f800b42..41dd78c15 100644 --- a/uefi/src/fs/file_system.rs +++ b/uefi/src/fs/file_system.rs @@ -1,17 +1,18 @@ //! Module for [`FileSystem`]. use super::*; +use crate::fs::path::{validate_path, PathError}; use crate::proto::media::file::{FileAttribute, FileInfo, FileType}; use crate::table::boot::ScopedProtocol; use alloc::boxed::Box; use alloc::string::{FromUtf8Error, String, ToString}; +use alloc::vec; 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 log::debug; /// All errors that can happen when working with the [`FileSystem`]. #[derive(Debug, Clone, Display, PartialEq, Eq)] @@ -19,6 +20,8 @@ pub enum FileSystemError { /// Can't open the root directory of the underlying volume. CantOpenVolume, /// The path is invalid because of the underlying [`PathError`]. + /// + /// [`PathError`]: path::PathError IllegalPath(PathError), /// The file or directory was not found in the underlying volume. FileNotFound(String), @@ -40,12 +43,28 @@ pub enum FileSystemError { ReadFailure, /// Can't parse file content as UTF-8. Utf8Error(FromUtf8Error), - /// Could not open the given path. - OpenError(String), + /// Could not open the given path. Carries the path that could not be opened + /// and the underlying UEFI error. + #[display(fmt = "{path:?}")] + OpenError { + /// Path that caused the failure. + path: String, + /// More detailed failure description. + error: crate::Error, + }, } #[cfg(feature = "unstable")] -impl core::error::Error for FileSystemError {} +impl core::error::Error for FileSystemError { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + FileSystemError::IllegalPath(e) => Some(e), + FileSystemError::Utf8Error(e) => Some(e), + FileSystemError::OpenError { path: _path, error } => Some(error), + _ => None, + } + } +} impl From for FileSystemError { fn from(err: PathError) -> Self { @@ -90,10 +109,6 @@ impl<'a> FileSystem<'a> { 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 @@ -101,33 +116,38 @@ impl<'a> FileSystem<'a> { 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); + // Collect all relevant sub paths in a vector. + let mut dirs_to_create = vec![path.to_path_buf()]; + while let Some(parent) = dirs_to_create.last().unwrap().parent() { + debug!("parent={parent}"); + dirs_to_create.push(parent) + } + // Now reverse, so that we have something like this: + // - a + // - a\\b + // - a\\b\\c + dirs_to_create.reverse(); + + for parent in dirs_to_create { + if self.try_exists(&parent).is_err() { + self.create_dir(parent)?; + } + } - 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())) + Ok(()) } /// 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()) + log::trace!("failed to fetch file info: {err:#?}"); + FileSystemError::OpenError { + path: path.to_cstr16().to_string(), + error: err, + } }) } @@ -138,11 +158,13 @@ impl<'a> FileSystem<'a> { 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()) - })?; + .ok_or(FileSystemError::NotAFile(path.to_cstr16().to_string()))?; + let info = file + .get_boxed_info::() + .map_err(|err| FileSystemError::OpenError { + path: path.to_cstr16().to_string(), + error: err, + })?; let mut vec = vec![0; info.file_size() as usize]; let read_bytes = file.read(vec.as_mut_slice()).map_err(|e| { @@ -164,7 +186,7 @@ impl<'a> FileSystem<'a> { let dir = self .open(path, UefiFileMode::Read, false)? .into_directory() - .ok_or(FileSystemError::NotADirectory(path.as_str().to_string()))?; + .ok_or(FileSystemError::NotADirectory(path.to_cstr16().to_string()))?; Ok(UefiDirectoryIter::new(dir)) } @@ -185,16 +207,18 @@ impl<'a> FileSystem<'a> { match file { FileType::Dir(dir) => dir.delete().map_err(|e| { log::error!("error removing dir: {e:?}"); - FileSystemError::CantDeleteDirectory(path.as_str().to_string()) + FileSystemError::CantDeleteDirectory(path.to_cstr16().to_string()) }), - FileType::Regular(_) => Err(FileSystemError::NotADirectory(path.as_str().to_string())), + FileType::Regular(_) => { + Err(FileSystemError::NotADirectory(path.to_cstr16().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!() + pub fn remove_dir_all(&mut self, path: impl AsRef) -> FileSystemResult<()> { + let path = path.as_ref(); }*/ /// Removes a file from the filesystem. @@ -209,9 +233,9 @@ impl<'a> FileSystem<'a> { match file { FileType::Regular(file) => file.delete().map_err(|e| { log::error!("error removing file: {e:?}"); - FileSystemError::CantDeleteFile(path.as_str().to_string()) + FileSystemError::CantDeleteFile(path.to_cstr16().to_string()) }), - FileType::Dir(_) => Err(FileSystemError::NotAFile(path.as_str().to_string())), + FileType::Dir(_) => Err(FileSystemError::NotAFile(path.to_cstr16().to_string())), } } @@ -278,8 +302,8 @@ impl<'a> FileSystem<'a> { mode: UefiFileMode, is_dir: bool, ) -> FileSystemResult { - let path = NormalizedPath::new("\\", path)?; - log::debug!("normalized path: {path}"); + validate_path(path)?; + log::trace!("open validated path: {path}"); let attr = if mode == UefiFileMode::CreateReadWrite && is_dir { FileAttribute::DIRECTORY @@ -287,10 +311,15 @@ impl<'a> FileSystem<'a> { 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()) - }) + self.open_root()? + .open(path.to_cstr16(), mode, attr) + .map_err(|err| { + log::trace!("Can't open file {path}: {err:?}"); + FileSystemError::OpenError { + path: path.to_cstr16().to_string(), + error: err, + } + }) } } diff --git a/uefi/src/fs/mod.rs b/uefi/src/fs/mod.rs index 44d8618b4..18253ebce 100644 --- a/uefi/src/fs/mod.rs +++ b/uefi/src/fs/mod.rs @@ -1,14 +1,14 @@ -//! A high-level file system API for UEFI applications close to the `fs` module -//! from Rust's standard library. +//! A high-level file system API for UEFI applications close to the `std::fs` +//! module from Rust's standard library. The main type by this module is +//! [`FileSystem`]. //! //! # 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. //! //! Unlike in the API of typical UNIX file system abstractions, there is -//! no virtual file system. -//! -//! Unlike Windows, there is no way to access volumes by a dedicated name. +//! no virtual file system. 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 @@ -17,7 +17,8 @@ //! directory is always `/`, i.e., the root, of the opened volume. //! //! Symlinks or hard-links are not supported but only directories and regular -//! files with plain linear paths to them. +//! files with plain linear paths to them. For more information, see +//! [`Path`] and [`PathBuf`]. //! //! # API Hints //! There are no `File` and `Path` abstractions similar to those from `std` that @@ -31,14 +32,11 @@ 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}; +pub use path::*; 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 deleted file mode 100644 index 2a93237f5..000000000 --- a/uefi/src/fs/normalized_path.rs +++ /dev/null @@ -1,239 +0,0 @@ -//! Module for path normalization. See [`NormalizedPath`]. - -use super::*; -use crate::data_types::FromStrError; -use crate::CString16; -use alloc::format; -use alloc::string::{String, ToString}; -use alloc::vec::Vec; -use core::ops::Deref; -use derive_more::Display; - -/// 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 deleted file mode 100644 index 36f850241..000000000 --- a/uefi/src/fs/path.rs +++ /dev/null @@ -1,154 +0,0 @@ -//! 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/path/mod.rs b/uefi/src/fs/path/mod.rs new file mode 100644 index 000000000..8fc0ba7e2 --- /dev/null +++ b/uefi/src/fs/path/mod.rs @@ -0,0 +1,50 @@ +//! This module offers the [`Path`] and [`PathBuf`] abstractions. +//! +//! # Interoperability with Rust strings +//! +//! For the interoperability with Rust strings, i.e., `String` and `str` from +//! the standard library, the API is intended to transform these types first to +//! `CString16` respectively `CStr16`. They do not directly translate to +//! [`Path`] and [`PathBuf`]. +//! +//! # Path Structure +//! +//! Paths use the [`SEPARATOR`] character as separator. Paths are absolute and +//! do not contain `.` or `..` components. However, this can be implemented in +//! the future. + +mod path; +mod pathbuf; +mod validation; + +pub use path::{Components, Path}; +pub use pathbuf::PathBuf; + +use uefi::data_types::chars::NUL_16; +use uefi::{CStr16, Char16}; +pub(super) use validation::validate_path; +pub use validation::PathError; + +/// The default separator for paths. +pub const SEPARATOR: Char16 = unsafe { Char16::from_u16_unchecked('\\' as u16) }; + +/// Stringified version of [`SEPARATOR`]. +pub const SEPARATOR_STR: &CStr16 = uefi_macros::cstr16!("\\"); + +/// 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: [Char16; 10] = unsafe { + [ + NUL_16, + Char16::from_u16_unchecked('"' as u16), + Char16::from_u16_unchecked('*' as u16), + Char16::from_u16_unchecked('/' as u16), + Char16::from_u16_unchecked(':' as u16), + Char16::from_u16_unchecked('<' as u16), + Char16::from_u16_unchecked('>' as u16), + Char16::from_u16_unchecked('?' as u16), + SEPARATOR, + Char16::from_u16_unchecked('|' as u16), + ] +}; diff --git a/uefi/src/fs/path/path.rs b/uefi/src/fs/path/path.rs new file mode 100644 index 000000000..bf6af9886 --- /dev/null +++ b/uefi/src/fs/path/path.rs @@ -0,0 +1,295 @@ +// allow "path.rs" in "path" +#![allow(clippy::module_inception)] + +use crate::fs::path::{PathBuf, SEPARATOR}; +use crate::CStr16; +use core::fmt::{Display, Formatter}; +use uefi::CString16; + +/// A path similar to the `Path` of the standard library, but based on +/// [`CStr16`] strings and [`SEPARATOR`] as separator. +/// +/// [`SEPARATOR`]: super::SEPARATOR +#[derive(Debug, Eq, PartialOrd, Ord)] +pub struct Path(CStr16); + +impl Path { + /// Constructor. + #[must_use] + pub fn new + ?Sized>(s: &S) -> &Self { + unsafe { &*(s.as_ref() as *const CStr16 as *const Self) } + } + + /// Returns the underlying string. + #[must_use] + pub fn to_cstr16(&self) -> &CStr16 { + &self.0 + } + + /// Returns a path buf from that type. + #[must_use] + pub fn to_path_buf(&self) -> PathBuf { + let cstring = CString16::from(&self.0); + PathBuf::from(cstring) + } + + /// Iterator over the components of a path. + #[must_use] + pub fn components(&self) -> Components { + Components { + path: self.as_ref(), + i: 0, + } + } + + /// Returns the parent directory as [`PathBuf`]. + /// + /// If the path is a top-level component, this returns None. + #[must_use] + pub fn parent(&self) -> Option { + let components_count = self.components().count(); + if components_count == 0 { + return None; + } + + // Return None, as we do not treat "\\" as dedicated component. + let sep_count = self + .0 + .as_slice() + .iter() + .filter(|char| **char == SEPARATOR) + .count(); + if sep_count == 0 { + return None; + } + + let path = + self.components() + .take(components_count - 1) + .fold(CString16::new(), |mut acc, next| { + // Add separator, as needed. + if !acc.is_empty() && *acc.as_slice().last().unwrap() != SEPARATOR { + acc.push(SEPARATOR); + } + acc.push_str(next.as_ref()); + acc + }); + let path = PathBuf::from(path); + Some(path) + } + + /// Returns of the path is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.to_cstr16().is_empty() + } +} + +impl Display for Path { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Display::fmt(self.to_cstr16(), f) + } +} + +impl PartialEq for Path { + fn eq(&self, other: &Self) -> bool { + self.components().count() == other.components().count() + && !self + .components() + .zip(other.components()) + .any(|(c1, c2)| c1 != c2) + } +} + +/// Iterator over the components of a path. For example, the path `\\a\\b\\c` +/// has the components `[a, b, c]`. This is a more basic approach than the +/// components type of the standard library. +#[derive(Debug)] +pub struct Components<'a> { + path: &'a CStr16, + i: usize, +} + +impl<'a> Iterator for Components<'a> { + // Attention. We can't iterate over &'Ctr16, as we would break any guarantee + // made for the terminating null character. + type Item = CString16; + + fn next(&mut self) -> Option { + if self.path.is_empty() { + return None; + } + if self.path.num_chars() == 1 && self.path.as_slice()[0] == SEPARATOR { + // The current implementation does not handle the root dir as + // dedicated component so far. We just return nothing. + return None; + } + + // If the path is not empty and starts with a separator, skip it. + if self.i == 0 && *self.path.as_slice().first().unwrap() == SEPARATOR { + self.i = 1; + } + + // Count how many characters are there until the next separator is + // found. + let len = self + .path + .iter() + .skip(self.i) + .take_while(|c| **c != SEPARATOR) + .count(); + + let progress = self.i + len; + if progress > self.path.num_chars() { + None + } else { + // select the next component and build an owned string + let part = &self.path.as_slice()[self.i..self.i + len]; + let mut string = CString16::new(); + part.iter().for_each(|c| string.push(*c)); + + // +1: skip the separator + self.i = progress + 1; + Some(string) + } + } +} + +mod convenience_impls { + use super::*; + use core::borrow::Borrow; + + impl AsRef for &Path { + fn as_ref(&self) -> &Path { + self + } + } + + impl<'a> From<&'a CStr16> for &'a Path { + fn from(value: &'a CStr16) -> Self { + Path::new(value) + } + } + + impl AsRef for Path { + fn as_ref(&self) -> &CStr16 { + &self.0 + } + } + + impl Borrow for Path { + fn borrow(&self) -> &CStr16 { + &self.0 + } + } + + impl AsRef for CStr16 { + fn as_ref(&self) -> &Path { + Path::new(self) + } + } + + impl Borrow for CStr16 { + fn borrow(&self) -> &Path { + Path::new(self) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec::Vec; + use uefi_macros::cstr16; + + #[test] + fn from_cstr16() { + let source: &CStr16 = cstr16!("\\hello\\foo\\bar"); + let _path: &Path = source.into(); + let _path: &Path = Path::new(source); + } + + #[test] + fn from_cstring16() { + let source = CString16::try_from("\\hello\\foo\\bar").unwrap(); + let _path: &Path = source.as_ref().into(); + let _path: &Path = Path::new(source.as_ref()); + } + + #[test] + fn components_iter() { + let path = Path::new(cstr16!("foo\\bar\\hello")); + let components = path.components().collect::>(); + let components: Vec<&CStr16> = components.iter().map(|x| x.as_ref()).collect::>(); + let expected: &[&CStr16] = &[cstr16!("foo"), cstr16!("bar"), cstr16!("hello")]; + assert_eq!(components.as_slice(), expected); + + // In case there is a leading slash, it should be ignored. + let path = Path::new(cstr16!("\\foo\\bar\\hello")); + let components = path.components().collect::>(); + let components: Vec<&CStr16> = components.iter().map(|x| x.as_ref()).collect::>(); + let expected: &[&CStr16] = &[cstr16!("foo"), cstr16!("bar"), cstr16!("hello")]; + assert_eq!(components.as_slice(), expected); + + // empty path iteration should be just fine + let empty_cstring16 = CString16::try_from("").unwrap(); + let path = Path::new(empty_cstring16.as_ref()); + let components = path.components().collect::>(); + let expected: &[CString16] = &[]; + assert_eq!(components.as_slice(), expected); + + // test empty path + let _path = Path::new(cstr16!()); + let path = Path::new(cstr16!("")); + let components = path.components().collect::>(); + let components: Vec<&CStr16> = components.iter().map(|x| x.as_ref()).collect::>(); + let expected: &[&CStr16] = &[]; + assert_eq!(components.as_slice(), expected); + + // test path that has only root component. Treated as empty path by + // the components iterator. + let path = Path::new(cstr16!("\\")); + let components = path.components().collect::>(); + let components: Vec<&CStr16> = components.iter().map(|x| x.as_ref()).collect::>(); + let expected: &[&CStr16] = &[]; + assert_eq!(components.as_slice(), expected); + } + + #[test] + fn test_parent() { + assert_eq!(None, Path::new(cstr16!("")).parent()); + assert_eq!(None, Path::new(cstr16!("\\")).parent()); + assert_eq!( + Path::new(cstr16!("a\\b")).parent(), + Some(PathBuf::from(cstr16!("a"))), + ); + assert_eq!( + Path::new(cstr16!("\\a\\b")).parent(), + Some(PathBuf::from(cstr16!("a"))), + ); + assert_eq!( + Path::new(cstr16!("a\\b\\c\\d")).parent(), + Some(PathBuf::from(cstr16!("a\\b\\c"))), + ); + assert_eq!(Path::new(cstr16!("abc")).parent(), None,); + } + + #[test] + fn partial_eq() { + let path1 = Path::new(cstr16!(r"a\b")); + let path2 = Path::new(cstr16!(r"\a\b")); + let path3 = Path::new(cstr16!(r"a\b\c")); + + assert_eq!(path1, path1); + assert_eq!(path2, path2); + assert_eq!(path3, path3); + + // Equal as currently, we only support absolute paths, so the leading + // separator is obligatory. + assert_eq!(path1, path2); + assert_eq!(path2, path1); + + assert_ne!(path1, path3); + assert_ne!(path3, path1); + } +} diff --git a/uefi/src/fs/path/pathbuf.rs b/uefi/src/fs/path/pathbuf.rs new file mode 100644 index 000000000..e018e3af0 --- /dev/null +++ b/uefi/src/fs/path/pathbuf.rs @@ -0,0 +1,186 @@ +use crate::fs::path::Path; +use crate::fs::SEPARATOR; +use crate::{CStr16, CString16, Char16}; +use core::fmt::{Display, Formatter}; + +/// A path buffer similar to the `PathBuf` of the standard library, but based on +/// [`CString16`] strings and [`SEPARATOR`] as separator. +/// +/// `/` is replaced by [`SEPARATOR`] on the fly. +#[derive(Debug, Default, Eq, PartialOrd, Ord)] +pub struct PathBuf(CString16); + +impl PathBuf { + /// Constructor. + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Constructor that replaces all occurrences of `/` with `\`. + fn new_from_cstring16(mut string: CString16) -> Self { + const SEARCH: Char16 = unsafe { Char16::from_u16_unchecked('/' as u16) }; + string.replace_char(SEARCH, SEPARATOR); + Self(string) + } + + /// Extends self with path. + /// + /// UNIX separators (`/`) will be replaced by [`SEPARATOR`] on the fly. + pub fn push>(&mut self, path: P) { + const SEARCH: Char16 = unsafe { Char16::from_u16_unchecked('/' as u16) }; + + // do nothing on empty path + if path.as_ref().is_empty() { + return; + } + + let empty = self.0.is_empty(); + let needs_sep = *self + .0 + .as_slice_with_nul() + .last() + .expect("Should have at least null character") + != SEPARATOR; + if !empty && needs_sep { + self.0.push(SEPARATOR) + } + + self.0.push_str(path.as_ref().to_cstr16()); + self.0.replace_char(SEARCH, SEPARATOR); + } +} + +impl PartialEq for PathBuf { + fn eq(&self, other: &Self) -> bool { + let path1: &Path = self.as_ref(); + let path2: &Path = other.as_ref(); + path1 == path2 + } +} + +impl Display for PathBuf { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Display::fmt(self.to_cstr16(), f) + } +} + +mod convenience_impls { + use super::*; + use core::borrow::Borrow; + use core::ops::Deref; + + impl From for PathBuf { + fn from(value: CString16) -> Self { + Self::new_from_cstring16(value) + } + } + + impl From<&CStr16> for PathBuf { + fn from(value: &CStr16) -> Self { + Self::new_from_cstring16(CString16::from(value)) + } + } + + impl Deref for PathBuf { + type Target = Path; + + fn deref(&self) -> &Self::Target { + Path::new(&self.0) + } + } + + impl AsRef for PathBuf { + fn as_ref(&self) -> &Path { + // falls back to deref impl + self + } + } + + impl Borrow for PathBuf { + fn borrow(&self) -> &Path { + // falls back to deref impl + self + } + } + + impl AsRef for PathBuf { + fn as_ref(&self) -> &CStr16 { + &self.0 + } + } + + impl Borrow for PathBuf { + fn borrow(&self) -> &CStr16 { + &self.0 + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::string::ToString; + use uefi_macros::cstr16; + + #[test] + fn from_cstr16() { + let source: &CStr16 = cstr16!("\\hello\\foo\\bar"); + let _path: PathBuf = source.into(); + } + + #[test] + fn from_cstring16() { + let source = CString16::try_from("\\hello\\foo\\bar").unwrap(); + let _path: PathBuf = source.as_ref().into(); + let _path: PathBuf = source.clone().into(); + let _path: PathBuf = PathBuf::new_from_cstring16(source); + } + + #[test] + fn from_std_string() { + let std_string = "\\hello\\foo\\bar".to_string(); + let _path = PathBuf::new_from_cstring16(CString16::try_from(std_string.as_str()).unwrap()); + } + + #[test] + fn push() { + let mut pathbuf = PathBuf::new(); + pathbuf.push(cstr16!("first")); + pathbuf.push(cstr16!("second")); + pathbuf.push(cstr16!("third")); + assert_eq!(pathbuf.to_cstr16(), cstr16!("first\\second\\third")); + + let mut pathbuf = PathBuf::new(); + pathbuf.push(cstr16!("\\first")); + pathbuf.push(cstr16!("second")); + assert_eq!(pathbuf.to_cstr16(), cstr16!("\\first\\second")); + + // empty pushes should be ignored and have no effect + let empty_cstring16 = CString16::try_from("").unwrap(); + let mut pathbuf = PathBuf::new(); + pathbuf.push(cstr16!("first")); + pathbuf.push(empty_cstring16.as_ref()); + pathbuf.push(empty_cstring16.as_ref()); + pathbuf.push(empty_cstring16.as_ref()); + pathbuf.push(cstr16!("second")); + assert_eq!(pathbuf.to_cstr16(), cstr16!("first\\second")); + } + + #[test] + fn partial_eq() { + let mut pathbuf1 = PathBuf::new(); + pathbuf1.push(cstr16!("first")); + pathbuf1.push(cstr16!("second")); + pathbuf1.push(cstr16!("third")); + + assert_eq!(pathbuf1, pathbuf1); + + let mut pathbuf2 = PathBuf::new(); + pathbuf2.push(cstr16!("\\first")); + pathbuf2.push(cstr16!("second")); + + assert_eq!(pathbuf2, pathbuf2); + assert_ne!(pathbuf1, pathbuf2); + } +} diff --git a/uefi/src/fs/path/validation.rs b/uefi/src/fs/path/validation.rs new file mode 100644 index 000000000..5a0323f9f --- /dev/null +++ b/uefi/src/fs/path/validation.rs @@ -0,0 +1,72 @@ +//! Path validation for the purpose of the [`fs`] module. This is decoupled from +//! [`Path`] and [`PathBuf`], as the Rust standard library also does it this +//! way. Instead, the FS implementation is responsible for that. +//! +//! [`PathBuf`]: super::PathBuf +//! [`fs`]: crate::fs + +use super::Path; +use crate::fs::CHARACTER_DENY_LIST; +use crate::Char16; +use derive_more::Display; + +/// Errors related to file paths. +#[derive(Debug, Clone, Eq, PartialEq, Display)] +pub enum PathError { + /// The path is empty / points to nothing. + Empty, + /// A component of the path is empty, i.e., two separators without content + /// in between were found. + EmptyComponent, + /// There are illegal characters in the path. + IllegalChar(Char16), +} + +#[cfg(feature = "unstable")] +impl core::error::Error for PathError {} + +/// Validates a path for the needs of the [`fs`] module. +/// +/// [`fs`]: crate::fs +pub fn validate_path>(path: P) -> Result<(), PathError> { + let path = path.as_ref(); + if path.is_empty() { + return Err(PathError::Empty); + } + for component in path.components() { + if component.is_empty() { + return Err(PathError::EmptyComponent); + } else if let Some(char) = component + .as_slice() + .iter() + .find(|c| CHARACTER_DENY_LIST.contains(c)) + { + return Err(PathError::IllegalChar(*char)); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::fs::PathBuf; + use crate::CString16; + use uefi_macros::cstr16; + + #[test] + fn test_validate_path() { + validate_path(cstr16!("hello\\foo\\bar")).unwrap(); + + let err = validate_path(cstr16!("hello\\f>oo\\bar")).unwrap_err(); + assert_eq!(err, PathError::IllegalChar(CHARACTER_DENY_LIST[6])); + + let err = validate_path(cstr16!("hello\\\\bar")).unwrap_err(); + assert_eq!(err, PathError::EmptyComponent); + + let empty_cstring16 = CString16::try_from("").unwrap(); + let path = PathBuf::from(empty_cstring16); + let err = validate_path(path).unwrap_err(); + assert_eq!(err, PathError::Empty) + } +} diff --git a/uefi/src/result/error.rs b/uefi/src/result/error.rs index 4cd657d83..8a88cc1b0 100644 --- a/uefi/src/result/error.rs +++ b/uefi/src/result/error.rs @@ -6,7 +6,7 @@ use core::fmt::{Debug, Display}; /// An UEFI-related error with optionally additional payload data. The error /// kind is encoded in the `status` field (see [`Status`]). Additional payload /// may be inside the `data` field. -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Error { status: Status, data: Data,