From bbdc273775dc1937d0598ca3e02de98ef9c31393 Mon Sep 17 00:00:00 2001 From: Koichi Imai Date: Thu, 12 Jun 2025 15:40:09 +0900 Subject: [PATCH 01/63] fix fatfs to use arc Signed-off-by: Koichi Imai --- awkernel_lib/src/file/fatfs/dir.rs | 106 +++++++++---------- awkernel_lib/src/file/fatfs/dir_entry.rs | 23 +++-- awkernel_lib/src/file/fatfs/file.rs | 53 +++++----- awkernel_lib/src/file/fatfs/fs.rs | 126 ++++++++++++----------- 4 files changed, 152 insertions(+), 156 deletions(-) diff --git a/awkernel_lib/src/file/fatfs/dir.rs b/awkernel_lib/src/file/fatfs/dir.rs index 88d207297..a71d3d22e 100644 --- a/awkernel_lib/src/file/fatfs/dir.rs +++ b/awkernel_lib/src/file/fatfs/dir.rs @@ -1,3 +1,4 @@ +use alloc::sync::Arc; #[cfg(all(not(feature = "std"), feature = "alloc", feature = "lfn"))] use alloc::vec::Vec; use core::num; @@ -21,12 +22,12 @@ use super::time::TimeProvider; #[cfg(feature = "lfn")] const LFN_PADDING: u16 = 0xFFFF; -pub(crate) enum DirRawStream<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { - File(File<'a, IO, TP, OCC>), - Root(DiskSlice, FsIoAdapter<'a, IO, TP, OCC>>), +pub(crate) enum DirRawStream { + File(File), + Root(DiskSlice, FsIoAdapter>), } -impl DirRawStream<'_, IO, TP, OCC> { +impl DirRawStream { fn abs_pos(&self) -> Option { match self { DirRawStream::File(file) => file.abs_pos(), @@ -50,7 +51,7 @@ impl DirRawStream<'_, IO, TP, OCC> { } // Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925 -impl Clone for DirRawStream<'_, IO, TP, OCC> { +impl Clone for DirRawStream { fn clone(&self) -> Self { match self { DirRawStream::File(file) => DirRawStream::File(file.clone()), @@ -59,13 +60,11 @@ impl Clone for DirRawStream<'_, IO, TP } } -impl IoBase for DirRawStream<'_, IO, TP, OCC> { +impl IoBase for DirRawStream { type Error = Error; } -impl Read - for DirRawStream<'_, IO, TP, OCC> -{ +impl Read for DirRawStream { fn read(&mut self, buf: &mut [u8]) -> Result { match self { DirRawStream::File(file) => file.read(buf), @@ -74,9 +73,7 @@ impl Read } } -impl Write - for DirRawStream<'_, IO, TP, OCC> -{ +impl Write for DirRawStream { fn write(&mut self, buf: &[u8]) -> Result { match self { DirRawStream::File(file) => file.write(buf), @@ -91,7 +88,7 @@ impl Write } } -impl Seek for DirRawStream<'_, IO, TP, OCC> { +impl Seek for DirRawStream { fn seek(&mut self, pos: SeekFrom) -> Result { match self { DirRawStream::File(file) => file.seek(pos), @@ -107,8 +104,8 @@ fn split_path(path: &str) -> (&str, Option<&str>) { }) } -enum DirEntryOrShortName<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { - DirEntry(DirEntry<'a, IO, TP, OCC>), +enum DirEntryOrShortName { + DirEntry(DirEntry), ShortName([u8; SFN_SIZE]), } @@ -116,36 +113,31 @@ enum DirEntryOrShortName<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { /// /// This struct is created by the `open_dir` or `create_dir` methods on `Dir`. /// The root directory is returned by the `root_dir` method on `FileSystem`. -pub struct Dir<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { - stream: DirRawStream<'a, IO, TP, OCC>, - fs: &'a FileSystem, +pub struct Dir { + stream: DirRawStream, + fs: Arc>, } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> Dir<'a, IO, TP, OCC> { - pub(crate) fn new( - stream: DirRawStream<'a, IO, TP, OCC>, - fs: &'a FileSystem, - ) -> Self { +impl Dir { + pub(crate) fn new(stream: DirRawStream, fs: Arc>) -> Self { Dir { stream, fs } } /// Creates directory entries iterator. #[must_use] #[allow(clippy::iter_not_returning_iterator)] - pub fn iter(&self) -> DirIter<'a, IO, TP, OCC> { - DirIter::new(self.stream.clone(), self.fs, true) + pub fn iter(&self) -> DirIter { + DirIter::new(self.stream.clone(), self.fs.clone(), true) } } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> - Dir<'a, IO, TP, OCC> -{ +impl Dir { fn find_entry( &self, name: &str, is_dir: Option, mut short_name_gen: Option<&mut ShortNameGenerator>, - ) -> Result, Error> { + ) -> Result, Error> { for r in self.iter() { let e = r?; // compare name ignoring case @@ -172,8 +164,8 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> #[allow(clippy::type_complexity)] pub(crate) fn find_volume_entry( &self, - ) -> Result>, Error> { - for r in DirIter::new(self.stream.clone(), self.fs, false) { + ) -> Result>, Error> { + for r in DirIter::new(self.stream.clone(), self.fs.clone(), false) { let e = r?; if e.data.is_volume() { return Ok(Some(e)); @@ -186,7 +178,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> &self, name: &str, is_dir: Option, - ) -> Result, Error> { + ) -> Result, Error> { let mut short_name_gen = ShortNameGenerator::new(name); loop { // find matching entry @@ -241,7 +233,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> /// * `Error::NotFound` will be returned if `path` points to a non-existing directory entry. /// * `Error::InvalidInput` will be returned if `path` points to a file that is a directory. /// * `Error::Io` will be returned if the underlying storage object returned an I/O error. - pub fn open_file(&self, path: &str) -> Result, Error> { + pub fn open_file(&self, path: &str) -> Result, Error> { log::trace!("Dir::open_file {path}"); // traverse path let (name, rest_opt) = split_path(path); @@ -268,7 +260,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> /// * `Error::UnsupportedFileNameCharacter` will be returned if the file name contains an invalid character. /// * `Error::NotEnoughSpace` will be returned if there is not enough free space to create a new file. /// * `Error::Io` will be returned if the underlying storage object returned an I/O error. - pub fn create_file(&self, path: &str) -> Result, Error> { + pub fn create_file(&self, path: &str) -> Result, Error> { log::trace!("Dir::create_file {path}"); // traverse path let (name, rest_opt) = split_path(path); @@ -321,7 +313,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> // directory does not exist - create it DirEntryOrShortName::ShortName(short_name) => { // alloc cluster for directory data - let cluster = self.fs.alloc_cluster(None, true)?; + let cluster = FileSystem::alloc_cluster(&self.fs, None, true)?; // create entry in parent directory let sfn_entry = self.create_sfn_entry(short_name, FileAttributes::DIRECTORY, Some(cluster)); @@ -395,7 +387,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> } // free data if let Some(n) = e.first_cluster() { - self.fs.free_cluster_chain(n)?; + FileSystem::free_cluster_chain(&self.fs, n)?; } // free long and short name entries let mut stream = self.stream.clone(); @@ -495,7 +487,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> fn find_free_entries( &self, num_entries: u32, - ) -> Result, Error> { + ) -> Result, Error> { let mut stream = self.stream.clone(); let mut first_free: u32 = 0; let mut num_free: u32 = 0; @@ -559,7 +551,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> &self, lfn_utf16: &LfnBuffer, short_name: &[u8; SFN_SIZE], - ) -> Result<(DirRawStream<'a, IO, TP, OCC>, u64), Error> { + ) -> Result<(DirRawStream, u64), Error> { // get short name checksum let lfn_chsum = lfn_checksum(short_name); // create LFN entries generator @@ -576,7 +568,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> } #[allow(clippy::type_complexity)] - fn alloc_sfn_entry(&self) -> Result<(DirRawStream<'a, IO, TP, OCC>, u64), Error> { + fn alloc_sfn_entry(&self) -> Result<(DirRawStream, u64), Error> { let mut stream = self.find_free_entries(1)?; let start_pos = stream.seek(super::super::io::SeekFrom::Current(0))?; Ok((stream, start_pos)) @@ -586,7 +578,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> &self, name: &str, raw_entry: DirFileEntryData, - ) -> Result, Error> { + ) -> Result, Error> { log::trace!("Dir::write_entry {name}"); // check if name doesn't contain unsupported characters validate_long_name(name)?; @@ -618,7 +610,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> short_name, #[cfg(feature = "lfn")] lfn_utf16, - fs: self.fs, + fs: self.fs.clone(), entry_pos: start_abs_pos, offset_range: (start_pos, end_pos), }) @@ -627,12 +619,12 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC: OemCpConverter> // Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925 impl Clone - for Dir<'_, IO, TP, OCC> + for Dir { fn clone(&self) -> Self { Self { stream: self.stream.clone(), - fs: self.fs, + fs: self.fs.clone(), } } } @@ -640,17 +632,17 @@ impl Clo /// An iterator over the directory entries. /// /// This struct is created by the `iter` method on `Dir`. -pub struct DirIter<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { - stream: DirRawStream<'a, IO, TP, OCC>, - fs: &'a FileSystem, +pub struct DirIter { + stream: DirRawStream, + fs: Arc>, skip_volume: bool, err: bool, } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> DirIter<'a, IO, TP, OCC> { +impl DirIter { fn new( - stream: DirRawStream<'a, IO, TP, OCC>, - fs: &'a FileSystem, + stream: DirRawStream, + fs: Arc>, skip_volume: bool, ) -> Self { DirIter { @@ -662,7 +654,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> DirIter<'a, IO, TP, OCC> { } } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC> DirIter<'a, IO, TP, OCC> { +impl DirIter { fn should_skip_entry(&self, raw_entry: &DirEntryData) -> bool { if raw_entry.is_deleted() { return true; @@ -674,7 +666,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC> DirIter<'a, IO, } #[allow(clippy::type_complexity)] - fn read_dir_entry(&mut self) -> Result>, Error> { + fn read_dir_entry(&mut self) -> Result>, Error> { log::trace!("DirIter::read_dir_entry"); let mut lfn_builder = LongNameBuilder::new(); let mut offset = self.stream.seek(SeekFrom::Current(0))?; @@ -713,7 +705,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC> DirIter<'a, IO, short_name, #[cfg(feature = "lfn")] lfn_utf16: lfn_builder.into_buf(), - fs: self.fs, + fs: self.fs.clone(), entry_pos: abs_pos, offset_range: (begin_offset, offset), })); @@ -729,21 +721,19 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC> DirIter<'a, IO, } // Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925 -impl Clone for DirIter<'_, IO, TP, OCC> { +impl Clone for DirIter { fn clone(&self) -> Self { Self { stream: self.stream.clone(), - fs: self.fs, + fs: self.fs.clone(), err: self.err, skip_volume: self.skip_volume, } } } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP: TimeProvider, OCC> Iterator - for DirIter<'a, IO, TP, OCC> -{ - type Item = Result, Error>; +impl Iterator for DirIter { + type Item = Result, Error>; fn next(&mut self) -> Option { if self.err { diff --git a/awkernel_lib/src/file/fatfs/dir_entry.rs b/awkernel_lib/src/file/fatfs/dir_entry.rs index 99faacdc5..0d6412dcd 100644 --- a/awkernel_lib/src/file/fatfs/dir_entry.rs +++ b/awkernel_lib/src/file/fatfs/dir_entry.rs @@ -1,5 +1,6 @@ +use alloc::sync::Arc; #[cfg(all(not(feature = "std"), feature = "alloc"))] -use alloc::string::String; +use alloc::{string::String, sync::Arc}; use bitflags::bitflags; use core::char; use core::convert::TryInto; @@ -552,18 +553,18 @@ impl DirEntryEditor { /// /// `DirEntry` is returned by `DirIter` when reading a directory. #[derive(Clone)] -pub struct DirEntry<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { +pub struct DirEntry { pub(crate) data: DirFileEntryData, pub(crate) short_name: ShortName, #[cfg(feature = "lfn")] pub(crate) lfn_utf16: LfnBuffer, pub(crate) entry_pos: u64, pub(crate) offset_range: (u64, u64), - pub(crate) fs: &'a FileSystem, + pub(crate) fs: Arc>, } #[allow(clippy::len_without_is_empty)] -impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC: OemCpConverter> DirEntry<'a, IO, TP, OCC> { +impl DirEntry { /// Returns short file name. /// /// Non-ASCII characters are replaced by the replacement character (U+FFFD). @@ -647,9 +648,9 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC: OemCpConverter> DirEntry<'a, /// /// Will panic if this is not a file. #[must_use] - pub fn to_file(&self) -> File<'a, IO, TP, OCC> { + pub fn to_file(&self) -> File { assert!(!self.is_dir(), "Not a file entry"); - File::new(self.first_cluster(), Some(self.editor()), self.fs) + File::new(self.first_cluster(), Some(self.editor()), self.fs.clone()) } /// Returns `Dir` struct for this entry. @@ -658,14 +659,14 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC: OemCpConverter> DirEntry<'a, /// /// Will panic if this is not a directory. #[must_use] - pub fn to_dir(&self) -> Dir<'a, IO, TP, OCC> { + pub fn to_dir(&self) -> Dir { assert!(self.is_dir(), "Not a directory entry"); match self.first_cluster() { Some(n) => { - let file = File::new(Some(n), Some(self.editor()), self.fs); - Dir::new(DirRawStream::File(file), self.fs) + let file = File::new(Some(n), Some(self.editor()), self.fs.clone()); + Dir::new(DirRawStream::File(file), self.fs.clone()) } - None => self.fs.root_dir(), + None => FileSystem::root_dir(&self.fs), } } @@ -740,7 +741,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC: OemCpConverter> DirEntry<'a, } } -impl fmt::Debug for DirEntry<'_, IO, TP, OCC> { +impl fmt::Debug for DirEntry { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { self.data.fmt(f) } diff --git a/awkernel_lib/src/file/fatfs/file.rs b/awkernel_lib/src/file/fatfs/file.rs index 1f1560a9d..61f6127ff 100644 --- a/awkernel_lib/src/file/fatfs/file.rs +++ b/awkernel_lib/src/file/fatfs/file.rs @@ -1,3 +1,4 @@ +use alloc::sync::Arc; use core::convert::TryFrom; use super::super::error::Error; @@ -13,7 +14,7 @@ const MAX_FILE_SIZE: u32 = u32::MAX; /// A FAT filesystem file object used for reading and writing data. /// /// This struct is created by the `open_file` or `create_file` methods on `Dir`. -pub struct File<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { +pub struct File { // Note first_cluster is None if file is empty first_cluster: Option, // Note: if offset points between clusters current_cluster is the previous cluster @@ -23,7 +24,7 @@ pub struct File<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { // file dir entry editor - None for root dir entry: Option, // file-system reference - fs: &'a FileSystem, + fs: Arc>, } /// An extent containing a file's data on disk. @@ -37,11 +38,11 @@ pub struct Extent { pub size: u32, } -impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { +impl File { pub(crate) fn new( first_cluster: Option, entry: Option, - fs: &'a FileSystem, + fs: Arc>, ) -> Self { File { first_cluster, @@ -75,11 +76,11 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { if let Some(current_cluster) = self.current_cluster { // current cluster is none only if offset is 0 debug_assert!(self.offset > 0); - self.fs.truncate_cluster_chain(current_cluster) + FileSystem::truncate_cluster_chain(&self.fs, current_cluster) } else { debug_assert!(self.offset == 0); if let Some(n) = self.first_cluster { - self.fs.free_cluster_chain(n)?; + FileSystem::free_cluster_chain(&self.fs, n)?; self.first_cluster = None; } Ok(()) @@ -90,8 +91,8 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { /// /// This returns an iterator over the byte ranges on-disk occupied by /// this file. - pub fn extents(&mut self) -> impl Iterator>> + 'a { - let fs = self.fs; + pub fn extents(&mut self) -> impl Iterator>> + '_ { + let fs = &self.fs; let cluster_size = fs.cluster_size(); let Some(mut bytes_left) = self.size() else { return None.into_iter().flatten(); @@ -102,7 +103,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { Some( core::iter::once(Ok(first)) - .chain(fs.cluster_iter(first)) + .chain(FileSystem::cluster_iter(fs, first)) .map(move |cluster_err| match cluster_err { Ok(cluster) => { let size = cluster_size.min(bytes_left); @@ -142,7 +143,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { fn flush_dir_entry(&mut self) -> Result<(), Error> { if let Some(ref mut e) = self.entry { - e.flush(self.fs)?; + e.flush(&self.fs)?; } Ok(()) } @@ -220,7 +221,7 @@ impl<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> File<'a, IO, TP, OCC> { } } -impl File<'_, IO, TP, OCC> { +impl File { fn update_dir_entry_after_write(&mut self) { let offset = self.offset; if let Some(ref mut e) = self.entry { @@ -233,7 +234,7 @@ impl File<'_, IO, TP, OC } } -impl Drop for File<'_, IO, TP, OCC> { +impl Drop for File { fn drop(&mut self) { if let Err(err) = self.flush() { log::error!("flush failed {err:?}"); @@ -242,23 +243,23 @@ impl Drop for File<'_, IO, TP, OCC> { } // Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925 -impl Clone for File<'_, IO, TP, OCC> { +impl Clone for File { fn clone(&self) -> Self { File { first_cluster: self.first_cluster, current_cluster: self.current_cluster, offset: self.offset, entry: self.entry.clone(), - fs: self.fs, + fs: Arc::clone(&self.fs), } } } -impl IoBase for File<'_, IO, TP, OCC> { +impl IoBase for File { type Error = Error; } -impl Read for File<'_, IO, TP, OCC> { +impl Read for File { fn read(&mut self, buf: &mut [u8]) -> Result { log::trace!("File::read"); let cluster_size = self.fs.cluster_size(); @@ -267,7 +268,7 @@ impl Read for File<'_, I match self.current_cluster { None => self.first_cluster, Some(n) => { - let r = self.fs.cluster_iter(n).next(); + let r = FileSystem::cluster_iter(&self.fs, n).next(); match r { Some(Err(err)) => return Err(err), Some(Ok(n)) => Some(n), @@ -314,7 +315,7 @@ impl Read for File<'_, I } #[cfg(feature = "std")] -impl std::io::Read for File<'_, IO, TP, OCC> +impl std::io::Read for File where std::io::Error: From>, { @@ -323,7 +324,7 @@ where } } -impl Write for File<'_, IO, TP, OCC> { +impl Write for File { fn write(&mut self, buf: &[u8]) -> Result { log::trace!("File::write"); let cluster_size = self.fs.cluster_size(); @@ -346,7 +347,7 @@ impl Write for File<'_, let next_cluster = match self.current_cluster { None => self.first_cluster, Some(n) => { - let r = self.fs.cluster_iter(n).next(); + let r = FileSystem::cluster_iter(&self.fs, n).next(); match r { Some(Err(err)) => return Err(err), Some(Ok(n)) => Some(n), @@ -358,7 +359,8 @@ impl Write for File<'_, n } else { // end of chain reached - allocate new cluster - let new_cluster = self.fs.alloc_cluster(self.current_cluster, self.is_dir())?; + let new_cluster = + FileSystem::alloc_cluster(&self.fs, self.current_cluster, self.is_dir())?; log::trace!("allocated cluster {new_cluster}"); if self.first_cluster.is_none() { self.set_first_cluster(new_cluster); @@ -397,8 +399,7 @@ impl Write for File<'_, } #[cfg(feature = "std")] -impl std::io::Write - for File<'_, IO, TP, OCC> +impl std::io::Write for File where std::io::Error: From>, { @@ -415,7 +416,7 @@ where } } -impl Seek for File<'_, IO, TP, OCC> { +impl Seek for File { fn seek(&mut self, pos: SeekFrom) -> Result { log::trace!("File::seek"); let size_opt = self.size(); @@ -461,7 +462,7 @@ impl Seek for File<'_, IO, TP, OCC> { debug_assert!(new_offset_in_clusters > 0); let clusters_to_skip = new_offset_in_clusters - 1; let mut cluster = first_cluster; - let mut iter = self.fs.cluster_iter(first_cluster); + let mut iter = FileSystem::cluster_iter(&self.fs, first_cluster); for i in 0..clusters_to_skip { cluster = if let Some(r) = iter.next() { r? @@ -484,7 +485,7 @@ impl Seek for File<'_, IO, TP, OCC> { } #[cfg(feature = "std")] -impl std::io::Seek for File<'_, IO, TP, OCC> +impl std::io::Seek for File where std::io::Error: From>, { diff --git a/awkernel_lib/src/file/fatfs/fs.rs b/awkernel_lib/src/file/fatfs/fs.rs index 29d8bf017..2a82abe27 100644 --- a/awkernel_lib/src/file/fatfs/fs.rs +++ b/awkernel_lib/src/file/fatfs/fs.rs @@ -1,5 +1,6 @@ #[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::string::String; +use alloc::sync::Arc; use core::borrow::BorrowMut; use core::convert::TryFrom; use core::fmt::Debug; @@ -489,39 +490,42 @@ impl FileSystem { self.bpb.clusters_from_bytes(bytes) } - fn fat_slice(&self) -> impl ReadWriteSeek> + '_ { - let io = FsIoAdapter { fs: self }; - fat_slice(io, &self.bpb) + fn fat_slice(fs: &Arc) -> impl ReadWriteSeek> + '_ { + let io = FsIoAdapter { fs: Arc::clone(fs) }; + fat_slice(io, &fs.bpb) } pub(crate) fn cluster_iter( - &self, + fs: &Arc, cluster: u32, ) -> ClusterIterator> + '_, IO::Error> { - let disk_slice = self.fat_slice(); - ClusterIterator::new(disk_slice, self.fat_type, cluster) + let disk_slice = FileSystem::fat_slice(fs); + ClusterIterator::new(disk_slice, fs.fat_type, cluster) } - pub(crate) fn truncate_cluster_chain(&self, cluster: u32) -> Result<(), Error> { - let mut iter = self.cluster_iter(cluster); + pub(crate) fn truncate_cluster_chain( + fs: &Arc, + cluster: u32, + ) -> Result<(), Error> { + let mut iter = FileSystem::cluster_iter(fs, cluster); let num_free = iter.truncate()?; let mut node = MCSNode::new(); - let mut fs_info_guard = self.fs_info.lock(&mut node); + let mut fs_info_guard = fs.fs_info.lock(&mut node); fs_info_guard.map_free_clusters(|n| n + num_free); Ok(()) } - pub(crate) fn free_cluster_chain(&self, cluster: u32) -> Result<(), Error> { - let mut iter = self.cluster_iter(cluster); + pub(crate) fn free_cluster_chain(fs: &Arc, cluster: u32) -> Result<(), Error> { + let mut iter = FileSystem::cluster_iter(fs, cluster); let num_free = iter.free()?; let mut node = MCSNode::new(); - let mut fs_info_guard = self.fs_info.lock(&mut node); + let mut fs_info_guard = fs.fs_info.lock(&mut node); fs_info_guard.map_free_clusters(|n| n + num_free); Ok(()) } pub(crate) fn alloc_cluster( - &self, + fs: &Arc, prev_cluster: Option, zero: bool, ) -> Result> { @@ -529,27 +533,21 @@ impl FileSystem { let hint; { let mut node = MCSNode::new(); - let fs_info_guard = self.fs_info.lock(&mut node); + let fs_info_guard = fs.fs_info.lock(&mut node); hint = fs_info_guard.next_free_cluster; } let cluster = { - let mut fat = self.fat_slice(); - alloc_cluster( - &mut fat, - self.fat_type, - prev_cluster, - hint, - self.total_clusters, - )? + let mut fat = FileSystem::fat_slice(fs); + alloc_cluster(&mut fat, fs.fat_type, prev_cluster, hint, fs.total_clusters)? }; if zero { let mut node_disk = MCSNode::new(); - let mut disk_guard = self.disk.lock(&mut node_disk); - disk_guard.seek(SeekFrom::Start(self.offset_from_cluster(cluster)))?; - write_zeros(&mut *disk_guard, u64::from(self.cluster_size()))?; + let mut disk_guard = fs.disk.lock(&mut node_disk); + disk_guard.seek(SeekFrom::Start(fs.offset_from_cluster(cluster)))?; + write_zeros(&mut *disk_guard, u64::from(fs.cluster_size()))?; } let mut node = MCSNode::new(); - let mut fs_info_guard = self.fs_info.lock(&mut node); + let mut fs_info_guard = fs.fs_info.lock(&mut node); fs_info_guard.set_next_free_cluster(cluster + 1); fs_info_guard.map_free_clusters(|n| n - 1); Ok(cluster) @@ -560,9 +558,9 @@ impl FileSystem { /// # Errors /// /// `Error::Io` will be returned if the underlying storage object returned an I/O error. - pub fn read_status_flags(&self) -> Result> { - let bpb_status = self.bpb.status_flags(); - let fat_status = read_fat_flags(&mut self.fat_slice(), self.fat_type)?; + pub fn read_status_flags(fs: &Arc) -> Result> { + let bpb_status = fs.bpb.status_flags(); + let fat_status = read_fat_flags(&mut FileSystem::fat_slice(fs), fs.fat_type)?; Ok(FsStatusFlags { dirty: bpb_status.dirty || fat_status.dirty, io_error: bpb_status.io_error || fat_status.io_error, @@ -577,29 +575,29 @@ impl FileSystem { /// # Errors /// /// `Error::Io` will be returned if the underlying storage object returned an I/O error. - pub fn stats(&self) -> Result> { + pub fn stats(fs: &Arc) -> Result> { let mut node = MCSNode::new(); - let fs_info_guard = self.fs_info.lock(&mut node); + let fs_info_guard = fs.fs_info.lock(&mut node); let free_clusters_option = fs_info_guard.free_cluster_count; drop(fs_info_guard); let free_clusters = if let Some(n) = free_clusters_option { n } else { - self.recalc_free_clusters()? + Self::recalc_free_clusters(fs)? }; Ok(FileSystemStats { - cluster_size: self.cluster_size(), - total_clusters: self.total_clusters, + cluster_size: fs.cluster_size(), + total_clusters: fs.total_clusters, free_clusters, }) } /// Forces free clusters recalculation. - fn recalc_free_clusters(&self) -> Result> { - let mut fat = self.fat_slice(); - let free_cluster_count = count_free_clusters(&mut fat, self.fat_type, self.total_clusters)?; + fn recalc_free_clusters(fs: &Arc) -> Result> { + let mut fat = FileSystem::fat_slice(fs); + let free_cluster_count = count_free_clusters(&mut fat, fs.fat_type, fs.total_clusters)?; let mut node = MCSNode::new(); - let mut fs_info_guard = self.fs_info.lock(&mut node); + let mut fs_info_guard = fs.fs_info.lock(&mut node); fs_info_guard.set_free_cluster_count(free_cluster_count); Ok(free_cluster_count) } @@ -677,23 +675,25 @@ impl FileSystem { } /// Returns a root directory object allowing for futher penetration of a filesystem structure. - pub fn root_dir(&self) -> Dir { + pub fn root_dir(fs: &Arc) -> Dir { log::trace!("root_dir"); let root_rdr = { - match self.fat_type { + match fs.fat_type { FatType::Fat12 | FatType::Fat16 => DirRawStream::Root(DiskSlice::from_sectors( - self.first_data_sector - self.root_dir_sectors, - self.root_dir_sectors, + fs.first_data_sector - fs.root_dir_sectors, + fs.root_dir_sectors, 1, - &self.bpb, - FsIoAdapter { fs: self }, + &fs.bpb, + FsIoAdapter { fs: Arc::clone(fs) }, + )), + FatType::Fat32 => DirRawStream::File(File::new( + Some(fs.bpb.root_dir_first_cluster), + None, + Arc::clone(fs), )), - FatType::Fat32 => { - DirRawStream::File(File::new(Some(self.bpb.root_dir_first_cluster), None, self)) - } } }; - Dir::new(root_rdr, self) + Dir::new(root_rdr, Arc::clone(fs)) } } @@ -724,10 +724,12 @@ impl /// /// `Error::Io` will be returned if the underlying storage object returned an I/O error. #[cfg(feature = "alloc")] - pub fn read_volume_label_from_root_dir(&self) -> Result, Error> { + pub fn read_volume_label_from_root_dir( + fs: &Arc, + ) -> Result, Error> { // Note: DirEntry::file_short_name() cannot be used because it interprets name as 8.3 // (adds dot before an extension) - let volume_label_opt = self.read_volume_label_from_root_dir_as_bytes()?; + let volume_label_opt = FileSystem::read_volume_label_from_root_dir_as_bytes(fs)?; volume_label_opt.map_or(Ok(None), |volume_label| { // Strip label padding let len = volume_label @@ -737,7 +739,7 @@ impl let label_slice = &volume_label[..len]; // Decode volume label from OEM codepage let volume_label_iter = label_slice.iter().copied(); - let char_iter = volume_label_iter.map(|c| self.options.oem_cp_converter.decode(c)); + let char_iter = volume_label_iter.map(|c| fs.options.oem_cp_converter.decode(c)); // Build string from character iterator Ok(Some(char_iter.collect::())) }) @@ -752,9 +754,9 @@ impl /// /// `Error::Io` will be returned if the underlying storage object returned an I/O error. pub fn read_volume_label_from_root_dir_as_bytes( - &self, + fs: &Arc, ) -> Result, Error> { - let entry_opt = self.root_dir().find_volume_entry()?; + let entry_opt = Self::root_dir(fs).find_volume_entry()?; Ok(entry_opt.map(|e| *e.raw_short_name())) } } @@ -768,15 +770,15 @@ impl Drop for FileSystem } } -pub(crate) struct FsIoAdapter<'a, IO: ReadWriteSeek + Send + Sync, TP, OCC> { - fs: &'a FileSystem, +pub(crate) struct FsIoAdapter { + fs: Arc>, } -impl IoBase for FsIoAdapter<'_, IO, TP, OCC> { +impl IoBase for FsIoAdapter { type Error = IO::Error; } -impl Read for FsIoAdapter<'_, IO, TP, OCC> { +impl Read for FsIoAdapter { fn read(&mut self, buf: &mut [u8]) -> Result { let mut node = MCSNode::new(); let mut disk_guard = self.fs.disk.lock(&mut node); @@ -784,7 +786,7 @@ impl Read for FsIoAdapter<'_, IO, TP, } } -impl Write for FsIoAdapter<'_, IO, TP, OCC> { +impl Write for FsIoAdapter { fn write(&mut self, buf: &[u8]) -> Result { let mut node = MCSNode::new(); let mut disk_guard = self.fs.disk.lock(&mut node); @@ -803,7 +805,7 @@ impl Write for FsIoAdapter<'_, IO, TP, } } -impl Seek for FsIoAdapter<'_, IO, TP, OCC> { +impl Seek for FsIoAdapter { fn seek(&mut self, pos: SeekFrom) -> Result { let mut node = MCSNode::new(); let mut disk_guard = self.fs.disk.lock(&mut node); @@ -812,9 +814,11 @@ impl Seek for FsIoAdapter<'_, IO, TP, } // Note: derive cannot be used because of invalid bounds. See: https://github.com/rust-lang/rust/issues/26925 -impl Clone for FsIoAdapter<'_, IO, TP, OCC> { +impl Clone for FsIoAdapter { fn clone(&self) -> Self { - FsIoAdapter { fs: self.fs } + FsIoAdapter { + fs: self.fs.clone(), + } } } From 58c3daa29b5710a12b8533f6039e5e385a72ce57 Mon Sep 17 00:00:00 2001 From: Koichi Imai Date: Fri, 13 Jun 2025 20:24:20 +0900 Subject: [PATCH 02/63] add vfs Signed-off-by: Koichi Imai --- awkernel_lib/src/file.rs | 1 + awkernel_lib/src/file/error.rs | 9 + awkernel_lib/src/file/io.rs | 52 + awkernel_lib/src/file/memfs.rs | 3 + awkernel_lib/src/file/vfs.rs | 3 + awkernel_lib/src/file/vfs/LICENSE | 201 +++ awkernel_lib/src/file/vfs/README.md | 143 ++ .../src/file/vfs/async_vfs/filesystem.rs | 79 + .../src/file/vfs/async_vfs/impls/altroot.rs | 156 ++ .../src/file/vfs/async_vfs/impls/memory.rs | 437 +++++ .../src/file/vfs/async_vfs/impls/mod.rs | 6 + .../src/file/vfs/async_vfs/impls/overlay.rs | 447 ++++++ .../src/file/vfs/async_vfs/impls/physical.rs | 312 ++++ awkernel_lib/src/file/vfs/async_vfs/mod.rs | 64 + awkernel_lib/src/file/vfs/async_vfs/path.rs | 1113 +++++++++++++ .../src/file/vfs/async_vfs/test_macros.rs | 1395 ++++++++++++++++ awkernel_lib/src/file/vfs/error.rs | 190 +++ awkernel_lib/src/file/vfs/filesystem.rs | 85 + awkernel_lib/src/file/vfs/impls/altroot.rs | 144 ++ awkernel_lib/src/file/vfs/impls/embedded.rs | 461 ++++++ awkernel_lib/src/file/vfs/impls/memory.rs | 521 ++++++ awkernel_lib/src/file/vfs/impls/mod.rs | 8 + awkernel_lib/src/file/vfs/impls/overlay.rs | 393 +++++ awkernel_lib/src/file/vfs/impls/physical.rs | 243 +++ awkernel_lib/src/file/vfs/path.rs | 1137 +++++++++++++ .../src/file/vfs/test/test_directory/a.txt | 1 + .../vfs/test/test_directory/a.txt.dir/g.txt | 0 .../src/file/vfs/test/test_directory/a/d.txt | 1 + .../src/file/vfs/test/test_directory/a/x/y/z | 1 + .../src/file/vfs/test/test_directory/b.txt | 1 + .../src/file/vfs/test/test_directory/c/e.txt | 1 + awkernel_lib/src/file/vfs/test_macros.rs | 1400 +++++++++++++++++ 32 files changed, 9008 insertions(+) create mode 100644 awkernel_lib/src/file/vfs.rs create mode 100644 awkernel_lib/src/file/vfs/LICENSE create mode 100644 awkernel_lib/src/file/vfs/README.md create mode 100644 awkernel_lib/src/file/vfs/async_vfs/filesystem.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/impls/altroot.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/impls/memory.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/impls/mod.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/impls/overlay.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/impls/physical.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/mod.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/path.rs create mode 100644 awkernel_lib/src/file/vfs/async_vfs/test_macros.rs create mode 100644 awkernel_lib/src/file/vfs/error.rs create mode 100644 awkernel_lib/src/file/vfs/filesystem.rs create mode 100644 awkernel_lib/src/file/vfs/impls/altroot.rs create mode 100644 awkernel_lib/src/file/vfs/impls/embedded.rs create mode 100644 awkernel_lib/src/file/vfs/impls/memory.rs create mode 100644 awkernel_lib/src/file/vfs/impls/mod.rs create mode 100644 awkernel_lib/src/file/vfs/impls/overlay.rs create mode 100644 awkernel_lib/src/file/vfs/impls/physical.rs create mode 100644 awkernel_lib/src/file/vfs/path.rs create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/a.txt create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/a.txt.dir/g.txt create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/a/d.txt create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/a/x/y/z create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/b.txt create mode 100644 awkernel_lib/src/file/vfs/test/test_directory/c/e.txt create mode 100644 awkernel_lib/src/file/vfs/test_macros.rs diff --git a/awkernel_lib/src/file.rs b/awkernel_lib/src/file.rs index 5e50af6e5..3544d8219 100644 --- a/awkernel_lib/src/file.rs +++ b/awkernel_lib/src/file.rs @@ -2,3 +2,4 @@ pub mod error; pub mod fatfs; pub mod io; pub mod memfs; +pub mod vfs; diff --git a/awkernel_lib/src/file/error.rs b/awkernel_lib/src/file/error.rs index d45a0eee3..535bb714a 100644 --- a/awkernel_lib/src/file/error.rs +++ b/awkernel_lib/src/file/error.rs @@ -26,6 +26,7 @@ pub enum Error { InvalidFileNameLength, /// The provided file name contains an invalid character. UnsupportedFileNameCharacter, + Others, } impl From for Error { @@ -68,6 +69,7 @@ impl core::fmt::Display for Error { Error::NotFound => write!(f, "No such file or directory"), Error::AlreadyExists => write!(f, "File or directory already exists"), Error::CorruptedFileSystem => write!(f, "Corrupted file system"), + Error::Others => write!(f, "Other errors"), } } } @@ -90,6 +92,7 @@ pub trait IoError: core::fmt::Debug { fn is_interrupted(&self) -> bool; fn new_unexpected_eof_error() -> Self; fn new_write_zero_error() -> Self; + fn other_error() -> Self; } impl IoError for Error { @@ -107,6 +110,9 @@ impl IoError for Error { fn new_write_zero_error() -> Self { Error::::WriteZero } + fn other_error() -> Self { + Error::::Others + } } impl IoError for () { @@ -121,6 +127,9 @@ impl IoError for () { fn new_write_zero_error() -> Self { // empty } + fn other_error() -> Self { + // empty + } } #[cfg(feature = "std")] diff --git a/awkernel_lib/src/file/io.rs b/awkernel_lib/src/file/io.rs index 47a9136c1..994e980d2 100644 --- a/awkernel_lib/src/file/io.rs +++ b/awkernel_lib/src/file/io.rs @@ -1,4 +1,6 @@ +use super::error::Error; use super::error::IoError; +use alloc::{string::String, vec::Vec}; /// Provides IO error as an associated type. /// @@ -71,6 +73,56 @@ pub trait Read: IoBase { Err(Self::Error::new_unexpected_eof_error()) } } + + fn read_to_string(&mut self, buf: &mut String) -> Result<(), Self::Error> { + let mut bytes = Vec::new(); + self.read_to_end(&mut bytes)?; + + let s = String::from_utf8(bytes).map_err(|e| Self::Error::other_error())?; + buf.push_str(&s); + + Ok(()) + } + + /// Reads all bytes until EOF in this source and appends them to the specified buffer. + /// This is a helper for read_to_string. You might need to implement this one too, + /// similar to read_exact. + fn read_to_end(&mut self, buf: &mut Vec) -> Result { + let mut sum_read = 0; + loop { + // 十分なスペースを確保 + if buf.len() == buf.capacity() { + buf.reserve(32); + } + let prev_len = buf.len(); + unsafe { + buf.set_len(buf.capacity()); + } + match self.read(&mut buf[prev_len..]) { + Ok(0) => { + unsafe { + buf.set_len(prev_len); + } + return Ok(sum_read); + } + Ok(n) => { + sum_read += n; + unsafe { + buf.set_len(prev_len + n); + } + } + Err(ref e) if e.is_interrupted() => unsafe { + buf.set_len(prev_len); + }, + Err(e) => { + unsafe { + buf.set_len(prev_len); + } + return Err(e); + } + } + } + } } /// The `Write` trait allows for writing bytes into the sink. diff --git a/awkernel_lib/src/file/memfs.rs b/awkernel_lib/src/file/memfs.rs index 3636faf30..e29dc14dd 100644 --- a/awkernel_lib/src/file/memfs.rs +++ b/awkernel_lib/src/file/memfs.rs @@ -103,4 +103,7 @@ impl IoError for InMemoryDiskError { fn new_write_zero_error() -> Self { InMemoryDiskError::WriteZero } + fn other_error() -> Self { + todo!(); + } } diff --git a/awkernel_lib/src/file/vfs.rs b/awkernel_lib/src/file/vfs.rs new file mode 100644 index 000000000..d66235f24 --- /dev/null +++ b/awkernel_lib/src/file/vfs.rs @@ -0,0 +1,3 @@ +pub mod error; +pub mod filesystem; +pub mod path; diff --git a/awkernel_lib/src/file/vfs/LICENSE b/awkernel_lib/src/file/vfs/LICENSE new file mode 100644 index 000000000..91deadd10 --- /dev/null +++ b/awkernel_lib/src/file/vfs/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Manuel Woelker + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/awkernel_lib/src/file/vfs/README.md b/awkernel_lib/src/file/vfs/README.md new file mode 100644 index 000000000..e8c40aead --- /dev/null +++ b/awkernel_lib/src/file/vfs/README.md @@ -0,0 +1,143 @@ +# rust-vfs + +[![Crate](https://img.shields.io/crates/v/vfs.svg)](https://crates.io/crates/vfs) +[![API](https://docs.rs/vfs/badge.svg)](https://docs.rs/vfs) +![Minimum rustc version](https://img.shields.io/badge/rustc-1.63.0+-green.svg) +[![Actions Status](https://github.com/manuel-woelker/rust-vfs/workflows/Continuous%20integration/badge.svg)](https://github.com/manuel-woelker/rust-vfs/actions?query=workflow%3A%22Continuous+integration%22) + +A virtual filesystem for Rust + +The virtual file system abstraction generalizes over file systems and allows using +different filesystem implementations (e.g. an in memory implementation for unit tests) + +This crate currently has the following implementations: + * **PhysicalFS** - the actual filesystem of the underlying OS + * **MemoryFS** - an ephemeral in-memory file system, intended mainly for unit tests + * **AltrootFS** - a file system with its root in a particular directory of another filesystem + * **OverlayFS** - an overlay file system combining two filesystems, an upper layer with read/write access and a lower layer with only read access + * **EmbeddedFS** - a read-only file system embedded in the executable, requires `embedded-fs` feature, no async version available + +The minimum supported Rust version (MSRV) is 1.63. + +Comments and pull-requests welcome! + +## Changelog + + +### 0.12.1 (2025-03-24) +* MemoryFS: The `flush()` method now makes the written data available to read calls (fixes [#70](https://github.com/manuel-woelker/rust-vfs/issues/70) - thanks [@krisajenkins](https://github.com/krisajenkins) for the throrough bug report!) + +### 0.12.0 (2024-03-09) +* Allow reading and setting modification/creation/access-times - thanks [@kartonrad](https://github.com/kartonrad)! +* Allow seek when writing - thanks [@jonmclean](https://github.com/jonmclean)! + +### 0.11.0 (2024-02-18) +* Updated minimum supported Rust version to 1.63. +* Updated rust-embed dependency to 8.0 - thanks [@NickAcPT](https://github.com/NickAcPT)! +* Unlocked tokio crate version to work with newer versions - thanks [@Fredrik-Reinholdsen](https://github.com/Fredrik-Reinholdsen)! +* use `Arc` for paths internally to reduce string allocations - thanks [@BrettMayson](https://github.com/BrettMayson)! + +### 0.10.0 (2023-09-08) +* Added async port of the crate, in a new module `async_vfs`. +The module is behind the `async-vfs` feature flag which is not enabled by default. Huge thank you to [@Fredrik Reinholdsen](https://github.com/Fredrik-Reinholdsen)! +* Ported all synchronous tests and doc-tests to async +* Updated minimum supported Rust version to 1.61.0, needed for the async port. +* Updated Rust edition from *2018* to *2021*, needed for the async port. +* Updated Rust versions used in CI pipeline. + +### 0.9.0 (2022-12-20) + +* prevent `Path::create_dir_all()` failures when executing in parallel + (fixes [#47](https://github.com/manuel-woelker/rust-vfs/pull/47)) +* Allow absolute paths (e.g. starting with "/") in `VfsPath::join()` + ([#45](https://github.com/manuel-woelker/rust-vfs/pull/45) - thanks [@Property404](https://github.com/Property404)) +* Allow multiple consecutive slashes in paths + ([#43](https://github.com/manuel-woelker/rust-vfs/pull/43) - thanks [@Property404](https://github.com/Property404)) +* Add method `VfsPath::is_root()` + ([#44](https://github.com/manuel-woelker/rust-vfs/pull/44) - thanks [@Property404](https://github.com/Property404)) +* `Path::join()` now allows resolving '..' at the root (resolving to root itself) + ([#41](https://github.com/manuel-woelker/rust-vfs/pull/41) - thanks [@Property404](https://github.com/Property404)) +* Add `Send` to trait objects returned from APIs + ([#40](https://github.com/manuel-woelker/rust-vfs/pull/40), + [#46](https://github.com/manuel-woelker/rust-vfs/pull/46) - thanks [@Property404](https://github.com/Property404)) + +### 0.8.0 (2022-11-24) + +* Impl `std::error::Error` for `VfsError` ([#32](https://github.com/manuel-woelker/rust-vfs/pull/32)) and improved error + ergonomics for end users ([#34](https://github.com/manuel-woelker/rust-vfs/pull/34)) - thanks [@Technohacker](https://github.com/Technohacker) + +### 0.7.1 (2022-04-15) + +* Fixed a panic when accessing non-existing paths in `MemoryFS::append_file()` (closes + [#31](https://github.com/manuel-woelker/rust-vfs/issues/31)) + +### 0.7.0 (2022-03-26) + +* Update to `EmbeddedFS` to `rust-embed` v6 (closes [#29](https://github.com/manuel-woelker/rust-vfs/issues/29)) +* Make `OverlayFS` and `AltrootFS` available at the crate root, making it more consistent + (PR [#30](https://github.com/manuel-woelker/rust-vfs/issues/30) - + thanks [@Zyian](https://github.com/Zyian)) + +### 0.6.2 (2022-03-07) + +* Activate `embedded-fs` feature when building on docs.rs to ensure that it actually shows up there + ([#28](https://github.com/manuel-woelker/rust-vfs/issues/28) - thanks [@Absolucy](https://github.com/Absolucy)) + +### 0.6.1 (2022-03-06) + +* Added `VfsPath::root()` method to access the root path of a virtual filesystem + (closes [#26](https://github.com/manuel-woelker/rust-vfs/issues/26)) +* Added doctests to `VfsPath` docs to provide usage examples + +### 0.6.0 (2022-03-02) + +* Fixed path inconsistency issues in `EmbeddedFS` (closes [#24](https://github.com/manuel-woelker/rust-vfs/issues/24)) +* Added the test macro `test_vfs_readonly!` which allows verifying read-only filesystem implementations +* Removed dependency on `thiserror` crate to improve compile times +(closes [#25](https://github.com/manuel-woelker/rust-vfs/issues/25)) + +### 0.5.2 (2022-02-07) + +* Removed potential panic in `OverlayFS` (closes [#23](https://github.com/manuel-woelker/rust-vfs/issues/23)) +* `VfsPath::join()` now takes AsRef instead of &str to improve ergonomics with crates like camino + +### 0.5.1 (2021-02-13) + +* Exported `test_vfs` macro via the feature flag `export-test-macros` to allow downstream implementations to verify + expected behaviour +* The MSRV is now 1.40 due to requirements in upstream crates +* The embedded implementation was broken by the 0.5.0 API changes, and is now fixed + +### 0.5.0 (2021-02-13) + +* Added `EmbeddedFS` for using filesystems embeded in the binary using +[rust-embed](https://github.com/pyros2097/rust-embed) +(PR [#12](https://github.com/manuel-woelker/rust-vfs/issues/12) - thanks [@ahouts](https://github.com/ahouts)) +* Changed `VfsPath::exists()` to return `VfsResult` instead of plain `bool` (closes [#17](https://github.com/manuel-woelker/rust-vfs/issues/17)) + +### 0.4.0 (2020-08-13) + + * Added `OverlayFS` union filesystem + * Added `VfsPath::read_to_string()` convenience method + * Added `VfsPath::walk_dir()` method for recursive directory traversal + * Added `VfsPath::{copy,move}_{file,dir}()` methods (closes [#9](https://github.com/manuel-woelker/rust-vfs/issues/9)) + * License is now Apache 2.0 + * Minimum supported Rust version (MSRV) is 1.32.0 + +### 0.3.0 (2020-08-04) + + * Refactored to use a trait based design, simplifying usage and testing + +### 0.2.1 (2020-02-06) + + * Added `AltrootFS` (thanks [@icefoxen](https://github.com/icefoxen)) + +### 0.1.0 (2016-05-14) + + * Initial release + +## Roadmap + + * Support for read-only filesystems + * Support for re-mounting filesystems + * Support for virtual filesystem access inside archives (e.g. zip) diff --git a/awkernel_lib/src/file/vfs/async_vfs/filesystem.rs b/awkernel_lib/src/file/vfs/async_vfs/filesystem.rs new file mode 100644 index 000000000..d474d01d7 --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/filesystem.rs @@ -0,0 +1,79 @@ +//! The async filesystem trait definitions needed to implement new async virtual filesystems + +use crate::async_vfs::{AsyncVfsPath, SeekAndRead}; +use crate::error::VfsErrorKind; +use crate::{VfsError, VfsMetadata, VfsResult}; + +use async_std::io::Write; +use async_std::stream::Stream; +use async_trait::async_trait; +use core::fmt::Debug; +use std::time::SystemTime; + +/// File system implementations must implement this trait +/// All path parameters are absolute, starting with '/', except for the root directory +/// which is simply the empty string (i.e. "") +/// The character '/' is used to delimit directories on all platforms. +/// Path components may be any UTF-8 string, except "/", "." and ".." +/// +/// Please use the test_macros [test_macros::test_async_vfs!] and [test_macros::test_async_vfs_readonly!] +#[async_trait] +pub trait AsyncFileSystem: Debug + Sync + Send + 'static { + /// Iterates over all direct children of this directory path + /// NOTE: the returned String items denote the local bare filenames, i.e. they should not contain "/" anywhere + async fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send>>; + /// Creates the directory at this path + /// + /// Note that the parent directory must already exist. + async fn create_dir(&self, path: &str) -> VfsResult<()>; + /// Opens the file at this path for reading + async fn open_file( + &self, + path: &str, + ) -> VfsResult + Send + Unpin>>; + /// Creates a file at this path for writing + async fn create_file(&self, path: &str) -> VfsResult>; + /// Opens the file at this path for appending + async fn append_file(&self, path: &str) -> VfsResult>; + /// Returns the file metadata for the file at this path + async fn metadata(&self, path: &str) -> VfsResult; + /// Sets the files creation timestamp, if the implementation supports it + async fn set_creation_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Sets the files modification timestamp, if the implementation supports it + async fn set_modification_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Sets the files access timestamp, if the implementation supports it + async fn set_access_time(&self, _path: &str, _time: SystemTime) -> VfsResult<()> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Returns true if a file or directory at path exists, false otherwise + async fn exists(&self, path: &str) -> VfsResult; + /// Removes the file at this path + async fn remove_file(&self, path: &str) -> VfsResult<()>; + /// Removes the directory at this path + async fn remove_dir(&self, path: &str) -> VfsResult<()>; + /// Copies the src path to the destination path within the same filesystem (optional) + async fn copy_file(&self, _src: &str, _dest: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + /// Moves the src path to the destination path within the same filesystem (optional) + async fn move_file(&self, _src: &str, _dest: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + /// Moves the src directory to the destination path within the same filesystem (optional) + async fn move_dir(&self, _src: &str, _dest: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } +} + +impl From for AsyncVfsPath { + fn from(filesystem: T) -> Self { + AsyncVfsPath::new(filesystem) + } +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/impls/altroot.rs b/awkernel_lib/src/file/vfs/async_vfs/impls/altroot.rs new file mode 100644 index 000000000..471361fd2 --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/impls/altroot.rs @@ -0,0 +1,156 @@ +//! A file system with its root in a particular directory of another filesystem + +use crate::async_vfs::{AsyncFileSystem, AsyncVfsPath, SeekAndRead}; +use crate::{error::VfsErrorKind, VfsMetadata, VfsResult}; +use std::time::SystemTime; + +use async_std::io::Write; +use async_trait::async_trait; +use futures::stream::{Stream, StreamExt}; + +/// Similar to a chroot but done purely by path manipulation +/// +/// NOTE: This mechanism should only be used for convenience, NOT FOR SECURITY +/// +/// Symlinks, hardlinks, remounts, side channels and other file system mechanisms can be exploited +/// to circumvent this mechanism +#[derive(Debug, Clone)] +pub struct AsyncAltrootFS { + root: AsyncVfsPath, +} + +impl AsyncAltrootFS { + /// Create a new root FileSystem at the given virtual path + pub fn new(root: AsyncVfsPath) -> Self { + AsyncAltrootFS { root } + } +} + +impl AsyncAltrootFS { + #[allow(clippy::manual_strip)] // strip prefix manually for MSRV 1.32 + fn path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.root.clone()); + } + if path.starts_with('/') { + return self.root.join(&path[1..]); + } + self.root.join(path) + } +} + +#[async_trait] +impl AsyncFileSystem for AsyncAltrootFS { + async fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send + Unpin>> { + self.path(path)? + .read_dir() + .await + .map(|result| result.map(|path| path.filename())) + .map(|entries| Box::new(entries) as Box + Send + Unpin>) + } + + async fn create_dir(&self, path: &str) -> VfsResult<()> { + self.path(path)?.create_dir().await + } + + async fn open_file(&self, path: &str) -> VfsResult + Send + Unpin>> { + self.path(path)?.open_file().await + } + + async fn create_file(&self, path: &str) -> VfsResult> { + self.path(path)?.create_file().await + } + + async fn append_file(&self, path: &str) -> VfsResult> { + self.path(path)?.append_file().await + } + + async fn metadata(&self, path: &str) -> VfsResult { + self.path(path)?.metadata().await + } + + async fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_creation_time(time).await + } + + async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_modification_time(time).await + } + + async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_access_time(time).await + } + + async fn exists(&self, path: &str) -> VfsResult { + match self.path(path) { + Ok(p) => p.exists().await, + Err(_) => Ok(false), + } + } + + async fn remove_file(&self, path: &str) -> VfsResult<()> { + self.path(path)?.remove_file().await + } + + async fn remove_dir(&self, path: &str) -> VfsResult<()> { + self.path(path)?.remove_dir().await + } + + async fn copy_file(&self, src: &str, dest: &str) -> VfsResult<()> { + if dest.is_empty() { + return Err(VfsErrorKind::NotSupported.into()); + } + self.path(src)?.copy_file(&self.path(dest)?).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::async_vfs::AsyncMemoryFS; + + test_async_vfs!(futures::executor::block_on(async { + let memory_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + let altroot_path = memory_root.join("altroot").unwrap(); + altroot_path.create_dir().await.unwrap(); + AsyncAltrootFS::new(altroot_path) + })); + + #[tokio::test] + async fn parent() { + let memory_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + let altroot_path = memory_root.join("altroot").unwrap(); + altroot_path.create_dir().await.unwrap(); + let altroot: AsyncVfsPath = AsyncAltrootFS::new(altroot_path.clone()).into(); + assert_eq!(altroot.parent(), altroot.root()); + assert_eq!(altroot_path.parent(), memory_root); + } +} + +#[cfg(test)] +mod tests_physical { + use super::*; + use crate::async_vfs::AsyncPhysicalFS; + + use async_std::io::ReadExt; + + test_async_vfs!(futures::executor::block_on(async { + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + std::fs::create_dir_all(&dir).unwrap(); + + let physical_root: AsyncVfsPath = AsyncPhysicalFS::new(dir).into(); + let altroot_path = physical_root.join("altroot").unwrap(); + altroot_path.create_dir().await.unwrap(); + AsyncAltrootFS::new(altroot_path) + })); + + test_async_vfs_readonly!({ + let physical_root: AsyncVfsPath = AsyncPhysicalFS::new("test").into(); + let altroot_path = physical_root.join("test_directory").unwrap(); + AsyncAltrootFS::new(altroot_path) + }); +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/impls/memory.rs b/awkernel_lib/src/file/vfs/async_vfs/impls/memory.rs new file mode 100644 index 000000000..ac696ad00 --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/impls/memory.rs @@ -0,0 +1,437 @@ +//! An ephemeral in-memory file system, intended mainly for unit tests +use crate::async_vfs::{AsyncFileSystem, SeekAndRead}; +use crate::error::VfsErrorKind; +use crate::path::VfsFileType; +use crate::{VfsMetadata, VfsResult}; + +use async_std::io::{prelude::SeekExt, Cursor, Read, Seek, SeekFrom, Write}; +use async_std::sync::{Arc, RwLock}; +use async_trait::async_trait; +use futures::task::{Context, Poll}; +use futures::{Stream, StreamExt}; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::mem::swap; +use std::pin::Pin; + +type AsyncMemoryFsHandle = Arc>; + +/// An ephemeral in-memory file system, intended mainly for unit tests +pub struct AsyncMemoryFS { + handle: AsyncMemoryFsHandle, +} + +impl Debug for AsyncMemoryFS { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("In Memory File System") + } +} + +impl AsyncMemoryFS { + /// Create a new in-memory filesystem + pub fn new() -> Self { + AsyncMemoryFS { + handle: Arc::new(RwLock::new(AsyncMemoryFsImpl::new())), + } + } + + async fn ensure_has_parent(&self, path: &str) -> VfsResult<()> { + let separator = path.rfind('/'); + if let Some(index) = separator { + if self.exists(&path[..index]).await? { + return Ok(()); + } + } + Err(VfsErrorKind::Other("Parent path does not exist".into()).into()) + } +} + +impl Default for AsyncMemoryFS { + fn default() -> Self { + Self::new() + } +} + +struct AsyncWritableFile { + content: Cursor>, + destination: String, + fs: AsyncMemoryFsHandle, +} + +impl Write for AsyncWritableFile { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + let file = Pin::new(&mut this.content); + file.poll_write(cx, buf) + } + // Flush any bytes left in the write buffer to the virtual file + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + let file = Pin::new(&mut this.content); + file.poll_flush(cx) + } + fn poll_close( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + let this = self.get_mut(); + let file = Pin::new(&mut this.content); + file.poll_close(cx) + } +} + +impl Drop for AsyncWritableFile { + fn drop(&mut self) { + let mut content = vec![]; + swap(&mut content, self.content.get_mut()); + futures::executor::block_on(self.fs.write()).files.insert( + self.destination.clone(), + AsyncMemoryFile { + file_type: VfsFileType::File, + content: Arc::new(content), + }, + ); + } +} + +struct AsyncReadableFile { + #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable + content: Arc>, + // Position of the read cursor in the "file" + cursor_pos: u64, +} + +impl AsyncReadableFile { + fn len(&self) -> u64 { + self.content.len() as u64 + } +} + +impl Read for AsyncReadableFile { + fn poll_read( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let this = self.get_mut(); + let bytes_left = this.len() - this.cursor_pos; + let bytes_read = std::cmp::min(buf.len() as u64, bytes_left); + if bytes_left == 0 { + return Poll::Ready(Ok(0)); + } + buf[..bytes_read as usize].copy_from_slice( + &this.content[this.cursor_pos as usize..(this.cursor_pos + bytes_read) as usize], + ); + this.cursor_pos += bytes_read; + Poll::Ready(Ok(bytes_read as usize)) + } +} + +impl Seek for AsyncReadableFile { + fn poll_seek( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + pos: SeekFrom, + ) -> Poll> { + let this = self.get_mut(); + let new_pos = match pos { + SeekFrom::Start(offset) => offset as i64, + SeekFrom::End(offset) => this.cursor_pos as i64 - offset, + SeekFrom::Current(offset) => this.cursor_pos as i64 + offset, + }; + if new_pos < 0 || new_pos >= this.len() as i64 { + Poll::Ready(Err(async_std::io::Error::new( + async_std::io::ErrorKind::InvalidData, + "Requested offset is outside the file!", + ))) + } else { + this.cursor_pos = new_pos as u64; + Poll::Ready(Ok(new_pos as u64)) + } + } +} + +#[async_trait] +impl AsyncFileSystem for AsyncMemoryFS { + async fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send>> { + let prefix = format!("{}/", path); + let handle = self.handle.read().await; + let mut found_directory = false; + #[allow(clippy::needless_collect)] // need collect to satisfy lifetime requirements + let entries: Vec = handle + .files + .iter() + .filter_map(|(candidate_path, _)| { + if candidate_path == path { + found_directory = true; + } + if candidate_path.starts_with(&prefix) { + let rest = &candidate_path[prefix.len()..]; + if !rest.contains('/') { + return Some(rest.to_string()); + } + } + None + }) + .collect(); + if !found_directory { + return Err(VfsErrorKind::FileNotFound.into()); + } + Ok(Box::new(futures::stream::iter(entries))) + } + + async fn create_dir(&self, path: &str) -> VfsResult<()> { + self.ensure_has_parent(path).await?; + let map = &mut self.handle.write().await.files; + let entry = map.entry(path.to_string()); + match entry { + Entry::Occupied(file) => { + return match file.get().file_type { + VfsFileType::File => Err(VfsErrorKind::FileExists.into()), + VfsFileType::Directory => Err(VfsErrorKind::DirectoryExists.into()), + } + } + Entry::Vacant(_) => { + map.insert( + path.to_string(), + AsyncMemoryFile { + file_type: VfsFileType::Directory, + content: Default::default(), + }, + ); + } + } + Ok(()) + } + + async fn open_file(&self, path: &str) -> VfsResult + Send + Unpin>> { + let handle = self.handle.read().await; + let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + ensure_file(file)?; + Ok(Box::new(AsyncReadableFile { + content: file.content.clone(), + cursor_pos: 0, + })) + } + + async fn create_file(&self, path: &str) -> VfsResult> { + self.ensure_has_parent(path).await?; + let content = Arc::new(Vec::::new()); + self.handle.write().await.files.insert( + path.to_string(), + AsyncMemoryFile { + file_type: VfsFileType::File, + content, + }, + ); + let writer = AsyncWritableFile { + content: Cursor::new(vec![]), + destination: path.to_string(), + fs: self.handle.clone(), + }; + Ok(Box::new(writer)) + } + + async fn append_file(&self, path: &str) -> VfsResult> { + let handle = self.handle.write().await; + let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + let mut content = Cursor::new(file.content.as_ref().clone()); + content.seek(SeekFrom::End(0)).await?; + let writer = AsyncWritableFile { + content, + destination: path.to_string(), + fs: self.handle.clone(), + }; + Ok(Box::new(writer)) + } + + async fn metadata(&self, path: &str) -> VfsResult { + let guard = self.handle.read().await; + let files = &guard.files; + let file = files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + Ok(VfsMetadata { + file_type: file.file_type, + len: file.content.len() as u64, + modified: None, + created: None, + accessed: None, + }) + } + + async fn exists(&self, path: &str) -> VfsResult { + Ok(self.handle.read().await.files.contains_key(path)) + } + + async fn remove_file(&self, path: &str) -> VfsResult<()> { + let mut handle = self.handle.write().await; + handle + .files + .remove(path) + .ok_or(VfsErrorKind::FileNotFound)?; + Ok(()) + } + + async fn remove_dir(&self, path: &str) -> VfsResult<()> { + if self.read_dir(path).await?.next().await.is_some() { + return Err(VfsErrorKind::Other("Directory to remove is not empty".into()).into()); + } + let mut handle = self.handle.write().await; + handle + .files + .remove(path) + .ok_or(VfsErrorKind::FileNotFound)?; + Ok(()) + } +} + +#[derive(Debug)] +struct AsyncMemoryFsImpl { + files: HashMap, +} + +impl AsyncMemoryFsImpl { + pub fn new() -> Self { + let mut files = HashMap::new(); + // Add root directory + files.insert( + "".to_string(), + AsyncMemoryFile { + file_type: VfsFileType::Directory, + content: Arc::new(vec![]), + }, + ); + Self { files } + } +} + +#[derive(Debug)] +struct AsyncMemoryFile { + file_type: VfsFileType, + #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable + content: Arc>, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::async_vfs::AsyncVfsPath; + use async_std::io::{ReadExt, WriteExt}; + test_async_vfs!(AsyncMemoryFS::new()); + + #[tokio::test] + async fn write_and_read_file() -> VfsResult<()> { + let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + let path = root.join("foobar.txt").unwrap(); + let _send = &path as &dyn Send; + { + let mut file = path.create_file().await.unwrap(); + write!(file, "Hello world").await.unwrap(); + write!(file, "!").await.unwrap(); + } + { + let mut file = path.open_file().await.unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).await.unwrap(); + assert_eq!(string, "Hello world!"); + } + assert!(path.exists().await?); + assert!(!root.join("foo").unwrap().exists().await?); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.len, 12); + assert_eq!(metadata.file_type, VfsFileType::File); + Ok(()) + } + + #[tokio::test] + async fn append_file() { + let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + let _string = String::new(); + let path = root.join("test_append.txt").unwrap(); + path.create_file() + .await + .unwrap() + .write_all(b"Testing 1") + .await + .unwrap(); + path.append_file() + .await + .unwrap() + .write_all(b"Testing 2") + .await + .unwrap(); + { + let mut file = path.open_file().await.unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).await.unwrap(); + assert_eq!(string, "Testing 1Testing 2"); + } + } + + #[tokio::test] + async fn create_dir() { + let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + let _string = String::new(); + let path = root.join("foo").unwrap(); + path.create_dir().await.unwrap(); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.file_type, VfsFileType::Directory); + } + + #[tokio::test] + async fn remove_dir_error_message() { + let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + let path = root.join("foo").unwrap(); + let result = path.remove_dir().await; + assert_eq!( + format!("{}", result.unwrap_err()), + "Could not remove directory for '/foo': The file or directory could not be found" + ); + } + + #[tokio::test] + async fn read_dir_error_message() { + let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + let path = root.join("foo").unwrap(); + let result = path.read_dir().await; + match result { + Ok(_) => panic!("Error expected"), + Err(err) => { + assert_eq!( + format!("{}", err), + "Could not read directory for '/foo': The file or directory could not be found" + ); + } + } + } + + #[tokio::test] + async fn copy_file_across_filesystems() -> VfsResult<()> { + let root_a = AsyncVfsPath::new(AsyncMemoryFS::new()); + let root_b = AsyncVfsPath::new(AsyncMemoryFS::new()); + let src = root_a.join("a.txt")?; + let dest = root_b.join("b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + src.copy_file(&dest).await?; + assert_eq!(&dest.read_to_string().await?, "Hello World"); + Ok(()) + } +} + +fn ensure_file(file: &AsyncMemoryFile) -> VfsResult<()> { + if file.file_type != VfsFileType::File { + return Err(VfsErrorKind::Other("Not a file".into()).into()); + } + Ok(()) +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/impls/mod.rs b/awkernel_lib/src/file/vfs/async_vfs/impls/mod.rs new file mode 100644 index 000000000..39d646b0a --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/impls/mod.rs @@ -0,0 +1,6 @@ +//! Async Virtual filesystem implementations + +pub mod altroot; +pub mod memory; +pub mod overlay; +pub mod physical; diff --git a/awkernel_lib/src/file/vfs/async_vfs/impls/overlay.rs b/awkernel_lib/src/file/vfs/async_vfs/impls/overlay.rs new file mode 100644 index 000000000..06b0d90cf --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/impls/overlay.rs @@ -0,0 +1,447 @@ +//! An overlay file system combining two filesystems, an upper layer with read/write access and a lower layer with only read access + +use crate::async_vfs::{AsyncFileSystem, AsyncVfsPath, SeekAndRead}; +use crate::error::VfsErrorKind; +use crate::{VfsMetadata, VfsResult}; + +use async_std::io::Write; +use async_trait::async_trait; +use futures::stream::{Stream, StreamExt}; +use std::collections::HashSet; +use std::time::SystemTime; + +/// An overlay file system combining several filesystems into one, an upper layer with read/write access and lower layers with only read access +/// +/// Files in upper layers shadow those in lower layers. Directories are the merged view of all layers. +/// +/// NOTE: To allow removing files and directories (e.g. via remove_file()) from the lower layer filesystems, this mechanism creates a `.whiteout` folder in the root of the upper level filesystem to mark removed files +/// +#[derive(Debug, Clone)] +pub struct AsyncOverlayFS { + layers: Vec, +} + +impl AsyncOverlayFS { + /// Create a new overlay FileSystem from the given layers, only the first layer is written to + pub fn new(layers: &[AsyncVfsPath]) -> Self { + if layers.is_empty() { + panic!("AsyncOverlayFS needs at least one layer") + } + AsyncOverlayFS { + layers: layers.to_vec(), + } + } + + fn write_layer(&self) -> &AsyncVfsPath { + &self.layers[0] + } + + async fn read_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.layers[0].clone()); + } + if self.whiteout_path(path)?.exists().await? { + return Err(VfsErrorKind::FileNotFound.into()); + } + for layer in &self.layers { + let layer_path = layer.join(&path[1..])?; + if layer_path.exists().await? { + return Ok(layer_path); + } + } + let read_path = self.write_layer().join(&path[1..])?; + if !read_path.exists().await? { + return Err(VfsErrorKind::FileNotFound.into()); + } + Ok(read_path) + } + + fn write_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.layers[0].clone()); + } + self.write_layer().join(&path[1..]) + } + + fn whiteout_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return self.write_layer().join(".whiteout/_wo"); + } + self.write_layer() + .join(format!(".whiteout/{}_wo", &path[1..])) + } + + async fn ensure_has_parent(&self, path: &str) -> VfsResult<()> { + let separator = path.rfind('/'); + if let Some(index) = separator { + let parent_path = &path[..index]; + if self.exists(parent_path).await? { + self.write_path(parent_path)?.create_dir_all().await?; + return Ok(()); + } + } + Err(VfsErrorKind::Other("Parent path does not exist".into()).into()) + } +} + +#[async_trait] +impl AsyncFileSystem for AsyncOverlayFS { + async fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send + Unpin>> { + let actual_path = if !path.is_empty() { &path[1..] } else { path }; + if !self.read_path(path).await?.exists().await? { + return Err(VfsErrorKind::FileNotFound.into()); + } + let mut entries = HashSet::::new(); + for layer in &self.layers { + let layer_path = layer.join(actual_path)?; + if layer_path.exists().await? { + let mut path_stream = layer_path.read_dir().await?; + while let Some(path) = path_stream.next().await { + entries.insert(path.filename()); + } + } + } + // remove whiteout entries that have been removed + let whiteout_path = self.write_layer().join(format!(".whiteout{}", path))?; + if whiteout_path.exists().await? { + let mut path_stream = whiteout_path.read_dir().await?; + while let Some(path) = path_stream.next().await { + let filename = path.filename(); + if filename.ends_with("_wo") { + entries.remove(&filename[..filename.len() - 3]); + } + } + } + Ok(Box::new(futures::stream::iter(entries))) + } + + async fn create_dir(&self, path: &str) -> VfsResult<()> { + self.ensure_has_parent(path).await?; + self.write_path(path)?.create_dir().await?; + let whiteout_path = self.whiteout_path(path)?; + if whiteout_path.exists().await? { + whiteout_path.remove_file().await?; + } + Ok(()) + } + + async fn open_file(&self, path: &str) -> VfsResult + Send + Unpin>> { + self.read_path(path).await?.open_file().await + } + + async fn create_file(&self, path: &str) -> VfsResult> { + self.ensure_has_parent(path).await?; + let result = self.write_path(path)?.create_file().await?; + let whiteout_path = self.whiteout_path(path)?; + if whiteout_path.exists().await? { + whiteout_path.remove_file().await?; + } + Ok(result) + } + + async fn append_file(&self, path: &str) -> VfsResult> { + let write_path = self.write_path(path)?; + if !write_path.exists().await? { + self.ensure_has_parent(path).await?; + self.read_path(path).await?.copy_file(&write_path).await?; + } + write_path.append_file().await + } + + async fn metadata(&self, path: &str) -> VfsResult { + self.read_path(path).await?.metadata().await + } + + async fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_creation_time(time).await + } + + async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_modification_time(time).await + } + + async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_access_time(time).await + } + + async fn exists(&self, path: &str) -> VfsResult { + if self + .whiteout_path(path) + .map_err(|err| err.with_context(|| "whiteout_path"))? + .exists() + .await? + { + return Ok(false); + } + match self.read_path(path).await { + Ok(p) => p.exists().await, + Err(_) => Ok(false), + } + } + + async fn remove_file(&self, path: &str) -> VfsResult<()> { + // Ensure path exists + self.read_path(path).await?; + let write_path = self.write_path(path)?; + if write_path.exists().await? { + write_path.remove_file().await?; + } + let whiteout_path = self.whiteout_path(path)?; + whiteout_path.parent().create_dir_all().await?; + whiteout_path.create_file().await?; + Ok(()) + } + + async fn remove_dir(&self, path: &str) -> VfsResult<()> { + // Ensure path exists + self.read_path(path).await?; + let write_path = self.write_path(path)?; + if write_path.exists().await? { + write_path.remove_dir().await?; + } + let whiteout_path = self.whiteout_path(path)?; + whiteout_path.parent().create_dir_all().await?; + whiteout_path.create_file().await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::async_vfs::AsyncMemoryFS; + + use async_std::io::WriteExt; + use futures::stream::StreamExt; + + test_async_vfs!({ + let upper_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + let lower_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + AsyncOverlayFS::new(&[upper_root, lower_root]) + }); + + fn create_roots() -> (AsyncVfsPath, AsyncVfsPath, AsyncVfsPath) { + let lower_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + let upper_root: AsyncVfsPath = AsyncMemoryFS::new().into(); + let overlay_root: AsyncVfsPath = + AsyncOverlayFS::new(&[upper_root.clone(), lower_root.clone()]).into(); + (lower_root, upper_root, overlay_root) + } + + #[tokio::test] + async fn read() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + let lower_path = lower_root.join("foo.txt")?; + let upper_path = upper_root.join("foo.txt")?; + let overlay_path = overlay_root.join("foo.txt")?; + lower_path + .create_file() + .await? + .write_all(b"Hello Lower") + .await?; + assert_eq!(&overlay_path.read_to_string().await?, "Hello Lower"); + upper_path + .create_file() + .await? + .write_all(b"Hello Upper") + .await?; + assert_eq!(&overlay_path.read_to_string().await?, "Hello Upper"); + lower_path.remove_file().await?; + assert_eq!(&overlay_path.read_to_string().await?, "Hello Upper"); + upper_path.remove_file().await?; + assert!( + !overlay_path.exists().await?, + "File should not exist anymore" + ); + Ok(()) + } + + #[tokio::test] + async fn read_dir() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + upper_root.join("foo/upper")?.create_dir_all().await?; + upper_root.join("foo/common")?.create_dir_all().await?; + lower_root.join("foo/common")?.create_dir_all().await?; + lower_root.join("foo/lower")?.create_dir_all().await?; + let entries: Vec<_> = overlay_root.join("foo")?.read_dir().await?.collect().await; + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/foo/common", "/foo/lower", "/foo/upper"]); + Ok(()) + } + + #[tokio::test] + async fn read_dir_root() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + upper_root.join("upper")?.create_dir_all().await?; + upper_root.join("common")?.create_dir_all().await?; + lower_root.join("common")?.create_dir_all().await?; + lower_root.join("lower")?.create_dir_all().await?; + let entries: Vec<_> = overlay_root.read_dir().await?.collect().await; + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/common", "/lower", "/upper"]); + Ok(()) + } + + #[tokio::test] + async fn create_dir() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + assert!( + overlay_root.join("foo")?.exists().await?, + "dir should exist" + ); + overlay_root.join("foo/bar")?.create_dir().await?; + assert!( + overlay_root.join("foo/bar")?.exists().await?, + "dir should exist" + ); + Ok(()) + } + + #[tokio::test] + async fn create_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + assert!( + overlay_root.join("foo")?.exists().await?, + "dir should exist" + ); + overlay_root.join("foo/bar")?.create_file().await?; + assert!( + overlay_root.join("foo/bar")?.exists().await?, + "file should exist" + ); + Ok(()) + } + + #[tokio::test] + async fn append_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + lower_root + .join("foo/bar.txt")? + .create_file() + .await? + .write_all(b"Hello Lower\n") + .await?; + overlay_root + .join("foo/bar.txt")? + .append_file() + .await? + .write_all(b"Hello Overlay\n") + .await?; + assert_eq!( + &overlay_root.join("foo/bar.txt")?.read_to_string().await?, + "Hello Lower\nHello Overlay\n" + ); + Ok(()) + } + + #[tokio::test] + async fn remove_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + lower_root + .join("foo/bar.txt")? + .create_file() + .await? + .write_all(b"Hello Lower\n") + .await?; + assert!( + overlay_root.join("foo/bar.txt")?.exists().await?, + "file should exist" + ); + + overlay_root.join("foo/bar.txt")?.remove_file().await?; + assert!( + !overlay_root.join("foo/bar.txt")?.exists().await?, + "file should not exist anymore" + ); + + overlay_root + .join("foo/bar.txt")? + .create_file() + .await? + .write_all(b"Hello Overlay\n") + .await?; + assert!( + overlay_root.join("foo/bar.txt")?.exists().await?, + "file should exist" + ); + assert_eq!( + &overlay_root.join("foo/bar.txt")?.read_to_string().await?, + "Hello Overlay\n" + ); + Ok(()) + } + + #[tokio::test] + async fn remove_dir() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + lower_root.join("foo/bar")?.create_dir_all().await?; + assert!( + overlay_root.join("foo/bar")?.exists().await?, + "dir should exist" + ); + + overlay_root.join("foo/bar")?.remove_dir().await?; + assert!( + !overlay_root.join("foo/bar")?.exists().await?, + "dir should not exist anymore" + ); + + overlay_root.join("foo/bar")?.create_dir().await?; + assert!( + overlay_root.join("foo/bar")?.exists().await?, + "dir should exist" + ); + Ok(()) + } + + #[tokio::test] + async fn read_dir_removed_entries() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all().await?; + lower_root.join("foo/bar")?.create_dir_all().await?; + lower_root.join("foo/bar.txt")?.create_dir_all().await?; + + let entries: Vec<_> = overlay_root.join("foo")?.read_dir().await?.collect().await; + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/foo/bar", "/foo/bar.txt"]); + overlay_root.join("foo/bar")?.remove_dir().await?; + overlay_root.join("foo/bar.txt")?.remove_file().await?; + + let entries: Vec<_> = overlay_root.join("foo")?.read_dir().await?.collect().await; + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec![] as Vec<&str>); + + Ok(()) + } +} + +#[cfg(test)] +mod tests_physical { + use super::*; + use crate::async_vfs::AsyncPhysicalFS; + + test_async_vfs!(futures::executor::block_on(async { + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + let lower_path = dir.join("lower"); + async_std::fs::create_dir_all(&lower_path).await.unwrap(); + let upper_path = dir.join("upper"); + async_std::fs::create_dir_all(&upper_path).await.unwrap(); + + let upper_root: AsyncVfsPath = AsyncPhysicalFS::new(upper_path).into(); + let lower_root: AsyncVfsPath = AsyncPhysicalFS::new(lower_path).into(); + AsyncOverlayFS::new(&[upper_root, lower_root]) + })); +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/impls/physical.rs b/awkernel_lib/src/file/vfs/async_vfs/impls/physical.rs new file mode 100644 index 000000000..43e555654 --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/impls/physical.rs @@ -0,0 +1,312 @@ +//! An async implementation of a "physical" file system implementation using the underlying OS file system +use crate::async_vfs::{AsyncFileSystem, SeekAndRead}; +use crate::error::VfsErrorKind; +use crate::path::VfsFileType; +use crate::{VfsError, VfsMetadata, VfsResult}; + +use async_std::fs::{File, OpenOptions}; +use async_std::io::{ErrorKind, Write}; +use async_std::path::{Path, PathBuf}; +use async_trait::async_trait; +use filetime::FileTime; +use futures::stream::{Stream, StreamExt}; +use std::pin::Pin; +use std::time::SystemTime; +use tokio::runtime::Handle; + +/// A physical filesystem implementation using the underlying OS file system +#[derive(Debug)] +pub struct AsyncPhysicalFS { + root: Pin, +} + +impl AsyncPhysicalFS { + /// Create a new physical filesystem rooted in `root` + pub fn new>(root: T) -> Self { + AsyncPhysicalFS { + root: Pin::new(root.as_ref().to_path_buf()), + } + } + + fn get_path(&self, mut path: &str) -> PathBuf { + if path.starts_with('/') { + path = &path[1..]; + } + self.root.join(path) + } +} + +/// Runs normal blocking io on a tokio thread. +/// Requires a tokio runtime. +async fn blocking_io(f: F) -> Result<(), VfsError> +where + F: FnOnce() -> std::io::Result<()> + Send + 'static, +{ + if Handle::try_current().is_ok() { + let result = tokio::task::spawn_blocking(f).await; + + match result { + Ok(val) => val, + Err(err) => { + return Err(VfsError::from(VfsErrorKind::Other(format!( + "Tokio Concurrency Error: {}", + err + )))); + } + }?; + + Ok(()) + } else { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } +} + +#[async_trait] +impl AsyncFileSystem for AsyncPhysicalFS { + async fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send>> { + let entries = Box::new( + self.get_path(path) + .read_dir() + .await? + .map(|entry| entry.unwrap().file_name().into_string().unwrap()), + ); + Ok(entries) + } + + async fn create_dir(&self, path: &str) -> VfsResult<()> { + let fs_path = self.get_path(path); + match async_std::fs::create_dir(&fs_path).await { + Ok(()) => Ok(()), + Err(e) => match e.kind() { + ErrorKind::AlreadyExists => { + let metadata = async_std::fs::metadata(&fs_path).await.unwrap(); + if metadata.is_dir() { + return Err(VfsError::from(VfsErrorKind::DirectoryExists)); + } + Err(VfsError::from(VfsErrorKind::FileExists)) + } + _ => Err(e.into()), + }, + } + } + + async fn open_file(&self, path: &str) -> VfsResult + Send + Unpin>> { + Ok(Box::new(File::open(self.get_path(path)).await?)) + } + + async fn create_file(&self, path: &str) -> VfsResult> { + Ok(Box::new(File::create(self.get_path(path)).await?)) + } + + async fn append_file(&self, path: &str) -> VfsResult> { + Ok(Box::new( + OpenOptions::new() + .write(true) + .append(true) + .open(self.get_path(path)) + .await?, + )) + } + + async fn metadata(&self, path: &str) -> VfsResult { + let metadata = self.get_path(path).metadata().await?; + Ok(if metadata.is_dir() { + VfsMetadata { + file_type: VfsFileType::Directory, + len: 0, + modified: metadata.modified().ok(), + created: metadata.created().ok(), + accessed: metadata.accessed().ok(), + } + } else { + VfsMetadata { + file_type: VfsFileType::File, + len: metadata.len(), + modified: metadata.modified().ok(), + created: metadata.created().ok(), + accessed: metadata.accessed().ok(), + } + }) + } + + async fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + let path = self.get_path(path); + + blocking_io(move || filetime::set_file_mtime(path, FileTime::from(time))).await?; + + Ok(()) + } + + async fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + let path = self.get_path(path); + + blocking_io(move || filetime::set_file_atime(path, FileTime::from(time))).await?; + + Ok(()) + } + + async fn exists(&self, path: &str) -> VfsResult { + Ok(self.get_path(path).exists().await) + } + + async fn remove_file(&self, path: &str) -> VfsResult<()> { + async_std::fs::remove_file(self.get_path(path)).await?; + Ok(()) + } + + async fn remove_dir(&self, path: &str) -> VfsResult<()> { + async_std::fs::remove_dir(self.get_path(path)).await?; + Ok(()) + } + + async fn copy_file(&self, src: &str, dest: &str) -> VfsResult<()> { + async_std::fs::copy(self.get_path(src), self.get_path(dest)).await?; + Ok(()) + } + + async fn move_file(&self, src: &str, dest: &str) -> VfsResult<()> { + async_std::fs::rename(self.get_path(src), self.get_path(dest)).await?; + + Ok(()) + } + + async fn move_dir(&self, src: &str, dest: &str) -> VfsResult<()> { + let result = async_std::fs::rename(self.get_path(src), self.get_path(dest)).await; + if result.is_err() { + // Error possibly due to different filesystems, return not supported and let the fallback handle it + return Err(VfsErrorKind::NotSupported.into()); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::async_vfs::AsyncVfsPath; + + use async_std::io::ReadExt; + use async_std::io::WriteExt; + use async_std::path::Path; + use futures::stream::StreamExt; + + test_async_vfs!(futures::executor::block_on(async { + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + async_std::fs::create_dir_all(&dir).await.unwrap(); + AsyncPhysicalFS::new(dir) + })); + test_async_vfs_readonly!({ AsyncPhysicalFS::new("test/test_directory") }); + + fn create_root() -> AsyncVfsPath { + AsyncPhysicalFS::new(std::env::current_dir().unwrap()).into() + } + + #[tokio::test] + async fn open_file() { + let expected = async_std::fs::read_to_string("Cargo.toml").await.unwrap(); + let root = create_root(); + let mut string = String::new(); + root.join("Cargo.toml") + .unwrap() + .open_file() + .await + .unwrap() + .read_to_string(&mut string) + .await + .unwrap(); + assert_eq!(string, expected); + } + + #[tokio::test] + async fn create_file() { + let root = create_root(); + let _string = String::new(); + let _ = async_std::fs::remove_file("target/test.txt").await; + root.join("target/test.txt") + .unwrap() + .create_file() + .await + .unwrap() + .write_all(b"Testing only") + .await + .unwrap(); + let read = std::fs::read_to_string("target/test.txt").unwrap(); + assert_eq!(read, "Testing only"); + } + + #[tokio::test] + async fn append_file() { + let root = create_root(); + let _string = String::new(); + let _ = async_std::fs::remove_file("target/test_append.txt").await; + let path = Box::pin(root.join("target/test_append.txt").unwrap()); + path.create_file() + .await + .unwrap() + .write_all(b"Testing 1") + .await + .unwrap(); + path.append_file() + .await + .unwrap() + .write_all(b"Testing 2") + .await + .unwrap(); + let read = async_std::fs::read_to_string("target/test_append.txt") + .await + .unwrap(); + assert_eq!(read, "Testing 1Testing 2"); + } + + #[tokio::test] + async fn read_dir() { + let _expected = async_std::fs::read_to_string("Cargo.toml").await.unwrap(); + let root = create_root(); + let entries: Vec<_> = root.read_dir().await.unwrap().collect().await; + let map: Vec<_> = entries + .iter() + .map(|path: &AsyncVfsPath| path.as_str()) + .filter(|x| x.ends_with(".toml")) + .collect(); + assert_eq!(&["/Cargo.toml"], &map[..]); + } + + #[tokio::test] + async fn create_dir() { + let _ = async_std::fs::remove_dir("target/fs_test").await; + let root = create_root(); + root.join("target/fs_test") + .unwrap() + .create_dir() + .await + .unwrap(); + let path = Path::new("target/fs_test"); + assert!(path.exists().await, "Path was not created"); + assert!(path.is_dir().await, "Path is not a directory"); + async_std::fs::remove_dir("target/fs_test").await.unwrap(); + } + + #[tokio::test] + async fn file_metadata() { + let expected = async_std::fs::read_to_string("Cargo.toml").await.unwrap(); + let root = create_root(); + let metadata = root.join("Cargo.toml").unwrap().metadata().await.unwrap(); + assert_eq!(metadata.len, expected.len() as u64); + assert_eq!(metadata.file_type, VfsFileType::File); + } + + #[tokio::test] + async fn dir_metadata() { + let root = create_root(); + let metadata = root.metadata().await.unwrap(); + assert_eq!(metadata.len, 0); + assert_eq!(metadata.file_type, VfsFileType::Directory); + let metadata = root.join("src").unwrap().metadata().await.unwrap(); + assert_eq!(metadata.len, 0); + assert_eq!(metadata.file_type, VfsFileType::Directory); + } +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/mod.rs b/awkernel_lib/src/file/vfs/async_vfs/mod.rs new file mode 100644 index 000000000..59448794a --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/mod.rs @@ -0,0 +1,64 @@ +//! Asynchronous port of virtual file system abstraction +//! +//! +//! Just as with the synchronous version, the main interaction with the virtual filesystem is by using virtual paths ([`AsyncVfsPath`](path/struct.AsyncVfsPath.html)). +//! +//! This module currently has the following asynchronous file system implementations: +//! +//! * **[`AsyncPhysicalFS`](impls/physical/struct.AsyncPhysicalFS.html)** - the actual filesystem of the underlying OS +//! * **[`AsyncMemoryFS`](impls/memory/struct.AsyncMemoryFS.html)** - an ephemeral in-memory implementation (intended for unit tests) +//! * **[`AsyncAltrootFS`](impls/altroot/struct.AsyncAltrootFS.html)** - a file system with its root in a particular directory of another filesystem +//! * **[`AsyncOverlayFS`](impls/overlay/struct.AsyncOverlayFS.html)** - a union file system consisting of a read/writable upper layer and several read-only lower layers +//! +//! # Usage Examples +//! +//! ``` +//! use async_std::io::{ReadExt, WriteExt}; +//! use vfs::async_vfs::{AsyncVfsPath, AsyncPhysicalFS}; +//! use vfs::VfsError; +//! +//! # tokio_test::block_on(async { +//! let root: AsyncVfsPath = AsyncPhysicalFS::new(std::env::current_dir().unwrap()).into(); +//! assert!(root.exists().await?); +//! +//! let mut content = String::new(); +//! root.join("README.md")?.open_file().await?.read_to_string(&mut content).await?; +//! assert!(content.contains("vfs")); +//! # Ok::<(), VfsError>(()) +//! # }); +//! ``` +//! +//! ``` +//! use async_std::io::{ReadExt, WriteExt}; +//! use vfs::async_vfs::{AsyncVfsPath, AsyncMemoryFS}; +//! use vfs::VfsError; +//! +//! # tokio_test::block_on(async { +//! let root: AsyncVfsPath = AsyncMemoryFS::new().into(); +//! let path = root.join("test.txt")?; +//! assert!(!path.exists().await?); +//! +//! path.create_file().await?.write_all(b"Hello world").await?; +//! assert!(path.exists().await?); +//! let mut content = String::new(); +//! path.open_file().await?.read_to_string(&mut content).await?; +//! assert_eq!(content, "Hello world"); +//! # Ok::<(), VfsError>(()) +//! # }); +//! ``` +//! + +#[cfg(any(test, feature = "export-test-macros"))] +#[macro_use] +pub mod test_macros; + +pub mod filesystem; +pub mod impls; +pub mod path; + +pub use filesystem::AsyncFileSystem; +pub use impls::altroot::AsyncAltrootFS; +pub use impls::memory::AsyncMemoryFS; +pub use impls::overlay::AsyncOverlayFS; +pub use impls::physical::AsyncPhysicalFS; +pub use path::*; diff --git a/awkernel_lib/src/file/vfs/async_vfs/path.rs b/awkernel_lib/src/file/vfs/async_vfs/path.rs new file mode 100644 index 000000000..ed637649d --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/path.rs @@ -0,0 +1,1113 @@ +//! Virtual filesystem path +//! +//! The virtual file system abstraction generalizes over file systems and allow using +//! different VirtualFileSystem implementations (i.e. an in memory implementation for unit tests) + +use crate::async_vfs::AsyncFileSystem; +use crate::error::{VfsError, VfsErrorKind}; +use crate::path::PathLike; +use crate::path::VfsFileType; +use crate::{VfsMetadata, VfsResult}; + +use async_recursion::async_recursion; +use async_std::io::{Read, ReadExt, Seek, Write}; +use async_std::sync::Arc; +use async_std::task::{Context, Poll}; +use futures::{future::BoxFuture, FutureExt, Stream, StreamExt}; +use std::pin::Pin; +use std::time::SystemTime; + +/// Trait combining Seek and Read, return value for opening files +pub trait SeekAndRead: Seek + Read {} + +impl SeekAndRead for T where T: Seek + Read {} + +#[derive(Debug)] +struct AsyncVFS { + fs: Box, +} + +/// A virtual filesystem path, identifying a single file or directory in this virtual filesystem +#[derive(Clone, Debug)] +pub struct AsyncVfsPath { + path: String, + fs: Arc, +} + +impl PathLike for AsyncVfsPath { + fn get_path(&self) -> String { + self.path.clone() + } +} + +impl PartialEq for AsyncVfsPath { + fn eq(&self, other: &Self) -> bool { + self.path == other.path && Arc::ptr_eq(&self.fs, &other.fs) + } +} + +impl Eq for AsyncVfsPath {} + +impl AsyncVfsPath { + /// Creates a root path for the given filesystem + /// + /// ``` + /// # use vfs::async_vfs::{AsyncPhysicalFS, AsyncVfsPath}; + /// let path = AsyncVfsPath::new(AsyncPhysicalFS::new(".")); + /// ```` + pub fn new(filesystem: T) -> Self { + AsyncVfsPath { + path: "".to_string(), + fs: Arc::new(AsyncVFS { + fs: Box::new(filesystem), + }), + } + } + + /// Returns the string representation of this path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncPhysicalFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// let path = AsyncVfsPath::new(AsyncPhysicalFS::new(".")); + /// + /// assert_eq!(path.as_str(), ""); + /// assert_eq!(path.join("foo.txt")?.as_str(), "/foo.txt"); + /// # Ok::<(), VfsError>(()) + /// ```` + pub fn as_str(&self) -> &str { + &self.path + } + + /// Appends a path segment to this path, returning the result + /// + /// ``` + /// # use vfs::async_vfs::{AsyncPhysicalFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// let path = AsyncVfsPath::new(AsyncPhysicalFS::new(".")); + /// + /// assert_eq!(path.join("foo.txt")?.as_str(), "/foo.txt"); + /// assert_eq!(path.join("foo/bar.txt")?.as_str(), "/foo/bar.txt"); + /// + /// let foo = path.join("foo")?; + /// + /// assert_eq!(path.join("foo/bar.txt")?, foo.join("bar.txt")?); + /// assert_eq!(path, foo.join("..")?); + /// # Ok::<(), VfsError>(()) + /// ``` + pub fn join(&self, path: impl AsRef) -> VfsResult { + let new_path = self.join_internal(&self.path, path.as_ref())?; + Ok(Self { + path: new_path, + fs: self.fs.clone(), + }) + } + + /// Returns the root path of this filesystem + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType}; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo/bar")?; + /// + /// assert_eq!(directory.root(), path); + /// # Ok::<(), VfsError>(()) + /// ``` + pub fn root(&self) -> AsyncVfsPath { + AsyncVfsPath { + path: "".to_string(), + fs: self.fs.clone(), + } + } + + /// Returns true if this is the root path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType}; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// assert!(path.is_root()); + /// let path = path.join("foo/bar")?; + /// assert!(! path.is_root()); + /// # Ok::<(), VfsError>(()) + /// ``` + pub fn is_root(&self) -> bool { + self.path.is_empty() + } + + /// Creates the directory at this path + /// + /// Note that the parent directory must exist, while the given path must not exist. + /// + /// Returns VfsErrorKind::FileExists if a file already exists at the given path + /// Returns VfsErrorKind::DirectoryExists if a directory already exists at the given path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// + /// directory.create_dir().await?; + /// + /// assert!(directory.exists().await?); + /// assert_eq!(directory.metadata().await?.file_type, VfsFileType::Directory); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn create_dir(&self) -> VfsResult<()> { + self.get_parent("create directory").await?; + self.fs.fs.create_dir(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not create directory") + }) + } + + /// Creates the directory at this path, also creating parent directories as necessary + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo/bar")?; + /// + /// directory.create_dir_all().await?; + /// + /// assert!(directory.exists().await?); + /// assert_eq!(directory.metadata().await?.file_type, VfsFileType::Directory); + /// let parent = path.join("foo")?; + /// assert!(parent.exists().await?); + /// assert_eq!(parent.metadata().await?.file_type, VfsFileType::Directory); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn create_dir_all(&self) -> VfsResult<()> { + let mut pos = 1; + let path = &self.path; + if path.is_empty() { + // root exists always + return Ok(()); + } + loop { + // Iterate over path segments + let end = path[pos..] + .find('/') + .map(|it| it + pos) + .unwrap_or_else(|| path.len()); + let directory = &path[..end]; + if let Err(error) = self.fs.fs.create_dir(directory).await { + match error.kind() { + VfsErrorKind::DirectoryExists => {} + _ => { + return Err(error.with_path(directory).with_context(|| { + format!("Could not create directories at '{}'", path) + })) + } + } + } + if end == path.len() { + break; + } + pos = end + 1; + } + Ok(()) + } + + /// Iterates over all entries of this directory path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use futures::stream::Collect; + /// use futures::stream::StreamExt; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// path.join("foo")?.create_dir().await?; + /// path.join("bar")?.create_dir().await?; + /// + /// let mut directories: Vec<_> = path.read_dir().await?.collect().await; + /// + /// directories.sort_by_key(|path| path.as_str().to_string()); + /// assert_eq!(directories, vec![path.join("bar")?, path.join("foo")?]); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn read_dir(&self) -> VfsResult + Send>> { + let parent = self.path.clone(); + let fs = self.fs.clone(); + Ok(Box::new( + self.fs + .fs + .read_dir(&self.path) + .await + .map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not read directory") + })? + .map(move |path| { + println!("{:?}", path); + AsyncVfsPath { + path: format!("{}/{}", parent, path), + fs: fs.clone(), + } + }), + )) + } + + /// Creates a file at this path for writing, overwriting any existing file + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use async_std::io:: {ReadExt, WriteExt}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// + /// write!(file.create_file().await?, "Hello, world!").await?; + /// + /// let mut result = String::new(); + /// file.open_file().await?.read_to_string(&mut result).await?; + /// assert_eq!(&result, "Hello, world!"); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn create_file(&self) -> VfsResult> { + self.get_parent("create file").await?; + self.fs.fs.create_file(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not create file") + }) + } + + /// Opens the file at this path for reading + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use async_std::io:: {ReadExt, WriteExt}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// write!(file.create_file().await?, "Hello, world!").await?; + /// let mut result = String::new(); + /// + /// file.open_file().await?.read_to_string(&mut result).await?; + /// + /// assert_eq!(&result, "Hello, world!"); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn open_file( + &self, + ) -> VfsResult + Send + Unpin>> { + self.fs.fs.open_file(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not open file") + }) + } + + /// Checks whether parent is a directory + async fn get_parent(&self, action: &str) -> VfsResult<()> { + let parent = self.parent(); + if !parent.exists().await? { + return Err(VfsError::from(VfsErrorKind::Other(format!( + "Could not {}, parent directory does not exist", + action + ))) + .with_path(&self.path)); + } + let metadata = parent.metadata().await?; + if metadata.file_type != VfsFileType::Directory { + return Err(VfsError::from(VfsErrorKind::Other(format!( + "Could not {}, parent path is not a directory", + action + ))) + .with_path(&self.path)); + } + Ok(()) + } + + /// Opens the file at this path for appending + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use async_std::io:: {ReadExt, WriteExt}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// write!(file.create_file().await?, "Hello, ").await?; + /// write!(file.append_file().await?, "world!").await?; + /// let mut result = String::new(); + /// file.open_file().await?.read_to_string(&mut result).await?; + /// assert_eq!(&result, "Hello, world!"); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn append_file(&self) -> VfsResult> { + self.fs.fs.append_file(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not open file for appending") + }) + } + + /// Removes the file at this path + /// + /// ``` + /// use async_std::io:: {ReadExt, WriteExt}; + /// # use vfs::async_vfs::{AsyncMemoryFS , AsyncVfsPath}; + /// # use vfs::VfsError; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// write!(file.create_file().await?, "Hello, ").await?; + /// assert!(file.exists().await?); + /// + /// file.remove_file().await?; + /// + /// assert!(!file.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn remove_file(&self) -> VfsResult<()> { + self.fs.fs.remove_file(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not remove file") + }) + } + + /// Removes the directory at this path + /// + /// The directory must be empty. + /// + /// ``` + /// # tokio_test::block_on(async { + /// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// use vfs::VfsError; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// directory.create_dir().await; + /// assert!(directory.exists().await?); + /// + /// directory.remove_dir().await?; + /// + /// assert!(!directory.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn remove_dir(&self) -> VfsResult<()> { + self.fs.fs.remove_dir(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not remove directory") + }) + } + + /// Ensures that the directory at this path is removed, recursively deleting all contents if necessary + /// + /// Returns successfully if directory does not exist + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// directory.join("bar")?.create_dir_all().await?; + /// assert!(directory.exists().await?); + /// + /// directory.remove_dir_all().await?; + /// + /// assert!(!directory.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + #[async_recursion] + pub async fn remove_dir_all(&self) -> VfsResult<()> { + if !self.exists().await? { + return Ok(()); + } + let mut path_stream = self.read_dir().await?; + while let Some(child) = path_stream.next().await { + let metadata = child.metadata().await?; + match metadata.file_type { + VfsFileType::File => child.remove_file().await?, + VfsFileType::Directory => child.remove_dir_all().await?, + } + } + self.remove_dir().await?; + Ok(()) + } + + /// Returns the file metadata for the file at this path + /// + /// ``` + /// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// use async_std::io::WriteExt; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// directory.create_dir().await?; + /// + /// assert_eq!(directory.metadata().await?.len, 0); + /// assert_eq!(directory.metadata().await?.file_type, VfsFileType::Directory); + /// + /// let file = path.join("bar.txt")?; + /// write!(file.create_file().await?, "Hello, world!").await?; + /// + /// assert_eq!(file.metadata().await?.len, 13); + /// assert_eq!(file.metadata().await?.file_type, VfsFileType::File); + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn metadata(&self) -> VfsResult { + self.fs.fs.metadata(&self.path).await.map_err(|err| { + err.with_path(&self.path) + .with_context(|| "Could not get metadata") + }) + } + + /// Sets the files creation timestamp at this path + /// + /// ``` + /// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath}; + /// use async_std::io::WriteExt; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// file.create_file(); + /// + /// let time = std::time::SystemTime::now(); + /// file.set_creation_time(time).await?; + /// + /// assert_eq!(file.metadata().await?.len, 0); + /// assert_eq!(file.metadata().await?.created, Some(time)); + /// + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn set_creation_time(&self, time: SystemTime) -> VfsResult<()> { + self.fs + .fs + .set_creation_time(&self.path, time) + .await + .map_err(|err| { + err.with_path(&*self.path) + .with_context(|| "Could not set creation timestamp.") + }) + } + + /// Sets the files modification timestamp at this path + /// + /// ``` + /// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath}; + /// use async_std::io::WriteExt; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// file.create_file(); + /// + /// let time = std::time::SystemTime::now(); + /// file.set_modification_time(time).await?; + /// + /// assert_eq!(file.metadata().await?.len, 0); + /// assert_eq!(file.metadata().await?.modified, Some(time)); + /// + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn set_modification_time(&self, time: SystemTime) -> VfsResult<()> { + self.fs + .fs + .set_modification_time(&self.path, time) + .await + .map_err(|err| { + err.with_path(&*self.path) + .with_context(|| "Could not set modification timestamp.") + }) + } + + /// Sets the files access timestamp at this path + /// + /// ``` + /// use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// use vfs::{VfsError, VfsFileType, VfsMetadata, VfsPath}; + /// use async_std::io::WriteExt; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// file.create_file(); + /// + /// let time = std::time::SystemTime::now(); + /// file.set_access_time(time).await?; + /// + /// assert_eq!(file.metadata().await?.len, 0); + /// assert_eq!(file.metadata().await?.accessed, Some(time)); + /// + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn set_access_time(&self, time: SystemTime) -> VfsResult<()> { + self.fs + .fs + .set_access_time(&self.path, time) + .await + .map_err(|err| { + err.with_path(&*self.path) + .with_context(|| "Could not set access timestamp.") + }) + } + + /// Returns `true` if the path exists and is pointing at a regular file, otherwise returns `false`. + /// + /// Note that this call may fail if the file's existence cannot be determined or the metadata can not be retrieved + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// directory.create_dir().await?; + /// let file = path.join("foo.txt")?; + /// file.create_file().await?; + /// + /// assert!(!directory.is_file().await?); + /// assert!(file.is_file().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn is_file(&self) -> VfsResult { + if !self.exists().await? { + return Ok(false); + } + let metadata = self.metadata().await?; + Ok(metadata.file_type == VfsFileType::File) + } + + /// Returns `true` if the path exists and is pointing at a directory, otherwise returns `false`. + /// + /// Note that this call may fail if the directory's existence cannot be determined or the metadata can not be retrieved + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// directory.create_dir().await?; + /// let file = path.join("foo.txt")?; + /// file.create_file().await?; + /// + /// assert!(directory.is_dir().await?); + /// assert!(!file.is_dir().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn is_dir(&self) -> VfsResult { + if !self.exists().await? { + return Ok(false); + } + let metadata = self.metadata().await?; + Ok(metadata.file_type == VfsFileType::Directory) + } + + /// Returns true if a file or directory exists at this path, false otherwise + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let directory = path.join("foo")?; + /// + /// assert!(!directory.exists().await?); + /// + /// directory.create_dir().await?; + /// + /// assert!(directory.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + pub async fn exists(&self) -> VfsResult { + self.fs.fs.exists(&self.path).await + } + + /// Returns the filename portion of this path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo/bar.txt")?; + /// + /// assert_eq!(&file.filename(), "bar.txt"); + /// + /// # Ok::<(), VfsError>(()) + pub fn filename(&self) -> String { + self.filename_internal() + } + + /// Returns the extension portion of this path + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// + /// assert_eq!(path.join("foo/bar.txt")?.extension(), Some("txt".to_string())); + /// assert_eq!(path.join("foo/bar.txt.zip")?.extension(), Some("zip".to_string())); + /// assert_eq!(path.join("foo/bar")?.extension(), None); + /// + /// # Ok::<(), VfsError>(()) + pub fn extension(&self) -> Option { + self.extension_internal() + } + + /// Returns the parent path of this portion of this path + /// + /// Root will return itself. + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsFileType, VfsMetadata}; + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// + /// assert_eq!(path.parent(), path.root()); + /// assert_eq!(path.join("foo/bar")?.parent(), path.join("foo")?); + /// assert_eq!(path.join("foo")?.parent(), path); + /// + /// # Ok::<(), VfsError>(()) + pub fn parent(&self) -> Self { + let parent_path = self.parent_internal(&self.path); + Self { + path: parent_path, + fs: self.fs.clone(), + } + } + + /// Recursively iterates over all the directories and files at this path + /// + /// Directories are visited before their children + /// + /// Note that the iterator items can contain errors, usually when directories are removed during the iteration. + /// The returned paths may also point to non-existant files if there is concurrent removal. + /// + /// Also note that loops in the file system hierarchy may cause this iterator to never terminate. + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::{VfsError, VfsResult}; + /// use futures::stream::StreamExt; + /// # tokio_test::block_on(async { + /// let root = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// root.join("foo/bar")?.create_dir_all().await?; + /// root.join("fizz/buzz")?.create_dir_all().await?; + /// root.join("foo/bar/baz")?.create_file().await?; + /// + /// let mut directories = root.walk_dir().await?.map(|res| res.unwrap()).collect::>().await; + /// + /// directories.sort_by_key(|path| path.as_str().to_string()); + /// let expected = vec!["fizz", "fizz/buzz", "foo", "foo/bar", "foo/bar/baz"].iter().map(|path| root.join(path)).collect::>>()?; + /// assert_eq!(directories, expected); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn walk_dir(&self) -> VfsResult { + Ok(WalkDirIterator { + inner: self.read_dir().await?, + todo: vec![], + prev_result: None, + metadata_fut: None, + read_dir_fut: None, + }) + } + + /// Reads a complete file to a string + /// + /// Returns an error if the file does not exist or is not valid UTF-8 + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use async_std::io::{ReadExt, WriteExt}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let file = path.join("foo.txt")?; + /// write!(file.create_file().await?, "Hello, world!").await?; + /// + /// let result = file.read_to_string().await?; + /// + /// assert_eq!(&result, "Hello, world!"); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn read_to_string(&self) -> VfsResult { + let metadata = self.metadata().await?; + if metadata.file_type != VfsFileType::File { + return Err( + VfsError::from(VfsErrorKind::Other("Path is a directory".into())) + .with_path(&self.path) + .with_context(|| "Could not read path"), + ); + } + let mut result = String::with_capacity(metadata.len as usize); + self.open_file() + .await? + .read_to_string(&mut result) + .await + .map_err(|source| { + VfsError::from(source) + .with_path(&self.path) + .with_context(|| "Could not read path") + })?; + Ok(result) + } + + /// Copies a file to a new destination + /// + /// The destination must not exist, but its parent directory must + /// + /// ``` + /// use async_std::io::{ReadExt, WriteExt}; + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let src = path.join("foo.txt")?; + /// write!(src.create_file().await?, "Hello, world!").await?; + /// let dest = path.join("bar.txt")?; + /// + /// src.copy_file(&dest).await?; + /// + /// assert_eq!(dest.read_to_string().await?, "Hello, world!"); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn copy_file(&self, destination: &AsyncVfsPath) -> VfsResult<()> { + async { + if destination.exists().await? { + return Err(VfsError::from(VfsErrorKind::Other( + "Destination exists already".into(), + )) + .with_path(&self.path)); + } + if Arc::ptr_eq(&self.fs, &destination.fs) { + let result = self.fs.fs.copy_file(&self.path, &destination.path).await; + match result { + Err(err) => match err.kind() { + VfsErrorKind::NotSupported => { + // continue + } + _ => return Err(err), + }, + other => return other, + } + } + let mut src = self.open_file().await?; + let mut dest = destination.create_file().await?; + async_std::io::copy(&mut src, &mut dest) + .await + .map_err(|source| { + VfsError::from(source) + .with_path(&self.path) + .with_context(|| "Could not read path") + })?; + Ok(()) + } + .await + .map_err(|err| { + err.with_path(&self.path).with_context(|| { + format!( + "Could not copy '{}' to '{}'", + self.as_str(), + destination.as_str() + ) + }) + })?; + Ok(()) + } + + /// Moves or renames a file to a new destination + /// + /// The destination must not exist, but its parent directory must + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// use async_std::io::{ReadExt, WriteExt}; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let src = path.join("foo.txt")?; + /// write!(src.create_file().await?, "Hello, world!").await?; + /// let dest = path.join("bar.txt")?; + /// + /// src.move_file(&dest).await?; + /// + /// assert_eq!(dest.read_to_string().await?, "Hello, world!"); + /// assert!(!src.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn move_file(&self, destination: &AsyncVfsPath) -> VfsResult<()> { + async { + if destination.exists().await? { + return Err(VfsError::from(VfsErrorKind::Other( + "Destination exists already".into(), + )) + .with_path(&destination.path)); + } + if Arc::ptr_eq(&self.fs, &destination.fs) { + let result = self.fs.fs.move_file(&self.path, &destination.path); + match result.await { + Err(err) => match err.kind() { + VfsErrorKind::NotSupported => { + // continue + } + _ => return Err(err), + }, + other => return other, + } + } + let mut src = self.open_file().await?; + let mut dest = destination.create_file().await?; + async_std::io::copy(&mut src, &mut dest) + .await + .map_err(|source| { + VfsError::from(source) + .with_path(&self.path) + .with_context(|| "Could not read path") + })?; + self.remove_file().await?; + Ok(()) + } + .await + .map_err(|err| { + err.with_path(&self.path).with_context(|| { + format!( + "Could not move '{}' to '{}'", + self.as_str(), + destination.as_str() + ) + }) + })?; + Ok(()) + } + + /// Copies a directory to a new destination, recursively + /// + /// The destination must not exist, but the parent directory must + /// + /// Returns the number of files copied + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let src = path.join("foo")?; + /// src.join("dir")?.create_dir_all().await?; + /// let dest = path.join("bar.txt")?; + /// + /// src.copy_dir(&dest).await?; + /// + /// assert!(dest.join("dir")?.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn copy_dir(&self, destination: &AsyncVfsPath) -> VfsResult { + let files_copied = async { + let mut files_copied = 0u64; + if destination.exists().await? { + return Err(VfsError::from(VfsErrorKind::Other( + "Destination exists already".into(), + )) + .with_path(&destination.path)); + } + destination.create_dir().await?; + let prefix = self.path.as_str(); + let prefix_len = prefix.len(); + let mut path_stream = self.walk_dir().await?; + while let Some(file) = path_stream.next().await { + let src_path: AsyncVfsPath = file?; + let dest_path = destination.join(&src_path.as_str()[prefix_len + 1..])?; + match src_path.metadata().await?.file_type { + VfsFileType::Directory => dest_path.create_dir().await?, + VfsFileType::File => src_path.copy_file(&dest_path).await?, + } + files_copied += 1; + } + Ok(files_copied) + } + .await + .map_err(|err| { + err.with_path(&self.path).with_context(|| { + format!( + "Could not copy directory '{}' to '{}'", + self.as_str(), + destination.as_str() + ) + }) + })?; + Ok(files_copied) + } + + /// Moves a directory to a new destination, including subdirectories and files + /// + /// The destination must not exist, but its parent directory must + /// + /// ``` + /// # use vfs::async_vfs::{AsyncMemoryFS, AsyncVfsPath}; + /// # use vfs::VfsError; + /// # tokio_test::block_on(async { + /// let path = AsyncVfsPath::new(AsyncMemoryFS::new()); + /// let src = path.join("foo")?; + /// src.join("dir")?.create_dir_all().await?; + /// let dest = path.join("bar.txt")?; + /// + /// src.move_dir(&dest).await?; + /// + /// assert!(dest.join("dir")?.exists().await?); + /// assert!(!src.join("dir")?.exists().await?); + /// # Ok::<(), VfsError>(()) + /// # }); + /// ``` + pub async fn move_dir(&self, destination: &AsyncVfsPath) -> VfsResult<()> { + async { + if destination.exists().await? { + return Err(VfsError::from(VfsErrorKind::Other( + "Destination exists already".into(), + )) + .with_path(&destination.path)); + } + if Arc::ptr_eq(&self.fs, &destination.fs) { + let result = self.fs.fs.move_dir(&self.path, &destination.path).await; + match result { + Err(err) => match err.kind() { + VfsErrorKind::NotSupported => { + // continue + } + _ => return Err(err), + }, + other => return other, + } + } + destination.create_dir().await?; + let prefix = self.path.as_str(); + let prefix_len = prefix.len(); + let mut path_stream = self.walk_dir().await?; + while let Some(file) = path_stream.next().await { + let src_path: AsyncVfsPath = file?; + let dest_path = destination.join(&src_path.as_str()[prefix_len + 1..])?; + match src_path.metadata().await?.file_type { + VfsFileType::Directory => dest_path.create_dir().await?, + VfsFileType::File => src_path.copy_file(&dest_path).await?, + } + } + self.remove_dir_all().await?; + Ok(()) + } + .await + .map_err(|err| { + err.with_path(&self.path).with_context(|| { + format!( + "Could not move directory '{}' to '{}'", + self.as_str(), + destination.as_str() + ) + }) + })?; + Ok(()) + } +} + +/// An iterator for recursively walking a file hierarchy +pub struct WalkDirIterator { + /// the path iterator of the current directory + inner: Box + Send + Unpin>, + /// stack of subdirectories still to walk + todo: Vec, + /// used to store the previous yield of the todo stream, + /// which would otherwise get dropped if path.metadata() is pending + prev_result: Option, + // Used to store futures when poll_next returns pending + // this ensures a new future is not spawned on each poll. + read_dir_fut: Option< + BoxFuture<'static, Result + Send + Unpin)>, VfsError>>, + >, + metadata_fut: Option>>, +} + +impl std::fmt::Debug for WalkDirIterator { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("WalkDirIterator")?; + self.todo.fmt(f) + } +} + +impl Stream for WalkDirIterator { + type Item = VfsResult; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + // Check if we have a previously stored result from last call + // that we could not utilize due to pending path.metadata() call + let result = if this.prev_result.is_none() { + loop { + match this.inner.poll_next_unpin(cx) { + Poll::Pending => return Poll::Pending, + Poll::Ready(Some(path)) => break Ok(path), + Poll::Ready(None) => { + let directory = if this.todo.is_empty() { + return Poll::Ready(None); + } else { + this.todo[this.todo.len() - 1].clone() + }; + let mut read_dir_fut = if this.read_dir_fut.is_some() { + this.read_dir_fut.take().unwrap() + } else { + Box::pin(async move { directory.read_dir().await }) + }; + match read_dir_fut.poll_unpin(cx) { + Poll::Pending => { + this.read_dir_fut = Some(read_dir_fut); + return Poll::Pending; + } + Poll::Ready(Err(err)) => { + let _ = this.todo.pop(); + break Err(err); + } + Poll::Ready(Ok(iterator)) => { + let _ = this.todo.pop(); + this.inner = iterator; + } + } + } + } + } + } else { + Ok(this.prev_result.take().unwrap()) + }; + if let Ok(path) = &result { + let mut metadata_fut = if this.metadata_fut.is_some() { + this.metadata_fut.take().unwrap() + } else { + let path_clone = path.clone(); + Box::pin(async move { path_clone.metadata().await }) + }; + match metadata_fut.poll_unpin(cx) { + Poll::Pending => { + this.prev_result = Some(path.clone()); + this.metadata_fut = Some(metadata_fut); + return Poll::Pending; + } + Poll::Ready(Ok(meta)) => { + if meta.file_type == VfsFileType::Directory { + this.todo.push(path.clone()); + } + } + Poll::Ready(Err(err)) => return Poll::Ready(Some(Err(err))), + } + } + Poll::Ready(Some(result)) + } +} diff --git a/awkernel_lib/src/file/vfs/async_vfs/test_macros.rs b/awkernel_lib/src/file/vfs/async_vfs/test_macros.rs new file mode 100644 index 000000000..a4e347c91 --- /dev/null +++ b/awkernel_lib/src/file/vfs/async_vfs/test_macros.rs @@ -0,0 +1,1395 @@ +/// Run basic read/write vfs test to check for conformance +/// If an Filesystem implementation is read-only use [test_async_vfs_readonly!] instead +#[macro_export] +macro_rules! test_async_vfs { + ($root:expr) => { + #[cfg(test)] + mod vfs_tests { + use super::*; + use $crate::VfsFileType; + use $crate::async_vfs::AsyncVfsPath; + use $crate::VfsResult; + use $crate::error::VfsErrorKind; + use futures::stream::StreamExt; + use async_std::io::{WriteExt, ReadExt}; + use std::time::SystemTime; + + fn create_root() -> AsyncVfsPath { + $root.into() + } + + #[tokio::test] + async fn vfs_can_be_created() { + create_root(); + } + + #[tokio::test] + async fn set_and_query_creation_timestamp() -> VfsResult<()> { + let root = create_root(); + let path = root.join("foobar.txt").unwrap(); + drop( path.create_file().await.unwrap() ); + + let time = SystemTime::now(); + let result = path.set_creation_time(time).await; + + match result { + Err(err) => { + if let VfsErrorKind::NotSupported = err.kind() { + println!("Skipping creation time test: set_creation_time unsupported!"); + } else { + return Err(err); + } + }, + _ => { + assert_eq!(path.metadata().await?.created, Some(time)); + } + } + Ok(()) + } + + #[tokio::test] + async fn set_and_query_modification_timestamp() -> VfsResult<()> { + let root = create_root(); + let path = root.join("foobar.txt").unwrap(); + drop( path.create_file().await.unwrap() ); + + let time = SystemTime::now(); + let result = path.set_modification_time(time).await; + + match result { + Err(err) => { + if let VfsErrorKind::NotSupported = err.kind() { + println!("Skipping creation time test: set_modification_time unsupported!"); + } else { + return Err(err); + } + }, + _ => { + assert_eq!(path.metadata().await?.modified, Some(time)); + } + } + Ok(()) + } + + #[tokio::test] + async fn set_and_query_access_timestamp() -> VfsResult<()> { + let root = create_root(); + let path = root.join("foobar.txt").unwrap(); + drop( path.create_file().await.unwrap() ); + + let time = SystemTime::now(); + let result = path.set_access_time(time).await; + + match result { + Err(err) => { + if let VfsErrorKind::NotSupported = err.kind() { + println!("Skipping access time test: set_access_time unsupported!"); + } else { + return Err(err); + } + }, + _ => { + assert_eq!(path.metadata().await?.accessed, Some(time)); + } + } + Ok(()) + } + + #[tokio::test] + async fn write_and_read_file() -> VfsResult<()>{ + let root = create_root(); + let path = root.join("foobar.txt").unwrap(); + let _send = &path as &dyn Send; + { + let mut file = path.create_file().await.unwrap(); + write!(file, "Hello world").await.unwrap(); + write!(file, "!").await.unwrap(); + } + { + let mut file = path.open_file().await.unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).await.unwrap(); + assert_eq!(string, "Hello world!"); + } + assert!(path.exists().await?); + assert!(!root.join("foo").unwrap().exists().await?); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.len, 12); + assert_eq!(metadata.file_type, VfsFileType::File); + Ok(()) + } + + #[tokio::test] + async fn append_file() { + let root = create_root(); + let path = root.join("test_append.txt").unwrap(); + path.create_file().await.unwrap().write_all(b"Testing 1").await.unwrap(); + path.append_file().await.unwrap().write_all(b"Testing 2").await.unwrap(); + { + let mut file = path.open_file().await.unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).await.unwrap(); + assert_eq!(string, "Testing 1Testing 2"); + } + } + + #[tokio::test] + async fn append_non_existing_file() { + let root = create_root(); + let path = root.join("test_append.txt").unwrap(); + let result = path.append_file().await; + match result { + Ok(_) => {panic!("Expected error");} + Err(err) => { + let error_message = format!("{}", err); + assert!( + error_message.starts_with("Could not open file for appending"), + "Actual message: {}", + error_message); + } + } + } + + #[tokio::test] + async fn create_dir() { + let root = create_root(); + let _string = String::new(); + let path = root.join("foo").unwrap(); + path.create_dir().await.unwrap(); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.file_type, VfsFileType::Directory); + assert_eq!(metadata.len, 0); + } + + #[tokio::test] + async fn create_dir_with_camino() { + let root = create_root(); + let _string = String::new(); + let path = root.join(camino::Utf8Path::new("foo")).unwrap(); + path.create_dir().await.unwrap(); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.file_type, VfsFileType::Directory); + assert_eq!(metadata.len, 0); + } + + #[tokio::test] + async fn create_dir_all() -> VfsResult<()>{ + let root = create_root(); + let _string = String::new(); + let path = root.join("foo").unwrap(); + path.create_dir().await.unwrap(); + let path = root.join("foo/bar/baz").unwrap(); + path.create_dir_all().await.unwrap(); + assert!(path.exists().await?); + assert!(root.join("foo/bar").unwrap().exists().await?); + let metadata = path.metadata().await.unwrap(); + assert_eq!(metadata.file_type, VfsFileType::Directory); + assert_eq!(metadata.len, 0); + path.create_dir_all().await.unwrap(); + root.create_dir_all().await.unwrap(); + Ok(()) + } + + #[tokio::test] + async fn create_dir_all_should_fail_for_existing_file() -> VfsResult<()>{ + let root = create_root(); + let _string = String::new(); + let path = root.join("foo").unwrap(); + let path2 = root.join("foo/bar").unwrap(); + path.create_file().await.unwrap(); + let result = path2.create_dir_all().await; + match result { + Ok(_) => {panic!("Expected error");} + Err(err) => { + let error_message = format!("{}", err); + if let VfsErrorKind::FileExists = err.kind() { + + } else { + panic!("Expected file exists error") + } + assert!( + error_message.eq("Could not create directories at '/foo/bar' for '/foo': File already exists"), + "Actual message: {}", + error_message); + } + } + Ok(()) + } + + #[tokio::test] + async fn read_dir() { + let root = create_root(); + let _string = String::new(); + root.join("foo/bar/biz").unwrap().create_dir_all().await.unwrap(); + root.join("baz").unwrap().create_file().await.unwrap(); + root.join("foo/fizz").unwrap().create_file().await.unwrap(); + let mut files: Vec<_> = root + .read_dir() + .await + .unwrap() + .map(|path| path.as_str().to_string()) + .collect() + .await; + files.sort(); + assert_eq!(files, vec!["/baz".to_string(), "/foo".to_string()]); + let mut files: Vec<_> = root + .join("foo") + .unwrap() + .read_dir() + .await + .unwrap() + .map(|path| path.as_str().to_string()) + .collect() + .await; + files.sort(); + assert_eq!(files, vec!["/foo/bar".to_string(), "/foo/fizz".to_string()]); + } + + #[tokio::test] + async fn remove_file() -> VfsResult<()> { + let root = create_root(); + let path = root.join("baz").unwrap(); + assert!(!path.exists().await?); + path.create_file().await.unwrap(); + assert!(path.exists().await?); + path.remove_file().await.unwrap(); + assert!(!path.exists().await?); + Ok(()) + } + + #[tokio::test] + async fn remove_file_nonexisting() -> VfsResult<()> { + let root = create_root(); + let path = root.join("baz").unwrap(); + assert!(!path.exists().await?); + assert!(path.remove_file().await.is_err()); + Ok(()) + } + + #[tokio::test] + async fn remove_dir() -> VfsResult<()>{ + let root = create_root(); + let path = root.join("baz").unwrap(); + assert!(!path.exists().await?); + path.create_dir().await.unwrap(); + assert!(path.exists().await?); + path.remove_dir().await.unwrap(); + assert!(!path.exists().await?); + Ok(()) + } + + #[tokio::test] + async fn remove_dir_nonexisting() -> VfsResult<()> { + let root = create_root(); + let path = root.join("baz").unwrap(); + assert!(!path.exists().await?); + assert!(path.remove_dir().await.is_err()); + Ok(()) + } + + #[tokio::test] + async fn remove_dir_notempty() { + let root = create_root(); + let path = root.join("bar").unwrap(); + root.join("bar/baz/fizz").unwrap().create_dir_all().await.unwrap(); + assert!(path.remove_dir().await.is_err()); + } + + #[tokio::test] + async fn remove_dir_all() -> VfsResult<()>{ + let root = create_root(); + let path = root.join("foo").unwrap(); + assert!(!path.exists().await?); + path.join("bar/baz/fizz").unwrap().create_dir_all().await.unwrap(); + path.join("bar/buzz").unwrap().create_file().await.unwrap(); + assert!(path.exists().await?); + assert!(path.remove_dir_all().await.is_ok()); + assert!(!path.exists().await?); + Ok(()) + } + + #[tokio::test] + async fn remove_dir_all_nonexisting() -> VfsResult<()> { + let root = create_root(); + let path = root.join("baz").unwrap(); + assert!(!path.exists().await?); + assert!(path.remove_dir_all().await.is_ok()); + Ok(()) + } + + #[test] + fn filename() { + let root = create_root(); + assert_eq!(root.filename(), ""); + assert_eq!( + root.join("name.foo.bar").unwrap().filename(), + "name.foo.bar" + ); + assert_eq!( + root.join("fizz.buzz/name.foo.bar").unwrap().filename(), + "name.foo.bar" + ); + assert_eq!( + root.join("fizz.buzz/.name.foo.bar").unwrap().filename(), + ".name.foo.bar" + ); + assert_eq!(root.join("fizz.buzz/foo.").unwrap().filename(), "foo."); + } + + #[test] + fn extension() { + let root = create_root(); + assert_eq!(root.extension(), None, "root"); + assert_eq!(root.join("name").unwrap().extension(), None, "name"); + assert_eq!( + root.join("name.bar").unwrap().extension(), + Some("bar".to_string()), + "name.bar" + ); + assert_eq!( + root.join("name.").unwrap().extension(), + Some("".to_string()), + "name." + ); + assert_eq!(root.join(".name").unwrap().extension(), None, ".name"); + assert_eq!( + root.join(".name.bar").unwrap().extension(), + Some("bar".to_string()), + ".name.bar" + ); + assert_eq!( + root.join(".name.").unwrap().extension(), + Some("".to_string()), + ".name." + ); + assert_eq!( + root.join("name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/.name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/foo.").unwrap().extension(), + Some("".to_string()) + ); + } + + #[test] + fn parent() { + let root = create_root(); + assert_eq!(root.parent(), root.clone(), "root"); + assert_eq!( + root.join("foo").unwrap().parent(), + root.clone(), + "foo" + ); + assert_eq!( + root.join("foo/bar").unwrap().parent(), + root.join("foo").unwrap(), + "foo/bar" + ); + assert_eq!( + root.join("foo/bar/baz").unwrap().parent(), + root.join("foo/bar").unwrap(), + "foo/bar/baz" + ); + } + + #[test] + fn eq() { + let root = create_root(); + + assert_eq!(root, root); + assert_eq!(root.join("foo").unwrap(), root.join("foo").unwrap()); + assert_eq!( + root.join("foo").unwrap(), + root.join("foo/bar").unwrap().parent() + ); + assert_eq!(root, root.join("foo").unwrap().parent()); + + assert_ne!(root, root.join("foo").unwrap()); + assert_ne!(root.join("bar").unwrap(), root.join("foo").unwrap()); + + let root2 = create_root(); + assert_ne!(root, root2); + assert_ne!(root.join("foo").unwrap(), root2.join("foo").unwrap()); + } + + #[test] + fn join() { + let root = create_root(); + assert_eq!(root.join("").unwrap().as_str(), ""); + assert_eq!(root.join("foo").unwrap().join("").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo/bar").unwrap().as_str(), "/foo/bar"); + assert_eq!(root.join("foo/////bar").unwrap().as_str(), "/foo/bar"); + assert_eq!(root.join("foo/bar/baz").unwrap().as_str(), "/foo/bar/baz"); + assert_eq!( + root.join("foo").unwrap().join("bar").unwrap().as_str(), + "/foo/bar" + ); + assert_eq!(root.join(".foo").unwrap().as_str(), "/.foo"); + assert_eq!(root.join("..foo").unwrap().as_str(), "/..foo"); + assert_eq!(root.join("foo.").unwrap().as_str(), "/foo."); + assert_eq!(root.join("foo..").unwrap().as_str(), "/foo.."); + + assert_eq!(root.join(".").unwrap().as_str(), ""); + assert_eq!(root.join("./foo").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo/.").unwrap().as_str(), "/foo"); + + assert_eq!(root.join("foo/..").unwrap().as_str(), ""); + assert_eq!(root.join("foo").unwrap().join("..").unwrap().as_str(), ""); + assert_eq!( + root.join("foo/bar").unwrap().join("..").unwrap().as_str(), + "/foo" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("../baz") + .unwrap() + .as_str(), + "/foo/baz" + ); + assert_eq!(root.join("foo/bar/../..").unwrap().as_str(), ""); + assert_eq!(root.join("foo/bar/../..").unwrap().as_str(), ""); + assert_eq!(root.join("foo/bar/baz/../..").unwrap().as_str(), "/foo"); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../..") + .unwrap() + .as_str(), + "/foo" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../../fizz") + .unwrap() + .as_str(), + "/foo/fizz" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../../fizz/..") + .unwrap() + .as_str(), + "/foo" + ); + assert_eq!( + root.join("..").unwrap(), + root + ); + assert_eq!( + root.join("../foo").unwrap(), + root.join("foo").unwrap() + ); + + assert_eq!(root.join("/").unwrap(), root); + assert_eq!(root.join("foo/bar").unwrap().join("/baz").unwrap(), root.join("baz").unwrap()); + + assert_eq!( + root.join("/foo/bar/baz").unwrap().join("../../..").unwrap(), + root + ); + + /// Utility function for templating the same error message + fn invalid_path_message(path: &str) -> String { + format!("An error occured for '{}': The path is invalid", path) + } + + assert_eq!( + root.join("foo/").unwrap_err().to_string(), + invalid_path_message("foo/"), + "foo/" + ); + } + + #[tokio::test] + async fn walk_dir_empty() -> VfsResult<()> { + let root = create_root(); + + assert_entries(&root, vec![]).await + } + + async fn assert_entries(path: &AsyncVfsPath, expected: Vec<&str>) -> VfsResult<()> { + let entries: Vec = path.walk_dir().await?.map(|path| path.unwrap()).collect().await; + let mut paths = entries.iter().map(|x| x.as_str()).collect::>(); + paths.sort(); + assert_eq!(paths, expected); + Ok(()) + } + + #[tokio::test] + async fn walk_dir_single_file() -> VfsResult<()> { + let root = create_root(); + root.join("baz").unwrap().create_file().await.unwrap(); + assert_entries(&root, vec!["/baz"]).await + } + + #[tokio::test] + async fn walk_dir_single_directory() -> VfsResult<()> { + let root = create_root(); + root.join("baz")?.create_dir().await?; + assert_entries(&root, vec!["/baz"]).await + } + + #[tokio::test] + async fn walk_dir_deep_directory() -> VfsResult<()> { + let root = create_root(); + root.join("foo/bar/fizz/buzz")?.create_dir_all().await?; + assert_entries( + &root, + vec!["/foo", "/foo/bar", "/foo/bar/fizz", "/foo/bar/fizz/buzz"], + ).await?; + assert_entries( + &root.join("foo")?, + vec!["/foo/bar", "/foo/bar/fizz", "/foo/bar/fizz/buzz"], + ).await + } + + #[tokio::test] + async fn walk_dir_flat() -> VfsResult<()> { + let root = create_root(); + root.join("foo/bar/foobar")?.create_dir_all().await?; + root.join("foo/baz")?.create_dir_all().await?; + root.join("foo/fizz")?.create_dir_all().await?; + root.join("foo/buzz")?.create_dir_all().await?; + root.join("foobar")?.create_dir_all().await?; + assert_entries( + &root, + vec![ + "/foo", + "/foo/bar", + "/foo/bar/foobar", + "/foo/baz", + "/foo/buzz", + "/foo/fizz", + "/foobar", + ], + ).await?; + assert_entries( + &root.join("foo")?, + vec![ + "/foo/bar", + "/foo/bar/foobar", + "/foo/baz", + "/foo/buzz", + "/foo/fizz", + ], + ).await + } + + #[tokio::test] + async fn walk_dir_file_in_dir() -> VfsResult<()> { + let root = create_root(); + root.join("foo/bar")?.create_dir_all().await?; + root.join("foo/bar/foobar")?.create_file().await?; + assert_entries(&root, vec!["/foo", "/foo/bar", "/foo/bar/foobar"]).await?; + assert_entries(&root.join("foo")?, vec!["/foo/bar", "/foo/bar/foobar"]).await + } + + #[tokio::test] + async fn walk_dir_missing_path() -> VfsResult<()> { + let root = create_root(); + let error_message = root + .join("foo")? + .walk_dir() + .await + .expect_err("walk_dir") + .to_string(); + assert!( + error_message.starts_with("Could not read directory for '/foo'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn walk_dir_remove_directory_while_walking() -> VfsResult<()> { + let root = create_root(); + root.join("foo")?.create_dir_all().await?; + let mut walker = root.walk_dir().await?; + assert_eq!(format!("{:?}", &walker), "WalkDirIterator[]"); + + assert_eq!(walker.next().await.expect("foo")?.as_str(), "/foo"); + root.join("foo")?.remove_dir().await?; + let error_message = walker + .next() + .await + .expect("no next") + .expect_err("walk_dir") + .to_string(); + assert!( + error_message.starts_with("Could not read directory for '/foo'"), + "Actual message: {}", + error_message + ); + let next = walker.next().await; + assert!(next.is_none(), "Got next: {:?}", next); + Ok(()) + } + + #[tokio::test] + async fn read_to_string() -> VfsResult<()> { + let root = create_root(); + let path = root.join("foobar.txt")?; + path.create_file().await?.write_all(b"Hello World").await?; + assert_eq!(path.read_to_string().await?, "Hello World"); + Ok(()) + } + + #[tokio::test] + async fn read_to_string_missing() -> VfsResult<()> { + let root = create_root(); + let error_message = root.join("foobar.txt")?.read_to_string().await.expect_err("read_to_string").to_string(); + assert!( + error_message.starts_with("Could not get metadata for '/foobar.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn read_to_string_directory() -> VfsResult<()> { + let root = create_root(); + root.join("foobar.txt")?.create_dir().await?; + let error_message = root.join("foobar.txt")?.read_to_string().await.expect_err("read_to_string").to_string(); + assert!( + error_message.starts_with("Could not read path for '/foobar.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn read_to_string_nonutf8() -> VfsResult<()> { + let root = create_root(); + let path = root.join("foobar.txt")?; + path.create_file().await?.write_all(&vec![0, 159, 146, 150]).await?; + let error_message = path.read_to_string().await.expect_err("read_to_string").to_string(); + assert_eq!( + &error_message, + "Could not read path for '/foobar.txt': IO error: stream did not contain valid UTF-8" + ); + Ok(()) + } + + #[tokio::test] + async fn copy_file() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + src.copy_file(&dest).await?; + assert_eq!(&dest.read_to_string().await?, "Hello World"); + Ok(()) + } + + #[tokio::test] + async fn copy_file_not_exist() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + + let error_message = src.copy_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not copy '/a.txt' to '/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_file_dest_already_exist() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + dest.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.copy_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not copy '/a.txt' to '/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_file_parent_directory_missing() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("x/b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.copy_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not copy '/a.txt' to '/x/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_file_parent_directory_is_file() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("a.txt/b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.copy_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not copy '/a.txt' to '/a.txt/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_file_to_root() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.copy_file(&root).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not copy '/a.txt' to ''"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_file() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + src.move_file(&dest).await?; + assert_eq!(&dest.read_to_string().await?, "Hello World"); + assert!(!src.exists().await?, "Source should not exist anymore"); + Ok(()) + } + + #[tokio::test] + async fn move_file_not_exist() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + + let error_message = src.move_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not move '/a.txt' to '/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_file_dest_already_exist() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + dest.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.move_file(&dest).await.expect_err("move_file").to_string(); + assert!( + error_message.starts_with("Could not move '/a.txt' to '/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + #[tokio::test] + async fn move_file_parent_directory_missing() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("x/b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.move_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not move '/a.txt' to '/x/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_file_parent_directory_is_file() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + let dest = root.join("a.txt/b.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.move_file(&dest).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not move '/a.txt' to '/a.txt/b.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_file_to_root() -> VfsResult<()> { + let root = create_root(); + let src = root.join("a.txt")?; + src.create_file().await?.write_all(b"Hello World").await?; + + let error_message = src.move_file(&root).await.expect_err("copy_file").to_string(); + assert!( + error_message.starts_with("Could not move '/a.txt' to ''"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_dir() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.join("bar/biz/fizz/buzz")?.create_dir_all().await?; + src.join("bar/baz.txt")?.create_file().await?.write_all(b"Hello World").await?; + + let dest = root.join("foo2")?; + assert_eq!(5, src.copy_dir(&dest).await?); + assert_eq!(&dest.join("bar/baz.txt")?.read_to_string().await?, "Hello World"); + assert!(&dest.join("bar/biz/fizz/buzz")?.exists().await?, "directory should exist"); + Ok(()) + } + + #[tokio::test] + async fn copy_dir_to_root() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.create_dir_all().await?; + let error_message = src.copy_dir(&root).await.expect_err("copy_dir").to_string(); + assert!( + error_message.starts_with("Could not copy directory '/foo' to ''"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn copy_dir_to_existing() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.create_dir_all().await?; + let dest = root.join("foo2")?; + dest.create_dir_all().await?; + + let error_message = src.copy_dir(&dest).await.expect_err("copy_dir").to_string(); + assert!( + error_message.starts_with("Could not copy directory '/foo' to '/foo2'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_dir() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.join("bar/biz/fizz/buzz")?.create_dir_all().await?; + src.join("bar/baz.txt")?.create_file().await?.write_all(b"Hello World").await?; + + let dest = root.join("foo2")?; + src.move_dir(&dest).await?; + assert_eq!(&dest.join("bar/baz.txt")?.read_to_string().await?, "Hello World"); + assert!(&dest.join("bar/biz/fizz/buzz")?.exists().await?, "directory should exist"); + assert!(!src.exists().await?, "source directory should not exist"); + Ok(()) + } + + #[tokio::test] + async fn move_dir_to_root() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.create_dir_all().await?; + let error_message = src.move_dir(&root).await.expect_err("move_dir").to_string(); + assert!( + error_message.starts_with("Could not move directory '/foo' to ''"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn move_dir_to_existing() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + src.create_dir_all().await?; + let dest = root.join("foo2")?; + dest.create_dir_all().await?; + + let error_message = src.move_dir(&dest).await.expect_err("move_dir").to_string(); + assert!( + error_message.starts_with("Could not move directory '/foo' to '/foo2'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn is_file_is_dir() -> VfsResult<()> { + let root = create_root(); + let src = root.join("foo")?; + + assert!(!root.is_file().await?); + assert!(root.is_dir().await?); + + assert!(!src.is_file().await?); + assert!(!src.is_dir().await?); + + src.create_dir_all().await?; + assert!(!src.is_file().await?); + assert!(src.is_dir().await?); + + src.remove_dir().await?; + assert!(!src.is_file().await?); + assert!(!src.is_dir().await?); + + src.create_file().await?; + assert!(src.is_file().await?); + assert!(!src.is_dir().await?); + + src.remove_file().await?; + assert!(!src.is_file().await?); + assert!(!src.is_dir().await?); + Ok(()) + } + + } + }; +} + +/// Run readonly vfs test to check for conformance +#[macro_export] +macro_rules! test_async_vfs_readonly { + ($root:expr) => { + #[cfg(test)] + mod vfs_tests_readonly { + use super::*; + use futures::stream::StreamExt; + use $crate::async_vfs::AsyncVfsPath; + use $crate::{VfsFileType, VfsResult}; + + fn create_root() -> AsyncVfsPath { + $root.into() + } + + #[test] + fn vfs_can_be_created() { + create_root(); + } + + #[tokio::test] + async fn read_file() -> VfsResult<()> { + let root = create_root(); + let path = root.join("a.txt").unwrap(); + { + let mut file = path.open_file().await.unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).await.unwrap(); + assert_eq!(string, "a"); + } + assert!(path.exists().await?); + let metadata = path.metadata().await?; + assert_eq!(metadata.len, 1); + assert_eq!(metadata.file_type, VfsFileType::File); + Ok(()) + } + + #[tokio::test] + async fn read_dir() { + let root = create_root(); + let mut files: Vec<_> = root + .read_dir() + .await + .unwrap() + .map(|path| path.as_str().to_string()) + .collect() + .await; + files.sort(); + assert_eq!( + files, + vec!["/a", "/a.txt", "/a.txt.dir", "/b.txt", "/c"] + .into_iter() + .map(String::from) + .collect::>() + ); + + let mut files: Vec<_> = root + .join("a") + .unwrap() + .read_dir() + .await + .unwrap() + .map(|path| path.as_str().to_string()) + .collect() + .await; + files.sort(); + assert_eq!(files, vec!["/a/d.txt".to_string(), "/a/x".to_string()]); + } + + #[test] + fn filename() { + let root = create_root(); + assert_eq!(root.filename(), ""); + assert_eq!( + root.join("name.foo.bar").unwrap().filename(), + "name.foo.bar" + ); + assert_eq!( + root.join("fizz.buzz/name.foo.bar").unwrap().filename(), + "name.foo.bar" + ); + assert_eq!( + root.join("fizz.buzz/.name.foo.bar").unwrap().filename(), + ".name.foo.bar" + ); + assert_eq!(root.join("fizz.buzz/foo.").unwrap().filename(), "foo."); + } + + #[test] + fn extension() { + let root = create_root(); + assert_eq!(root.extension(), None, "root"); + assert_eq!(root.join("name").unwrap().extension(), None, "name"); + assert_eq!( + root.join("name.bar").unwrap().extension(), + Some("bar".to_string()), + "name.bar" + ); + assert_eq!( + root.join("name.").unwrap().extension(), + Some("".to_string()), + "name." + ); + assert_eq!(root.join(".name").unwrap().extension(), None, ".name"); + assert_eq!( + root.join(".name.bar").unwrap().extension(), + Some("bar".to_string()), + ".name.bar" + ); + assert_eq!( + root.join(".name.").unwrap().extension(), + Some("".to_string()), + ".name." + ); + assert_eq!( + root.join("name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/.name.foo.bar").unwrap().extension(), + Some("bar".to_string()) + ); + assert_eq!( + root.join("fizz.buzz/foo.").unwrap().extension(), + Some("".to_string()) + ); + } + + #[test] + fn parent() { + let root = create_root(); + assert_eq!(root.parent(), root.clone(), "root"); + assert_eq!(root.join("foo").unwrap().parent(), root.clone(), "foo"); + assert_eq!( + root.join("foo/bar").unwrap().parent(), + root.join("foo").unwrap(), + "foo/bar" + ); + assert_eq!( + root.join("foo/bar/baz").unwrap().parent(), + root.join("foo/bar").unwrap(), + "foo/bar/baz" + ); + } + + #[test] + fn root() { + let root = create_root(); + assert_eq!(root, root.root()); + assert_eq!(root.join("foo/bar").unwrap().root(), root.root()); + } + + #[test] + fn eq() { + let root = create_root(); + + assert_eq!(root, root); + assert_eq!(root.join("foo").unwrap(), root.join("foo").unwrap()); + assert_eq!( + root.join("foo").unwrap(), + root.join("foo/bar").unwrap().parent() + ); + assert_eq!(root, root.join("foo").unwrap().parent()); + + assert_ne!(root, root.join("foo").unwrap()); + assert_ne!(root.join("bar").unwrap(), root.join("foo").unwrap()); + + let root2 = create_root(); + assert_ne!(root, root2); + assert_ne!(root.join("foo").unwrap(), root2.join("foo").unwrap()); + } + + #[test] + fn join() { + let root = create_root(); + assert_eq!(root.join("").unwrap().as_str(), ""); + assert_eq!(root.join("foo").unwrap().join("").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo/bar").unwrap().as_str(), "/foo/bar"); + assert_eq!(root.join("foo/bar/baz").unwrap().as_str(), "/foo/bar/baz"); + assert_eq!( + root.join("foo").unwrap().join("bar").unwrap().as_str(), + "/foo/bar" + ); + assert_eq!(root.join(".foo").unwrap().as_str(), "/.foo"); + assert_eq!(root.join("..foo").unwrap().as_str(), "/..foo"); + assert_eq!(root.join("foo.").unwrap().as_str(), "/foo."); + assert_eq!(root.join("foo..").unwrap().as_str(), "/foo.."); + + assert_eq!(root.join(".").unwrap().as_str(), ""); + assert_eq!(root.join("./foo").unwrap().as_str(), "/foo"); + assert_eq!(root.join("foo/.").unwrap().as_str(), "/foo"); + + assert_eq!(root.join("foo/..").unwrap().as_str(), ""); + assert_eq!(root.join("foo").unwrap().join("..").unwrap().as_str(), ""); + assert_eq!( + root.join("foo/bar").unwrap().join("..").unwrap().as_str(), + "/foo" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("../baz") + .unwrap() + .as_str(), + "/foo/baz" + ); + assert_eq!(root.join("foo/bar/../..").unwrap().as_str(), ""); + assert_eq!(root.join("foo/bar/../..").unwrap().as_str(), ""); + assert_eq!(root.join("foo/bar/baz/../..").unwrap().as_str(), "/foo"); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../..") + .unwrap() + .as_str(), + "/foo" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../../fizz") + .unwrap() + .as_str(), + "/foo/fizz" + ); + assert_eq!( + root.join("foo/bar") + .unwrap() + .join("baz/../../fizz/..") + .unwrap() + .as_str(), + "/foo" + ); + assert_eq!(root.join("..").unwrap(), root); + assert_eq!(root.join("../foo").unwrap(), root.join("foo").unwrap()); + + assert_eq!(root.join("/").unwrap(), root); + assert_eq!( + root.join("foo/bar").unwrap().join("/baz").unwrap(), + root.join("baz").unwrap() + ); + + assert_eq!( + root.join("/foo/bar/baz").unwrap().join("../../..").unwrap(), + root + ); + + /// Utility function for templating the same error message + // TODO: Maybe deduplicate this function + fn invalid_path_message(path: &str) -> String { + format!("An error occured for '{}': The path is invalid", path) + } + + assert_eq!( + root.join("foo/").unwrap_err().to_string(), + invalid_path_message("foo/"), + "foo/" + ); + } + + #[tokio::test] + async fn walk_dir_root() -> VfsResult<()> { + let root = create_root(); + + assert_entries( + &root, + vec![ + "/a", + "/a.txt", + "/a.txt.dir", + "/a.txt.dir/g.txt", + "/a/d.txt", + "/a/x", + "/a/x/y", + "/a/x/y/z", + "/b.txt", + "/c", + "/c/e.txt", + ], + ) + .await + } + + #[tokio::test] + async fn walk_dir_folder() -> VfsResult<()> { + let root = create_root(); + + assert_entries( + &root.join("a")?, + vec!["/a/d.txt", "/a/x", "/a/x/y", "/a/x/y/z"], + ) + .await + } + + #[tokio::test] + async fn walk_dir_nested() -> VfsResult<()> { + let root = create_root(); + + assert_entries(&root.join("a/x/y")?, vec!["/a/x/y/z"]).await + } + + async fn assert_entries(path: &AsyncVfsPath, expected: Vec<&str>) -> VfsResult<()> { + let entries: Vec = path + .walk_dir() + .await? + .map(|path| path.unwrap()) + .collect() + .await; + let mut paths = entries.iter().map(|x| x.as_str()).collect::>(); + paths.sort(); + assert_eq!(paths, expected); + Ok(()) + } + + #[tokio::test] + async fn walk_dir_missing_path() -> VfsResult<()> { + let root = create_root(); + let error_message = root + .join("foo")? + .walk_dir() + .await + .expect_err("walk_dir") + .to_string(); + assert!( + error_message.starts_with("Could not read directory for '/foo'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn read_to_string() -> VfsResult<()> { + let root = create_root(); + let path = root.join("a.txt")?; + assert_eq!(path.read_to_string().await?, "a"); + Ok(()) + } + + #[tokio::test] + async fn read_to_string_missing() -> VfsResult<()> { + let root = create_root(); + let error_message = root + .join("foobar.txt")? + .read_to_string() + .await + .expect_err("read_to_string") + .to_string(); + assert!( + error_message.starts_with("Could not get metadata for '/foobar.txt'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn read_to_string_directory() -> VfsResult<()> { + let root = create_root(); + let error_message = root + .join("a")? + .read_to_string() + .await + .expect_err("read_to_string") + .to_string(); + assert!( + error_message.starts_with("Could not read path for '/a'"), + "Actual message: {}", + error_message + ); + Ok(()) + } + + #[tokio::test] + async fn is_file_is_dir() -> VfsResult<()> { + let root = create_root(); + + assert!(!root.is_file().await?); + assert!(root.is_dir().await?); + + let missing = root.join("foo")?; + + assert!(!missing.is_file().await?); + assert!(!missing.is_dir().await?); + + let a = root.join("a")?; + assert!(!a.is_file().await?); + assert!(a.is_dir().await?); + + let atxt = root.join("a.txt")?; + assert!(atxt.is_file().await?); + assert!(!atxt.is_dir().await?); + + Ok(()) + } + } + }; +} diff --git a/awkernel_lib/src/file/vfs/error.rs b/awkernel_lib/src/file/vfs/error.rs new file mode 100644 index 000000000..631b8ebd6 --- /dev/null +++ b/awkernel_lib/src/file/vfs/error.rs @@ -0,0 +1,190 @@ +//! Error and Result definitions + +use super::super::error::IoError; +use super::super::io; +use alloc::{ + boxed::Box, + string::{String, ToString}, +}; +use core::fmt; + +/// The error type of this crate +#[derive(Debug)] +pub struct VfsError { + /// The path this error was encountered in + path: String, + /// The kind of error + kind: VfsErrorKind, + /// An optional human-readable string describing the context for this error + /// + /// If not provided, a generic context message is used + context: String, + /// The underlying error + cause: Option>>, +} + +/// The only way to create a VfsError is via a VfsErrorKind +/// +/// This conversion implements certain normalizations +impl From> for VfsError { + fn from(kind: VfsErrorKind) -> Self { + Self { + path: "PATH NOT FILLED BY VFS LAYER".into(), + kind, + context: "An error occured".into(), + cause: None, + } + } +} + +impl From for VfsError { + fn from(err: E) -> Self { + let kind = VfsErrorKind::IoError(err); + Self { + path: "PATH NOT FILLED BY VFS LAYER".into(), + kind, + context: "An error occurred".into(), + cause: None, + } + } +} + +impl VfsError { + // Path filled by the VFS crate rather than the implementations + pub(crate) fn with_path(mut self, path: impl Into) -> Self { + self.path = path.into(); + self + } + + pub fn with_context(mut self, context: F) -> Self + where + C: fmt::Display + Send + Sync + 'static, + F: FnOnce() -> C, + { + self.context = context().to_string(); + self + } + + pub fn with_cause(mut self, cause: VfsError) -> Self { + self.cause = Some(Box::new(cause)); + self + } + + pub fn kind(&self) -> &VfsErrorKind { + &self.kind + } + + pub fn path(&self) -> &String { + &self.path + } +} + +impl fmt::Display for VfsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} for '{}': {}", self.context, self.path, self.kind()) + } +} + +/// The kinds of errors that can occur +#[derive(Debug)] +pub enum VfsErrorKind { + /// A generic I/O error + /// + /// Certain standard I/O errors are normalized to their VfsErrorKind counterparts + IoError(E), + + #[cfg(feature = "async-vfs")] + /// A generic async I/O error + AsyncIoError(io::Error), + + /// The file or directory at the given path could not be found + FileNotFound, + + /// The given path is invalid, e.g. because contains '.' or '..' + InvalidPath, + + /// Generic error variant + Other(String), + + /// There is already a directory at the given path + DirectoryExists, + + /// There is already a file at the given path + FileExists, + + /// Functionality not supported by this filesystem + NotSupported, +} + +impl fmt::Display for VfsErrorKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + VfsErrorKind::IoError(cause) => { + write!(f, "IO error: {}", cause) + } + #[cfg(feature = "async-vfs")] + VfsErrorKind::AsyncIoError(cause) => { + write!(f, "Async IO error: {}", cause) + } + VfsErrorKind::FileNotFound => { + write!(f, "The file or directory could not be found") + } + VfsErrorKind::InvalidPath => { + write!(f, "The path is invalid") + } + VfsErrorKind::Other(message) => { + write!(f, "FileSystem error: {}", message) + } + VfsErrorKind::NotSupported => { + write!(f, "Functionality not supported by this filesystem") + } + VfsErrorKind::DirectoryExists => { + write!(f, "Directory already exists") + } + VfsErrorKind::FileExists => { + write!(f, "File already exists") + } + } + } +} + +/// The result type of this crate +pub type VfsResult = core::result::Result>; + +#[cfg(test)] +mod tests { + use crate::error::VfsErrorKind; + use crate::{VfsError, VfsResult}; + + fn produce_vfs_result() -> VfsResult<()> { + Err(VfsError::from(VfsErrorKind::Other("Not a file".into())).with_path("foo")) + } + + fn produce_anyhow_result() -> anyhow::Result<()> { + Ok(produce_vfs_result()?) + } + + #[test] + fn anyhow_compatibility() { + let result = produce_anyhow_result().unwrap_err(); + assert_eq!( + result.to_string(), + "An error occured for 'foo': FileSystem error: Not a file" + ) + } +} + +impl IoError for VfsError { + fn is_interrupted(&self) -> bool { + todo!(); + } + fn new_unexpected_eof_error() -> Self { + todo!(); + } + fn new_write_zero_error() -> Self { + todo!(); + } + fn other_error() -> Self { + todo!(); + } +} diff --git a/awkernel_lib/src/file/vfs/filesystem.rs b/awkernel_lib/src/file/vfs/filesystem.rs new file mode 100644 index 000000000..1e74fc547 --- /dev/null +++ b/awkernel_lib/src/file/vfs/filesystem.rs @@ -0,0 +1,85 @@ +//! The filesystem trait definitions needed to implement new virtual filesystems + +use super::super::error::IoError; +use super::error::VfsErrorKind; +use super::{ + error::VfsError, error::VfsResult, path::SeekAndRead, path::SeekAndWrite, path::VfsMetadata, + path::VfsPath, +}; +use crate::time::Time; +use alloc::{boxed::Box, string::String}; +use core::fmt::Debug; + +/// File system implementations must implement this trait +/// All path parameters are absolute, starting with '/', except for the root directory +/// which is simply the empty string (i.e. "") +/// The character '/' is used to delimit directories on all platforms. +/// Path components may be any UTF-8 string, except "/", "." and ".." +/// +/// Please use the test_macros [test_macros::test_vfs!] and [test_macros::test_vfs_readonly!] +pub trait FileSystem: Debug + Sync + Send + 'static { + type Error: IoError + Clone; + /// Iterates over all direct children of this directory path + /// NOTE: the returned String items denote the local bare filenames, i.e. they should not contain "/" anywhere + fn read_dir( + &self, + path: &str, + ) -> VfsResult + Send>, Self::Error>; + /// Creates the directory at this path + /// + /// Note that the parent directory must already exist. + fn create_dir(&self, path: &str) -> VfsResult<(), Self::Error>; + /// Opens the file at this path for reading + fn open_file( + &self, + path: &str, + ) -> VfsResult> + Send>, Self::Error>; + /// Creates a file at this path for writing + fn create_file( + &self, + path: &str, + ) -> VfsResult> + Send>, Self::Error>; + /// Opens the file at this path for appending + fn append_file( + &self, + path: &str, + ) -> VfsResult> + Send>, Self::Error>; + /// Returns the file metadata for the file at this path + fn metadata(&self, path: &str) -> VfsResult; + /// Sets the files creation timestamp, if the implementation supports it + fn set_creation_time(&self, _path: &str, _time: Time) -> VfsResult<(), Self::Error> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Sets the files modification timestamp, if the implementation supports it + fn set_modification_time(&self, _path: &str, _time: Time) -> VfsResult<(), Self::Error> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Sets the files access timestamp, if the implementation supports it + fn set_access_time(&self, _path: &str, _time: Time) -> VfsResult<(), Self::Error> { + Err(VfsError::from(VfsErrorKind::NotSupported)) + } + /// Returns true if a file or directory at path exists, false otherwise + fn exists(&self, path: &str) -> VfsResult; + /// Removes the file at this path + fn remove_file(&self, path: &str) -> VfsResult<(), Self::Error>; + /// Removes the directory at this path + fn remove_dir(&self, path: &str) -> VfsResult<(), Self::Error>; + /// Copies the src path to the destination path within the same filesystem (optional) + fn copy_file(&self, _src: &str, _dest: &str) -> VfsResult<(), Self::Error> { + Err(VfsErrorKind::NotSupported.into()) + } + /// Moves the src path to the destination path within the same filesystem (optional) + fn move_file(&self, _src: &str, _dest: &str) -> VfsResult<(), Self::Error> { + Err(VfsErrorKind::NotSupported.into()) + } + /// Moves the src directory to the destination path within the same filesystem (optional) + fn move_dir(&self, _src: &str, _dest: &str) -> VfsResult<(), Self::Error> { + Err(VfsErrorKind::NotSupported.into()) + } +} + +impl From for VfsPath { + fn from(filesystem: T) -> Self { + VfsPath::new(filesystem) + } +} diff --git a/awkernel_lib/src/file/vfs/impls/altroot.rs b/awkernel_lib/src/file/vfs/impls/altroot.rs new file mode 100644 index 000000000..ef7f92bcf --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/altroot.rs @@ -0,0 +1,144 @@ +//! A file system with its root in a particular directory of another filesystem + +use crate::{ + error::VfsErrorKind, FileSystem, SeekAndRead, SeekAndWrite, VfsMetadata, VfsPath, VfsResult, +}; + +use std::time::SystemTime; + +/// Similar to a chroot but done purely by path manipulation +/// +/// NOTE: This mechanism should only be used for convenience, NOT FOR SECURITY +/// +/// Symlinks, hardlinks, remounts, side channels and other file system mechanisms can be exploited +/// to circumvent this mechanism +#[derive(Debug, Clone)] +pub struct AltrootFS { + root: VfsPath, +} + +impl AltrootFS { + /// Create a new root FileSystem at the given virtual path + pub fn new(root: VfsPath) -> Self { + AltrootFS { root } + } +} + +impl AltrootFS { + #[allow(clippy::manual_strip)] // strip prefix manually for MSRV 1.32 + fn path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.root.clone()); + } + if path.starts_with('/') { + return self.root.join(&path[1..]); + } + self.root.join(path) + } +} + +impl FileSystem for AltrootFS { + fn read_dir(&self, path: &str) -> VfsResult + Send>> { + self.path(path)? + .read_dir() + .map(|result| result.map(|path| path.filename())) + .map(|entries| Box::new(entries) as Box + Send>) + } + + fn create_dir(&self, path: &str) -> VfsResult<()> { + self.path(path)?.create_dir() + } + + fn open_file(&self, path: &str) -> VfsResult + Send>> { + self.path(path)?.open_file() + } + + fn create_file(&self, path: &str) -> VfsResult + Send>> { + self.path(path)?.create_file() + } + + fn append_file(&self, path: &str) -> VfsResult + Send>> { + self.path(path)?.append_file() + } + + fn metadata(&self, path: &str) -> VfsResult { + self.path(path)?.metadata() + } + + fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_creation_time(time) + } + + fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_modification_time(time) + } + + fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.path(path)?.set_access_time(time) + } + + fn exists(&self, path: &str) -> VfsResult { + self.path(path) + .map(|path| path.exists()) + .unwrap_or(Ok(false)) + } + + fn remove_file(&self, path: &str) -> VfsResult<()> { + self.path(path)?.remove_file() + } + + fn remove_dir(&self, path: &str) -> VfsResult<()> { + self.path(path)?.remove_dir() + } + + fn copy_file(&self, src: &str, dest: &str) -> VfsResult<()> { + if dest.is_empty() { + return Err(VfsErrorKind::NotSupported.into()); + } + self.path(src)?.copy_file(&self.path(dest)?) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MemoryFS; + test_vfs!({ + let memory_root: VfsPath = MemoryFS::new().into(); + let altroot_path = memory_root.join("altroot").unwrap(); + altroot_path.create_dir().unwrap(); + AltrootFS::new(altroot_path) + }); + + #[test] + fn parent() { + let memory_root: VfsPath = MemoryFS::new().into(); + let altroot_path = memory_root.join("altroot").unwrap(); + altroot_path.create_dir().unwrap(); + let altroot: VfsPath = AltrootFS::new(altroot_path.clone()).into(); + assert_eq!(altroot.parent(), altroot.root()); + assert_eq!(altroot_path.parent(), memory_root); + } +} + +#[cfg(test)] +mod tests_physical { + use super::*; + use crate::PhysicalFS; + test_vfs!({ + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + std::fs::create_dir_all(&dir).unwrap(); + + let physical_root: VfsPath = PhysicalFS::new(dir).into(); + let altroot_path = physical_root.join("altroot").unwrap(); + altroot_path.create_dir().unwrap(); + AltrootFS::new(altroot_path) + }); + + test_vfs_readonly!({ + let physical_root: VfsPath = PhysicalFS::new("test").into(); + let altroot_path = physical_root.join("test_directory").unwrap(); + AltrootFS::new(altroot_path) + }); +} diff --git a/awkernel_lib/src/file/vfs/impls/embedded.rs b/awkernel_lib/src/file/vfs/impls/embedded.rs new file mode 100644 index 000000000..eb3fb9276 --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/embedded.rs @@ -0,0 +1,461 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::io::Cursor; +use std::marker::PhantomData; +use std::time::{Duration, SystemTime}; + +use rust_embed::RustEmbed; + +use crate::error::VfsErrorKind; +use crate::{FileSystem, SeekAndRead, SeekAndWrite, VfsFileType, VfsMetadata, VfsResult}; + +type EmbeddedPath = Cow<'static, str>; + +#[derive(Debug)] +/// a read-only file system embedded in the executable +/// see [rust-embed](https://docs.rs/rust-embed/) for how to create a `RustEmbed` +pub struct EmbeddedFS +where + T: RustEmbed + Send + Sync + Debug + 'static, +{ + p: PhantomData, + directory_map: HashMap>, + files: HashMap, +} + +impl EmbeddedFS +where + T: RustEmbed + Send + Sync + Debug + 'static, +{ + pub fn new() -> Self { + let mut directory_map: HashMap> = Default::default(); + let mut files: HashMap = Default::default(); + for file in T::iter() { + let mut path = file.clone(); + files.insert( + file.clone(), + T::get(&path).expect("Path should exist").data.len() as u64, + ); + while let Some((prefix, suffix)) = rsplit_once_cow(&path, "/") { + let children = directory_map.entry(prefix.clone()).or_default(); + children.insert(suffix); + path = prefix; + } + let children = directory_map.entry("".into()).or_default(); + children.insert(path); + } + EmbeddedFS { + p: PhantomData, + directory_map, + files, + } + } +} + +impl Default for EmbeddedFS +where + T: RustEmbed + Send + Sync + Debug + 'static, +{ + fn default() -> Self { + Self::new() + } +} + +fn rsplit_once_cow(input: &EmbeddedPath, delimiter: &str) -> Option<(EmbeddedPath, EmbeddedPath)> { + let mut result: Vec<_> = match input { + EmbeddedPath::Borrowed(s) => s.rsplitn(2, delimiter).map(Cow::Borrowed).collect(), + EmbeddedPath::Owned(s) => s + .rsplitn(2, delimiter) + .map(|a| Cow::Owned(a.to_string())) + .collect(), + }; + if result.len() == 2 { + Some((result.remove(1), result.remove(0))) + } else { + None + } +} + +impl FileSystem for EmbeddedFS +where + T: RustEmbed + Send + Sync + Debug + 'static, +{ + fn read_dir(&self, path: &str) -> VfsResult + Send>> { + let normalized_path = normalize_path(path)?; + if let Some(children) = self.directory_map.get(normalized_path) { + Ok(Box::new( + children.clone().into_iter().map(|path| path.into_owned()), + )) + } else { + if self.files.contains_key(normalized_path) { + // Actually a file + return Err(VfsErrorKind::Other("Not a directory".into()).into()); + } + Err(VfsErrorKind::FileNotFound.into()) + } + } + + fn create_dir(&self, _path: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn open_file(&self, path: &str) -> VfsResult + Send>> { + match T::get(path.split_at(1).1) { + None => Err(VfsErrorKind::FileNotFound.into()), + Some(file) => Ok(Box::new(Cursor::new(file.data))), + } + } + + fn create_file( + &self, + _path: &str, + ) -> VfsResult + Send>> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn append_file( + &self, + _path: &str, + ) -> VfsResult + Send>> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn metadata(&self, path: &str) -> VfsResult { + let normalized_path = normalize_path(path)?; + if let Some(len) = self.files.get(normalized_path) { + return match T::get(path.split_at(1).1) { + None => Err(VfsErrorKind::FileNotFound.into()), + Some(file) => Ok(VfsMetadata { + file_type: VfsFileType::File, + len: *len, + modified: file + .metadata + .last_modified() + .map(|secs| SystemTime::UNIX_EPOCH + Duration::from_secs(secs)), + created: file + .metadata + .created() + .map(|secs| SystemTime::UNIX_EPOCH + Duration::from_secs(secs)), + accessed: None, + }), + }; + } + if self.directory_map.contains_key(normalized_path) { + return Ok(VfsMetadata { + file_type: VfsFileType::Directory, + len: 0, + modified: None, + created: None, + accessed: None, + }); + } + Err(VfsErrorKind::FileNotFound.into()) + } + + fn exists(&self, path: &str) -> VfsResult { + let path = normalize_path(path)?; + if self.files.contains_key(path) { + return Ok(true); + } + if self.directory_map.contains_key(path) { + return Ok(true); + } + if path.is_empty() { + // Root always exists + return Ok(true); + } + Ok(false) + } + + fn remove_file(&self, _path: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } + + fn remove_dir(&self, _path: &str) -> VfsResult<()> { + Err(VfsErrorKind::NotSupported.into()) + } +} + +fn normalize_path(path: &str) -> VfsResult<&str> { + if path.is_empty() { + return Ok(""); + } + let path = &path[1..]; + Ok(path) +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + use std::io::Read; + + use crate::{FileSystem, VfsFileType, VfsPath}; + + use super::*; + + #[derive(RustEmbed, Debug)] + #[folder = "test/test_directory"] + struct TestEmbed; + + fn get_test_fs() -> EmbeddedFS { + EmbeddedFS::new() + } + + test_vfs_readonly!({ get_test_fs() }); + #[test] + fn read_dir_lists_directory() { + let fs = get_test_fs(); + assert_eq!( + fs.read_dir("/").unwrap().collect::>(), + vec!["a", "a.txt.dir", "c", "a.txt", "b.txt"] + .into_iter() + .map(String::from) + .collect::>() + ); + assert_eq!( + fs.read_dir("/a").unwrap().collect::>(), + vec!["d.txt", "x"] + .into_iter() + .map(String::from) + .collect::>() + ); + assert_eq!( + fs.read_dir("/a.txt.dir").unwrap().collect::>(), + vec!["g.txt"] + .into_iter() + .map(String::from) + .collect::>() + ); + } + + #[test] + fn read_dir_no_directory_err() { + let fs = get_test_fs(); + assert!(match fs.read_dir("/c/f").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + }); + assert!( + match fs.read_dir("/a.txt.").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + } + ); + assert!( + match fs.read_dir("/abc/def/ghi").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + } + ); + } + + #[test] + fn read_dir_on_file_err() { + let fs = get_test_fs(); + assert!( + match fs.read_dir("/a.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::Other(message) => message == "Not a directory", + _ => false, + } + ); + assert!( + match fs.read_dir("/a/d.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::Other(message) => message == "Not a directory", + _ => false, + } + ); + } + + #[test] + fn create_dir_not_supported() { + let fs = get_test_fs(); + assert!( + match fs.create_dir("/abc").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::NotSupported => true, + _ => false, + } + ) + } + + #[test] + fn open_file() { + let fs = get_test_fs(); + let mut text = String::new(); + fs.open_file("/a.txt") + .unwrap() + .read_to_string(&mut text) + .unwrap(); + assert_eq!(text, "a"); + } + + #[test] + fn open_empty_file() { + let fs = get_test_fs(); + let mut text = String::new(); + fs.open_file("/a.txt.dir/g.txt") + .unwrap() + .read_to_string(&mut text) + .unwrap(); + assert_eq!(text, ""); + } + + #[test] + fn open_file_not_found() { + let fs = get_test_fs(); + // FIXME: These tests have been weakened since the FS implementations aren't intended to + // provide paths for errors. Maybe this could be handled better + assert!(match fs.open_file("/") { + Err(err) => match err.kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + }, + _ => false, + }); + assert!(match fs.open_file("/abc.txt") { + Err(err) => match err.kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + }, + _ => false, + }); + assert!(match fs.open_file("/c/f.txt") { + Err(err) => match err.kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + }, + _ => false, + }); + } + + #[test] + fn create_file_not_supported() { + let fs = get_test_fs(); + assert!( + match fs.create_file("/abc.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::NotSupported => true, + _ => false, + } + ); + } + + #[test] + fn append_file_not_supported() { + let fs = get_test_fs(); + assert!( + match fs.append_file("/abc.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::NotSupported => true, + _ => false, + } + ); + } + + #[test] + fn metadata_file() { + let fs = get_test_fs(); + let d = fs.metadata("/a/d.txt").unwrap(); + assert_eq!(d.len, 1); + assert_eq!(d.file_type, VfsFileType::File); + + let g = fs.metadata("/a.txt.dir/g.txt").unwrap(); + assert_eq!(g.len, 0); + assert_eq!(g.file_type, VfsFileType::File); + } + + #[test] + fn metadata_directory() { + let fs = get_test_fs(); + let root = fs.metadata("/").unwrap(); + assert_eq!(root.len, 0); + assert_eq!(root.file_type, VfsFileType::Directory); + + // The empty path is treated as root + let root = fs.metadata("").unwrap(); + assert_eq!(root.len, 0); + assert_eq!(root.file_type, VfsFileType::Directory); + + let a = fs.metadata("/a").unwrap(); + assert_eq!(a.len, 0); + assert_eq!(a.file_type, VfsFileType::Directory); + } + + #[test] + fn metadata_not_found() { + let fs = get_test_fs(); + assert!(match fs.metadata("/abc.txt") { + Err(err) => match err.kind() { + VfsErrorKind::FileNotFound => true, + _ => false, + }, + _ => false, + }); + } + + #[test] + fn exists() { + let fs = get_test_fs(); + assert!(fs.exists("").unwrap()); + assert!(fs.exists("/a").unwrap()); + assert!(fs.exists("/a/d.txt").unwrap()); + assert!(fs.exists("/a.txt.dir").unwrap()); + assert!(fs.exists("/a.txt.dir/g.txt").unwrap()); + assert!(fs.exists("/c").unwrap()); + assert!(fs.exists("/c/e.txt").unwrap()); + assert!(fs.exists("/a.txt").unwrap()); + assert!(fs.exists("/b.txt").unwrap()); + + assert!(!fs.exists("/abc").unwrap()); + assert!(!fs.exists("/a.txt.").unwrap()); + } + + #[test] + fn remove_file_not_supported() { + let fs = get_test_fs(); + assert!( + match fs.remove_file("/abc.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::NotSupported => true, + _ => false, + } + ); + } + + #[test] + fn remove_dir_not_supported() { + let fs = get_test_fs(); + assert!( + match fs.remove_dir("/abc.txt").map(|_| ()).unwrap_err().kind() { + VfsErrorKind::NotSupported => true, + _ => false, + } + ); + } + + #[test] + fn integration() { + let root: VfsPath = get_test_fs().into(); + let a_file = root.join("a.txt").unwrap(); + assert!(a_file.exists().unwrap()); + let mut text = String::new(); + a_file + .open_file() + .unwrap() + .read_to_string(&mut text) + .unwrap(); + assert_eq!(text.as_str(), "a"); + assert_eq!(a_file.filename(), String::from("a.txt")); + + text.clear(); + root.join("a") + .unwrap() + .join("d.txt") + .unwrap() + .open_file() + .unwrap() + .read_to_string(&mut text) + .unwrap(); + assert_eq!(text, String::from("d")); + + assert!(root.join("a.txt.dir").unwrap().exists().unwrap()); + assert!(!root.join("g").unwrap().exists().unwrap()); + } +} diff --git a/awkernel_lib/src/file/vfs/impls/memory.rs b/awkernel_lib/src/file/vfs/impls/memory.rs new file mode 100644 index 000000000..76db99e54 --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/memory.rs @@ -0,0 +1,521 @@ +//! An ephemeral in-memory file system, intended mainly for unit tests + +use crate::error::VfsErrorKind; +use crate::{FileSystem, VfsFileType}; +use crate::{SeekAndRead, VfsMetadata}; +use crate::{SeekAndWrite, VfsResult}; +use core::cmp; +use std::collections::hash_map::Entry; +use std::collections::HashMap; +use std::fmt; +use std::fmt::{Debug, Formatter}; +use std::io::{Cursor, Read, Seek, SeekFrom, Write}; +use std::mem::swap; +use std::sync::{Arc, RwLock}; +use std::time::SystemTime; + +type MemoryFsHandle = Arc>; + +/// An ephemeral in-memory file system, intended mainly for unit tests +pub struct MemoryFS { + handle: MemoryFsHandle, +} + +impl Debug for MemoryFS { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str("In Memory File System") + } +} + +impl MemoryFS { + /// Create a new in-memory filesystem + pub fn new() -> Self { + MemoryFS { + handle: Arc::new(RwLock::new(MemoryFsImpl::new())), + } + } + + fn ensure_has_parent(&self, path: &str) -> VfsResult<()> { + let separator = path.rfind('/'); + if let Some(index) = separator { + if self.exists(&path[..index])? { + return Ok(()); + } + } + Err(VfsErrorKind::Other("Parent path does not exist".into()).into()) + } +} + +impl Default for MemoryFS { + fn default() -> Self { + Self::new() + } +} + +struct WritableFile { + content: Cursor>, + destination: String, + fs: MemoryFsHandle, +} + +impl Seek for WritableFile { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.content.seek(pos) + } +} + +impl Write for WritableFile { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.content.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.content.flush()?; + let mut content = self.content.get_ref().clone(); + swap(&mut content, self.content.get_mut()); + let mut handle = self.fs.write().unwrap(); + let previous_file = handle.files.get(&self.destination); + + let new_file = MemoryFile { + file_type: VfsFileType::File, + content: Arc::new(content), + created: previous_file + .map(|file| file.created) + .unwrap_or(SystemTime::now()), + modified: Some(SystemTime::now()), + accessed: previous_file.map(|file| file.accessed).unwrap_or(None), + }; + + handle.files.insert(self.destination.clone(), new_file); + Ok(()) + } +} + +impl Drop for WritableFile { + fn drop(&mut self) { + self.flush() + .expect("Flush failed while dropping in-memory file"); + } +} + +struct ReadableFile { + #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable + content: Arc>, + position: u64, +} + +impl ReadableFile { + fn len(&self) -> u64 { + self.content.len() as u64 - self.position + } +} + +impl Read for ReadableFile { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let amt = cmp::min(buf.len(), self.len() as usize); + + if amt == 1 { + buf[0] = self.content[self.position as usize]; + } else { + buf[..amt].copy_from_slice( + &self.content.as_slice()[self.position as usize..self.position as usize + amt], + ); + } + self.position += amt as u64; + Ok(amt) + } +} + +impl Seek for ReadableFile { + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + match pos { + SeekFrom::Start(offset) => self.position = offset, + SeekFrom::Current(offset) => self.position = (self.position as i64 + offset) as u64, + SeekFrom::End(offset) => self.position = (self.content.len() as i64 + offset) as u64, + } + Ok(self.position) + } +} + +impl FileSystem for MemoryFS { + fn read_dir(&self, path: &str) -> VfsResult + Send>> { + let prefix = format!("{}/", path); + let handle = self.handle.read().unwrap(); + let mut found_directory = false; + #[allow(clippy::needless_collect)] // need collect to satisfy lifetime requirements + let entries: Vec<_> = handle + .files + .iter() + .filter_map(|(candidate_path, _)| { + if candidate_path == path { + found_directory = true; + } + if candidate_path.starts_with(&prefix) { + let rest = &candidate_path[prefix.len()..]; + if !rest.contains('/') { + return Some(rest.to_string()); + } + } + None + }) + .collect(); + if !found_directory { + return Err(VfsErrorKind::FileNotFound.into()); + } + Ok(Box::new(entries.into_iter())) + } + + fn create_dir(&self, path: &str) -> VfsResult<()> { + self.ensure_has_parent(path)?; + let map = &mut self.handle.write().unwrap().files; + let entry = map.entry(path.to_string()); + match entry { + Entry::Occupied(file) => { + return match file.get().file_type { + VfsFileType::File => Err(VfsErrorKind::FileExists.into()), + VfsFileType::Directory => Err(VfsErrorKind::DirectoryExists.into()), + } + } + Entry::Vacant(_) => { + map.insert( + path.to_string(), + MemoryFile { + file_type: VfsFileType::Directory, + content: Default::default(), + created: SystemTime::now(), + modified: Some(SystemTime::now()), + accessed: Some(SystemTime::now()), + }, + ); + } + } + Ok(()) + } + + fn open_file(&self, path: &str) -> VfsResult + Send>> { + self.set_access_time(path, SystemTime::now())?; + + let handle = self.handle.read().unwrap(); + let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + ensure_file(file)?; + Ok(Box::new(ReadableFile { + content: file.content.clone(), + position: 0, + })) + } + + fn create_file(&self, path: &str) -> VfsResult + Send>> { + self.ensure_has_parent(path)?; + let content = Arc::new(Vec::::new()); + self.handle.write().unwrap().files.insert( + path.to_string(), + MemoryFile { + file_type: VfsFileType::File, + content, + created: SystemTime::now(), + modified: Some(SystemTime::now()), + accessed: Some(SystemTime::now()), + }, + ); + let writer = WritableFile { + content: Cursor::new(vec![]), + destination: path.to_string(), + fs: self.handle.clone(), + }; + Ok(Box::new(writer)) + } + + fn append_file(&self, path: &str) -> VfsResult + Send>> { + let handle = self.handle.write().unwrap(); + let file = handle.files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + let mut content = Cursor::new(file.content.as_ref().clone()); + content.seek(SeekFrom::End(0))?; + let writer = WritableFile { + content, + destination: path.to_string(), + fs: self.handle.clone(), + }; + Ok(Box::new(writer)) + } + + fn metadata(&self, path: &str) -> VfsResult { + let guard = self.handle.read().unwrap(); + let files = &guard.files; + let file = files.get(path).ok_or(VfsErrorKind::FileNotFound)?; + Ok(VfsMetadata { + file_type: file.file_type, + len: file.content.len() as u64, + modified: file.modified, + created: Some(file.created), + accessed: file.accessed, + }) + } + + fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + let mut guard = self.handle.write().unwrap(); + let files = &mut guard.files; + let file = files.get_mut(path).ok_or(VfsErrorKind::FileNotFound)?; + + file.created = time; + + Ok(()) + } + + fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + let mut guard = self.handle.write().unwrap(); + let files = &mut guard.files; + let file = files.get_mut(path).ok_or(VfsErrorKind::FileNotFound)?; + + file.modified = Some(time); + + Ok(()) + } + + fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + let mut guard = self.handle.write().unwrap(); + let files = &mut guard.files; + let file = files.get_mut(path).ok_or(VfsErrorKind::FileNotFound)?; + + file.accessed = Some(time); + + Ok(()) + } + + fn exists(&self, path: &str) -> VfsResult { + Ok(self.handle.read().unwrap().files.contains_key(path)) + } + + fn remove_file(&self, path: &str) -> VfsResult<()> { + let mut handle = self.handle.write().unwrap(); + handle + .files + .remove(path) + .ok_or(VfsErrorKind::FileNotFound)?; + Ok(()) + } + + fn remove_dir(&self, path: &str) -> VfsResult<()> { + if self.read_dir(path)?.next().is_some() { + return Err(VfsErrorKind::Other("Directory to remove is not empty".into()).into()); + } + let mut handle = self.handle.write().unwrap(); + handle + .files + .remove(path) + .ok_or(VfsErrorKind::FileNotFound)?; + Ok(()) + } +} + +struct MemoryFsImpl { + files: HashMap, +} + +impl MemoryFsImpl { + pub fn new() -> Self { + let mut files = HashMap::new(); + // Add root directory + files.insert( + "".to_string(), + MemoryFile { + file_type: VfsFileType::Directory, + content: Arc::new(vec![]), + created: SystemTime::now(), + modified: None, + accessed: None, + }, + ); + Self { files } + } +} + +struct MemoryFile { + file_type: VfsFileType, + #[allow(clippy::rc_buffer)] // to allow accessing the same object as writable + content: Arc>, + + created: SystemTime, + modified: Option, + accessed: Option, +} + +fn ensure_file(file: &MemoryFile) -> VfsResult<()> { + if file.file_type != VfsFileType::File { + return Err(VfsErrorKind::Other("Not a file".into()).into()); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::VfsPath; + test_vfs!(MemoryFS::new()); + + #[test] + fn write_and_read_file() -> VfsResult<()> { + let root = VfsPath::new(MemoryFS::new()); + let path = root.join("foobar.txt").unwrap(); + let _send = &path as &dyn Send; + { + let mut file = path.create_file().unwrap(); + write!(file, "Hello world").unwrap(); + write!(file, "!").unwrap(); + } + { + let mut file = path.open_file().unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Hello world!"); + } + assert!(path.exists()?); + assert!(!root.join("foo").unwrap().exists()?); + let metadata = path.metadata().unwrap(); + assert_eq!(metadata.len, 12); + assert_eq!(metadata.file_type, VfsFileType::File); + Ok(()) + } + + #[test] + fn write_and_seek_and_read_file() -> VfsResult<()> { + let root = VfsPath::new(MemoryFS::new()); + let path = root.join("foobar.txt").unwrap(); + let _send = &path as &dyn Send; + { + let mut file = path.create_file().unwrap(); + write!(file, "Hello world").unwrap(); + write!(file, "!").unwrap(); + write!(file, " Before seek!!").unwrap(); + file.seek(SeekFrom::Current(-2)).unwrap(); + write!(file, " After the Seek!").unwrap(); + } + { + let mut file = path.open_file().unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Hello world! Before seek After the Seek!"); + } + assert!(path.exists()?); + assert!(!root.join("foo").unwrap().exists()?); + let metadata = path.metadata().unwrap(); + assert_eq!(metadata.len, 40); + assert_eq!(metadata.file_type, VfsFileType::File); + Ok(()) + } + + #[test] + fn append_file() { + let root = VfsPath::new(MemoryFS::new()); + let _string = String::new(); + let path = root.join("test_append.txt").unwrap(); + path.create_file().unwrap().write_all(b"Testing 1").unwrap(); + path.append_file().unwrap().write_all(b"Testing 2").unwrap(); + { + let mut file = path.open_file().unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Testing 1Testing 2"); + } + } + + #[test] + fn append_file_with_seek() { + let root = VfsPath::new(MemoryFS::new()); + let _string = String::new(); + let path = root.join("test_append.txt").unwrap(); + path.create_file().unwrap().write_all(b"Testing 1").unwrap(); + path.append_file().unwrap().write_all(b"Testing 2").unwrap(); + { + let mut file = path.append_file().unwrap(); + file.seek(SeekFrom::End(-1)).unwrap(); + file.write_all(b"Testing 3").unwrap(); + } + { + let mut file = path.open_file().unwrap(); + let mut string: String = String::new(); + file.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Testing 1Testing Testing 3"); + } + } + + #[test] + fn create_dir() { + let root = VfsPath::new(MemoryFS::new()); + let _string = String::new(); + let path = root.join("foo").unwrap(); + path.create_dir().unwrap(); + let metadata = path.metadata().unwrap(); + assert_eq!(metadata.file_type, VfsFileType::Directory); + } + + #[test] + fn remove_dir_error_message() { + let root = VfsPath::new(MemoryFS::new()); + let path = root.join("foo").unwrap(); + let result = path.remove_dir(); + assert_eq!( + format!("{}", result.unwrap_err()), + "Could not remove directory for '/foo': The file or directory could not be found" + ); + } + + #[test] + fn read_dir_error_message() { + let root = VfsPath::new(MemoryFS::new()); + let path = root.join("foo").unwrap(); + let result = path.read_dir(); + match result { + Ok(_) => panic!("Error expected"), + Err(err) => { + assert_eq!( + format!("{}", err), + "Could not read directory for '/foo': The file or directory could not be found" + ); + } + } + } + + #[test] + fn copy_file_across_filesystems() -> VfsResult<()> { + let root_a = VfsPath::new(MemoryFS::new()); + let root_b = VfsPath::new(MemoryFS::new()); + let src = root_a.join("a.txt")?; + let dest = root_b.join("b.txt")?; + src.create_file()?.write_all(b"Hello World")?; + src.copy_file(&dest)?; + assert_eq!(&dest.read_to_string()?, "Hello World"); + Ok(()) + } + + // cf. https://github.com/manuel-woelker/rust-vfs/issues/70 + #[test] + fn flush_then_read_with_new_handle() { + let root = VfsPath::new(MemoryFS::new()); + let path = root.join("test.txt").unwrap(); + let mut write_handle = path.create_file().unwrap(); + write_handle.write_all(b"Testing 1").unwrap(); + + // Ensure flushed data can be read + write_handle.flush().unwrap(); + let mut read_handle = path.open_file().unwrap(); + let mut string: String = String::new(); + read_handle.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Testing 1"); + + // Ensure second flush data can be read + write_handle.write_all(b"Testing 2").unwrap(); + write_handle.flush().unwrap(); + let mut read_handle = path.open_file().unwrap(); + let mut string: String = String::new(); + read_handle.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Testing 1Testing 2"); + + // Ensure everything can be read on drop + write_handle.write_all(b"Testing 3").unwrap(); + drop(write_handle); + let mut read_handle = path.open_file().unwrap(); + let mut string: String = String::new(); + read_handle.read_to_string(&mut string).unwrap(); + assert_eq!(string, "Testing 1Testing 2Testing 3"); + } +} diff --git a/awkernel_lib/src/file/vfs/impls/mod.rs b/awkernel_lib/src/file/vfs/impls/mod.rs new file mode 100644 index 000000000..8e9b4ecd4 --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/mod.rs @@ -0,0 +1,8 @@ +//! Virtual filesystem implementations + +pub mod altroot; +#[cfg(feature = "embedded-fs")] +pub mod embedded; +pub mod memory; +pub mod overlay; +pub mod physical; diff --git a/awkernel_lib/src/file/vfs/impls/overlay.rs b/awkernel_lib/src/file/vfs/impls/overlay.rs new file mode 100644 index 000000000..de9c45906 --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/overlay.rs @@ -0,0 +1,393 @@ +//! An overlay file system combining two filesystems, an upper layer with read/write access and a lower layer with only read access + +use crate::error::VfsErrorKind; +use crate::{FileSystem, SeekAndRead, SeekAndWrite, VfsMetadata, VfsPath, VfsResult}; +use std::collections::HashSet; + +use std::time::SystemTime; + +/// An overlay file system combining several filesystems into one, an upper layer with read/write access and lower layers with only read access +/// +/// Files in upper layers shadow those in lower layers. Directories are the merged view of all layers. +/// +/// NOTE: To allow removing files and directories (e.g. via remove_file()) from the lower layer filesystems, this mechanism creates a `.whiteout` folder in the root of the upper level filesystem to mark removed files +/// +#[derive(Debug, Clone)] +pub struct OverlayFS { + layers: Vec, +} + +impl OverlayFS { + /// Create a new overlay FileSystem from the given layers, only the first layer is written to + pub fn new(layers: &[VfsPath]) -> Self { + if layers.is_empty() { + panic!("OverlayFS needs at least one layer") + } + OverlayFS { + layers: layers.to_vec(), + } + } + + fn write_layer(&self) -> &VfsPath { + &self.layers[0] + } + + fn read_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.layers[0].clone()); + } + if self.whiteout_path(path)?.exists()? { + return Err(VfsErrorKind::FileNotFound.into()); + } + for layer in &self.layers { + let layer_path = layer.join(&path[1..])?; + if layer_path.exists()? { + return Ok(layer_path); + } + } + let read_path = self.write_layer().join(&path[1..])?; + if !read_path.exists()? { + return Err(VfsErrorKind::FileNotFound.into()); + } + Ok(read_path) + } + + fn write_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(self.layers[0].clone()); + } + self.write_layer().join(&path[1..]) + } + + fn whiteout_path(&self, path: &str) -> VfsResult { + if path.is_empty() { + return self.write_layer().join(".whiteout/_wo"); + } + self.write_layer() + .join(format!(".whiteout/{}_wo", &path[1..])) + } + + fn ensure_has_parent(&self, path: &str) -> VfsResult<()> { + let separator = path.rfind('/'); + if let Some(index) = separator { + let parent_path = &path[..index]; + if self.exists(parent_path)? { + self.write_path(parent_path)?.create_dir_all()?; + return Ok(()); + } + } + Err(VfsErrorKind::Other("Parent path does not exist".into()).into()) + } +} + +impl FileSystem for OverlayFS { + fn read_dir(&self, path: &str) -> VfsResult + Send>> { + let actual_path = if !path.is_empty() { &path[1..] } else { path }; + if !self.read_path(path)?.exists()? { + return Err(VfsErrorKind::FileNotFound.into()); + } + let mut entries = HashSet::::new(); + for layer in &self.layers { + let layer_path = layer.join(actual_path)?; + if layer_path.exists()? { + for path in layer_path.read_dir()? { + entries.insert(path.filename()); + } + } + } + // remove whiteout entries that have been removed + let whiteout_path = self.write_layer().join(format!(".whiteout{}", path))?; + if whiteout_path.exists()? { + for path in whiteout_path.read_dir()? { + let filename = path.filename(); + if filename.ends_with("_wo") { + entries.remove(&filename[..filename.len() - 3]); + } + } + } + Ok(Box::new(entries.into_iter())) + } + + fn create_dir(&self, path: &str) -> VfsResult<()> { + self.ensure_has_parent(path)?; + self.write_path(path)?.create_dir()?; + let whiteout_path = self.whiteout_path(path)?; + if whiteout_path.exists()? { + whiteout_path.remove_file()?; + } + Ok(()) + } + + fn open_file(&self, path: &str) -> VfsResult + Send>> { + self.read_path(path)?.open_file() + } + + fn create_file(&self, path: &str) -> VfsResult + Send>> { + self.ensure_has_parent(path)?; + let result = self.write_path(path)?.create_file()?; + let whiteout_path = self.whiteout_path(path)?; + if whiteout_path.exists()? { + whiteout_path.remove_file()?; + } + Ok(result) + } + + fn append_file(&self, path: &str) -> VfsResult + Send>> { + let write_path = self.write_path(path)?; + if !write_path.exists()? { + self.ensure_has_parent(path)?; + self.read_path(path)?.copy_file(&write_path)?; + } + write_path.append_file() + } + + fn metadata(&self, path: &str) -> VfsResult { + self.read_path(path)?.metadata() + } + + fn set_creation_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_creation_time(time) + } + + fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_modification_time(time) + } + + fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + self.write_path(path)?.set_access_time(time) + } + + fn exists(&self, path: &str) -> VfsResult { + if self + .whiteout_path(path) + .map_err(|err| err.with_context(|| "whiteout_path"))? + .exists()? + { + return Ok(false); + } + self.read_path(path) + .map(|path| path.exists()) + .unwrap_or(Ok(false)) + } + + fn remove_file(&self, path: &str) -> VfsResult<()> { + // Ensure path exists + self.read_path(path)?; + let write_path = self.write_path(path)?; + if write_path.exists()? { + write_path.remove_file()?; + } + let whiteout_path = self.whiteout_path(path)?; + whiteout_path.parent().create_dir_all()?; + whiteout_path.create_file()?; + Ok(()) + } + + fn remove_dir(&self, path: &str) -> VfsResult<()> { + // Ensure path exists + self.read_path(path)?; + let write_path = self.write_path(path)?; + if write_path.exists()? { + write_path.remove_dir()?; + } + let whiteout_path = self.whiteout_path(path)?; + whiteout_path.parent().create_dir_all()?; + whiteout_path.create_file()?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MemoryFS; + test_vfs!({ + let upper_root: VfsPath = MemoryFS::new().into(); + let lower_root: VfsPath = MemoryFS::new().into(); + OverlayFS::new(&[upper_root, lower_root]) + }); + + fn create_roots() -> (VfsPath, VfsPath, VfsPath) { + let lower_root: VfsPath = MemoryFS::new().into(); + let upper_root: VfsPath = MemoryFS::new().into(); + let overlay_root: VfsPath = + OverlayFS::new(&[upper_root.clone(), lower_root.clone()]).into(); + (lower_root, upper_root, overlay_root) + } + + #[test] + fn read() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + let lower_path = lower_root.join("foo.txt")?; + let upper_path = upper_root.join("foo.txt")?; + let overlay_path = overlay_root.join("foo.txt")?; + lower_path.create_file()?.write_all(b"Hello Lower")?; + assert_eq!(&overlay_path.read_to_string()?, "Hello Lower"); + upper_path.create_file()?.write_all(b"Hello Upper")?; + assert_eq!(&overlay_path.read_to_string()?, "Hello Upper"); + lower_path.remove_file()?; + assert_eq!(&overlay_path.read_to_string()?, "Hello Upper"); + upper_path.remove_file()?; + assert!(!overlay_path.exists()?, "File should not exist anymore"); + Ok(()) + } + + #[test] + fn read_dir() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + upper_root.join("foo/upper")?.create_dir_all()?; + upper_root.join("foo/common")?.create_dir_all()?; + lower_root.join("foo/common")?.create_dir_all()?; + lower_root.join("foo/lower")?.create_dir_all()?; + let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect(); + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/foo/common", "/foo/lower", "/foo/upper"]); + Ok(()) + } + + #[test] + fn read_dir_root() -> VfsResult<()> { + let (lower_root, upper_root, overlay_root) = create_roots(); + upper_root.join("upper")?.create_dir_all()?; + upper_root.join("common")?.create_dir_all()?; + lower_root.join("common")?.create_dir_all()?; + lower_root.join("lower")?.create_dir_all()?; + let entries: Vec<_> = overlay_root.read_dir()?.collect(); + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/common", "/lower", "/upper"]); + Ok(()) + } + + #[test] + fn create_dir() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + assert!(overlay_root.join("foo")?.exists()?, "dir should exist"); + overlay_root.join("foo/bar")?.create_dir()?; + assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist"); + Ok(()) + } + + #[test] + fn create_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + assert!(overlay_root.join("foo")?.exists()?, "dir should exist"); + overlay_root.join("foo/bar")?.create_file()?; + assert!(overlay_root.join("foo/bar")?.exists()?, "file should exist"); + Ok(()) + } + + #[test] + fn append_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + lower_root + .join("foo/bar.txt")? + .create_file()? + .write_all(b"Hello Lower\n")?; + overlay_root + .join("foo/bar.txt")? + .append_file()? + .write_all(b"Hello Overlay\n")?; + assert_eq!( + &overlay_root.join("foo/bar.txt")?.read_to_string()?, + "Hello Lower\nHello Overlay\n" + ); + Ok(()) + } + + #[test] + fn remove_file() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + lower_root + .join("foo/bar.txt")? + .create_file()? + .write_all(b"Hello Lower\n")?; + assert!( + overlay_root.join("foo/bar.txt")?.exists()?, + "file should exist" + ); + + overlay_root.join("foo/bar.txt")?.remove_file()?; + assert!( + !overlay_root.join("foo/bar.txt")?.exists()?, + "file should not exist anymore" + ); + + overlay_root + .join("foo/bar.txt")? + .create_file()? + .write_all(b"Hello Overlay\n")?; + assert!( + overlay_root.join("foo/bar.txt")?.exists()?, + "file should exist" + ); + assert_eq!( + &overlay_root.join("foo/bar.txt")?.read_to_string()?, + "Hello Overlay\n" + ); + Ok(()) + } + + #[test] + fn remove_dir() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + lower_root.join("foo/bar")?.create_dir_all()?; + assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist"); + + overlay_root.join("foo/bar")?.remove_dir()?; + assert!( + !overlay_root.join("foo/bar")?.exists()?, + "dir should not exist anymore" + ); + + overlay_root.join("foo/bar")?.create_dir()?; + assert!(overlay_root.join("foo/bar")?.exists()?, "dir should exist"); + Ok(()) + } + + #[test] + fn read_dir_removed_entries() -> VfsResult<()> { + let (lower_root, _upper_root, overlay_root) = create_roots(); + lower_root.join("foo")?.create_dir_all()?; + lower_root.join("foo/bar")?.create_dir_all()?; + lower_root.join("foo/bar.txt")?.create_dir_all()?; + + let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect(); + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec!["/foo/bar", "/foo/bar.txt"]); + overlay_root.join("foo/bar")?.remove_dir()?; + overlay_root.join("foo/bar.txt")?.remove_file()?; + + let entries: Vec<_> = overlay_root.join("foo")?.read_dir()?.collect(); + let mut paths: Vec<_> = entries.iter().map(|path| path.as_str()).collect(); + paths.sort(); + assert_eq!(paths, vec![] as Vec<&str>); + + Ok(()) + } +} + +#[cfg(test)] +mod tests_physical { + use super::*; + use crate::PhysicalFS; + test_vfs!({ + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + let lower_path = dir.join("lower"); + std::fs::create_dir_all(&lower_path).unwrap(); + let upper_path = dir.join("upper"); + std::fs::create_dir_all(&upper_path).unwrap(); + + let upper_root: VfsPath = PhysicalFS::new(upper_path).into(); + let lower_root: VfsPath = PhysicalFS::new(lower_path).into(); + OverlayFS::new(&[upper_root, lower_root]) + }); +} diff --git a/awkernel_lib/src/file/vfs/impls/physical.rs b/awkernel_lib/src/file/vfs/impls/physical.rs new file mode 100644 index 000000000..9f0187209 --- /dev/null +++ b/awkernel_lib/src/file/vfs/impls/physical.rs @@ -0,0 +1,243 @@ +//! A "physical" file system implementation using the underlying OS file system + +use crate::error::VfsErrorKind; +use crate::{FileSystem, SeekAndWrite, VfsMetadata}; +use crate::{SeekAndRead, VfsFileType}; +use crate::{VfsError, VfsResult}; +use filetime::FileTime; +use std::fs::{File, OpenOptions}; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; +use std::time::SystemTime; + +/// A physical filesystem implementation using the underlying OS file system +#[derive(Debug)] +pub struct PhysicalFS { + root: PathBuf, +} + +impl PhysicalFS { + /// Create a new physical filesystem rooted in `root` + pub fn new>(root: T) -> Self { + PhysicalFS { + root: root.as_ref().to_path_buf(), + } + } + + fn get_path(&self, mut path: &str) -> PathBuf { + if path.starts_with('/') { + path = &path[1..]; + } + self.root.join(path) + } +} + +impl FileSystem for PhysicalFS { + fn read_dir(&self, path: &str) -> VfsResult + Send>> { + let entries = Box::new( + self.get_path(path) + .read_dir()? + .map(|entry| entry.unwrap().file_name().into_string().unwrap()), + ); + Ok(entries) + } + + fn create_dir(&self, path: &str) -> VfsResult<()> { + let fs_path = self.get_path(path); + std::fs::create_dir(&fs_path).map_err(|err| match err.kind() { + ErrorKind::AlreadyExists => { + let metadata = std::fs::metadata(&fs_path).unwrap(); + if metadata.is_dir() { + return VfsError::from(VfsErrorKind::DirectoryExists); + } + VfsError::from(VfsErrorKind::FileExists) + } + _ => err.into(), + })?; + Ok(()) + } + + fn open_file(&self, path: &str) -> VfsResult + Send>> { + Ok(Box::new(File::open(self.get_path(path))?)) + } + + fn create_file(&self, path: &str) -> VfsResult + Send>> { + Ok(Box::new(File::create(self.get_path(path))?)) + } + + fn append_file(&self, path: &str) -> VfsResult + Send>> { + Ok(Box::new( + OpenOptions::new().append(true).open(self.get_path(path))?, + )) + } + + fn metadata(&self, path: &str) -> VfsResult { + let metadata = self.get_path(path).metadata()?; + Ok(if metadata.is_dir() { + VfsMetadata { + file_type: VfsFileType::Directory, + len: 0, + modified: metadata.modified().ok(), + created: metadata.created().ok(), + accessed: metadata.accessed().ok(), + } + } else { + VfsMetadata { + file_type: VfsFileType::File, + len: metadata.len(), + modified: metadata.modified().ok(), + created: metadata.created().ok(), + accessed: metadata.accessed().ok(), + } + }) + } + + fn set_modification_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + filetime::set_file_mtime(self.get_path(path), FileTime::from(time))?; + Ok(()) + } + + fn set_access_time(&self, path: &str, time: SystemTime) -> VfsResult<()> { + filetime::set_file_atime(self.get_path(path), FileTime::from(time))?; + Ok(()) + } + + fn exists(&self, path: &str) -> VfsResult { + Ok(self.get_path(path).exists()) + } + + fn remove_file(&self, path: &str) -> VfsResult<()> { + std::fs::remove_file(self.get_path(path))?; + Ok(()) + } + + fn remove_dir(&self, path: &str) -> VfsResult<()> { + std::fs::remove_dir(self.get_path(path))?; + Ok(()) + } + + fn copy_file(&self, src: &str, dest: &str) -> VfsResult<()> { + std::fs::copy(self.get_path(src), self.get_path(dest))?; + Ok(()) + } + + fn move_file(&self, src: &str, dest: &str) -> VfsResult<()> { + std::fs::rename(self.get_path(src), self.get_path(dest))?; + + Ok(()) + } + + fn move_dir(&self, src: &str, dest: &str) -> VfsResult<()> { + let result = std::fs::rename(self.get_path(src), self.get_path(dest)); + if result.is_err() { + // Error possibly due to different filesystems, return not supported and let the fallback handle it + return Err(VfsErrorKind::NotSupported.into()); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::path::Path; + + use super::*; + + use crate::VfsPath; + test_vfs!({ + let temp_dir = std::env::temp_dir(); + let dir = temp_dir.join(uuid::Uuid::new_v4().to_string()); + std::fs::create_dir_all(&dir).unwrap(); + PhysicalFS::new(dir) + }); + test_vfs_readonly!({ PhysicalFS::new("test/test_directory") }); + + fn create_root() -> VfsPath { + PhysicalFS::new(std::env::current_dir().unwrap()).into() + } + + #[test] + fn open_file() { + let expected = std::fs::read_to_string("Cargo.toml").unwrap(); + let root = create_root(); + let mut string = String::new(); + root.join("Cargo.toml") + .unwrap() + .open_file() + .unwrap() + .read_to_string(&mut string) + .unwrap(); + assert_eq!(string, expected); + } + + #[test] + fn create_file() { + let root = create_root(); + let _string = String::new(); + let _ = std::fs::remove_file("target/test.txt"); + root.join("target/test.txt") + .unwrap() + .create_file() + .unwrap() + .write_all(b"Testing only") + .unwrap(); + let read = std::fs::read_to_string("target/test.txt").unwrap(); + assert_eq!(read, "Testing only"); + } + + #[test] + fn append_file() { + let root = create_root(); + let _string = String::new(); + let _ = std::fs::remove_file("target/test_append.txt"); + let path = root.join("target/test_append.txt").unwrap(); + path.create_file().unwrap().write_all(b"Testing 1").unwrap(); + path.append_file().unwrap().write_all(b"Testing 2").unwrap(); + let read = std::fs::read_to_string("target/test_append.txt").unwrap(); + assert_eq!(read, "Testing 1Testing 2"); + } + + #[test] + fn read_dir() { + let _expected = std::fs::read_to_string("Cargo.toml").unwrap(); + let root = create_root(); + let entries: Vec<_> = root.read_dir().unwrap().collect(); + let map: Vec<_> = entries + .iter() + .map(|path: &VfsPath| path.as_str()) + .filter(|x| x.ends_with(".toml")) + .collect(); + assert_eq!(&["/Cargo.toml"], &map[..]); + } + + #[test] + fn create_dir() { + let _ = std::fs::remove_dir("target/fs_test"); + let root = create_root(); + root.join("target/fs_test").unwrap().create_dir().unwrap(); + let path = Path::new("target/fs_test"); + assert!(path.exists(), "Path was not created"); + assert!(path.is_dir(), "Path is not a directory"); + std::fs::remove_dir("target/fs_test").unwrap(); + } + + #[test] + fn file_metadata() { + let expected = std::fs::read_to_string("Cargo.toml").unwrap(); + let root = create_root(); + let metadata = root.join("Cargo.toml").unwrap().metadata().unwrap(); + assert_eq!(metadata.len, expected.len() as u64); + assert_eq!(metadata.file_type, VfsFileType::File); + } + + #[test] + fn dir_metadata() { + let root = create_root(); + let metadata = root.metadata().unwrap(); + assert_eq!(metadata.len, 0); + assert_eq!(metadata.file_type, VfsFileType::Directory); + let metadata = root.join("src").unwrap().metadata().unwrap(); + assert_eq!(metadata.len, 0); + assert_eq!(metadata.file_type, VfsFileType::Directory); + } +} diff --git a/awkernel_lib/src/file/vfs/path.rs b/awkernel_lib/src/file/vfs/path.rs new file mode 100644 index 000000000..c1a5a170d --- /dev/null +++ b/awkernel_lib/src/file/vfs/path.rs @@ -0,0 +1,1137 @@ +//! Virtual filesystem path +//! +//! The virtual file system abstraction generalizes over file systems and allow using +//! different VirtualFileSystem implementations (i.e. an in memory implementation for unit tests) + +use super::super::error::IoError; +use super::super::io::{Read, Seek, Write}; +use crate::time::Time; +use alloc::{ + boxed::Box, + format, + string::{String, ToString}, + sync::Arc, + vec, + vec::Vec, +}; + +use super::error::VfsErrorKind; +use super::{error::VfsError, error::VfsResult, filesystem::FileSystem}; + +/// Trait combining Seek and Read, return value for opening files +pub trait SeekAndRead: Seek + Read {} + +/// Trait combining Seek and Write, return value for writing files +pub trait SeekAndWrite: Seek + Write {} + +impl SeekAndRead for T where T: Seek + Read {} + +impl SeekAndWrite for T where T: Seek + Write {} + +/// A trait for common non-async behaviour of both sync and async paths +pub(crate) trait PathLike: Clone { + type Error: IoError; + fn get_path(&self) -> String; + fn filename_internal(&self) -> String { + let path = self.get_path(); + let index = path.rfind('/').map(|x| x + 1).unwrap_or(0); + path[index..].to_string() + } + + fn extension_internal(&self) -> Option { + let filename = self.filename_internal(); + let mut parts = filename.rsplitn(2, '.'); + let after = parts.next(); + let before = parts.next(); + match before { + None | Some("") => None, + _ => after.map(|x| x.to_string()), + } + } + + fn parent_internal(&self, path: &str) -> String { + let index = path.rfind('/'); + index.map(|idx| path[..idx].to_string()).unwrap_or_default() + } + + fn join_internal(&self, in_path: &str, path: &str) -> VfsResult { + if path.is_empty() { + return Ok(in_path.to_string()); + } + let mut new_components: Vec<&str> = Vec::with_capacity( + in_path.chars().filter(|c| *c == '/').count() + + path.chars().filter(|c| *c == '/').count() + + 1, + ); + let mut base_path = if path.starts_with('/') { + "".to_string() + } else { + in_path.to_string() + }; + // Prevent paths from ending in slashes unless this is just the root directory. + if path.len() > 1 && path.ends_with('/') { + return Err(VfsError::from(VfsErrorKind::InvalidPath).with_path(path)); + } + for component in path.split('/') { + if component == "." || component.is_empty() { + continue; + } + if component == ".." { + if !new_components.is_empty() { + new_components.truncate(new_components.len() - 1); + } else { + base_path = self.parent_internal(&base_path); + } + } else { + new_components.push(component); + } + } + let mut path = base_path; + path.reserve( + new_components.len() + + new_components + .iter() + .fold(0, |accum, part| accum + part.len()), + ); + for component in new_components { + path.push('/'); + path.push_str(component); + } + Ok(path) + } +} + +/// Type of file +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum VfsFileType { + /// A plain file + File, + /// A Directory + Directory, +} + +/// File metadata information +#[derive(Debug)] +pub struct VfsMetadata { + /// The type of file + pub file_type: VfsFileType, + /// Length of the file in bytes, 0 for directories + pub len: u64, + /// Creation time of the file, if supported by the vfs implementation + pub created: Option