From 97d06f1f67f325a950485d9134b4f79143562291 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Sat, 11 Nov 2023 13:10:44 +0000 Subject: [PATCH] Add virtual runtime (FS only) --- mfio-netfs/src/net/client.rs | 14 +- mfio-netfs/src/net/server.rs | 33 +- mfio-rt/Cargo.toml | 19 +- mfio-rt/src/__doctest.rs | 35 +- mfio-rt/src/lib.rs | 198 ++- mfio-rt/src/native/impls/iocp/tcp_stream.rs | 3 +- mfio-rt/src/native/mod.rs | 71 +- mfio-rt/src/test_suite.rs | 854 +++++++------ mfio-rt/src/util.rs | 75 +- mfio-rt/src/virt/mod.rs | 1215 +++++++++++++++++++ mfio/src/error.rs | 4 + mfio/src/io/packet/mod.rs | 1 + mfio/src/lib.rs | 1 + 13 files changed, 2050 insertions(+), 473 deletions(-) create mode 100644 mfio-rt/src/virt/mod.rs diff --git a/mfio-netfs/src/net/client.rs b/mfio-netfs/src/net/client.rs index 1faf36e..6db8734 100644 --- a/mfio-netfs/src/net/client.rs +++ b/mfio-netfs/src/net/client.rs @@ -664,7 +664,11 @@ impl DirHandle for NetworkFsDir { /// /// This function accepts an absolute or relative path to a file for reading. If the path is /// relative, it is opened relative to this `DirHandle`. - fn open_file>(&self, path: P, options: OpenOptions) -> Self::OpenFileFuture<'_> { + fn open_file<'a, P: AsRef + ?Sized>( + &'a self, + path: &'a P, + options: OpenOptions, + ) -> Self::OpenFileFuture<'a> { OpenFileOp::make_future( self, FsRequest::OpenFile { @@ -678,7 +682,7 @@ impl DirHandle for NetworkFsDir { /// /// This function accepts an absolute or relative path to a directory for reading. If the path /// is relative, it is opened relative to this `DirHandle`. - fn open_dir>(&self, path: P) -> Self::OpenDirFuture<'_> { + fn open_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpenDirFuture<'a> { OpenDirOp::make_future( self, FsRequest::OpenDir { @@ -687,7 +691,7 @@ impl DirHandle for NetworkFsDir { ) } - fn metadata>(&self, path: P) -> Self::MetadataFuture<'_> { + fn metadata<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::MetadataFuture<'a> { MetadataOp::make_future( self, FsRequest::Metadata { @@ -699,8 +703,8 @@ impl DirHandle for NetworkFsDir { /// Do an operation. /// /// This function performs an operation from the [`DirOp`](DirOp) enum. - fn do_op>(&self, operation: DirOp

) -> Self::OpFuture<'_> { - OpOp::make_future(self, FsRequest::DirOp(operation.as_path().into_string())) + fn do_op<'a, P: AsRef + ?Sized>(&'a self, operation: DirOp<&'a P>) -> Self::OpFuture<'a> { + OpOp::make_future(self, FsRequest::DirOp(operation.into_string())) } } diff --git a/mfio-netfs/src/net/server.rs b/mfio-netfs/src/net/server.rs index a7596bb..01271f9 100644 --- a/mfio-netfs/src/net/server.rs +++ b/mfio-netfs/src/net/server.rs @@ -468,7 +468,7 @@ async fn run_server(stream: NativeTcpStream, fs: &NativeRt) { } FsRequest::OpenDir { path } => { trace!("Open dir {path}"); - let dir_id = match dh.open_dir(path).await { + let dir_id = match dh.open_dir(&path).await { Ok(dir) => { let dir_id = dir_handles .borrow_mut() @@ -507,7 +507,7 @@ async fn run_server(stream: NativeTcpStream, fs: &NativeRt) { FsRequest::Metadata { path } => { trace!("Metadata {path}"); let metadata = dh - .metadata(path) + .metadata(&path) .await .map_err(Error::into_int_err); FsResponse::Metadata { metadata } @@ -515,7 +515,7 @@ async fn run_server(stream: NativeTcpStream, fs: &NativeRt) { FsRequest::DirOp(op) => { trace!("Do dir op"); FsResponse::DirOp( - dh.do_op(op) + dh.do_op(op.as_path()) .await .map_err(Error::into_int_err) .err(), @@ -568,12 +568,13 @@ async fn run_server(stream: NativeTcpStream, fs: &NativeRt) { l1.await; } -pub fn single_client_server(addr: SocketAddr) -> (std::thread::JoinHandle<()>, SocketAddr) { +fn single_client_server_with( + addr: SocketAddr, + fs: NativeRt, +) -> (std::thread::JoinHandle<()>, SocketAddr) { let (tx, rx) = flume::bounded(1); let ret = std::thread::spawn(move || { - let fs = NativeRt::default(); - fs.block_on(async { let mut listener = fs.bind(addr).await.unwrap(); let _ = tx.send_async(listener.local_addr().unwrap()).await; @@ -589,6 +590,10 @@ pub fn single_client_server(addr: SocketAddr) -> (std::thread::JoinHandle<()>, S (ret, addr) } +pub fn single_client_server(addr: SocketAddr) -> (std::thread::JoinHandle<()>, SocketAddr) { + single_client_server_with(addr, NativeRt::default()) +} + pub async fn server_bind(fs: &NativeRt, bind_addr: SocketAddr) { let listener = fs.bind(bind_addr).await.unwrap(); server(fs, listener).await @@ -663,13 +668,18 @@ mod tests { server.join().unwrap(); } - mfio_rt::test_suite!(tests, |closure| { + mfio_rt::test_suite!(tests, |test_name, closure| { let _ = ::env_logger::builder().is_test(true).try_init(); - use super::{single_client_server, NetworkFs, SocketAddr}; + use super::{single_client_server_with, NetworkFs, SocketAddr}; let addr: SocketAddr = "127.0.0.1:0".parse().unwrap(); - let (server, addr) = single_client_server(addr); + + let mut rt = mfio_rt::NativeRt::default(); + let dir = TempDir::new(test_name).unwrap(); + rt.set_cwd(dir.path().to_path_buf()); + let (server, addr) = single_client_server_with(addr, rt); let rt = mfio_rt::NativeRt::default(); + let mut rt = NetworkFs::with_fs(addr, rt.into(), true).unwrap(); let fs = staticify(&mut rt); @@ -681,7 +691,10 @@ mod tests { fs.block_on(func(fs)) } - run(fs, closure); + run(fs, move |rt| { + let run = TestRun::new(rt, dir); + closure(run) + }); core::mem::drop(rt); diff --git a/mfio-rt/Cargo.toml b/mfio-rt/Cargo.toml index 7669ef0..d5cd406 100644 --- a/mfio-rt/Cargo.toml +++ b/mfio-rt/Cargo.toml @@ -18,11 +18,14 @@ required-features = ["mfio/tokio", "mfio/async-io"] [dependencies] mfio = { version = "0.1", path = "../mfio", default-features = false } -futures = { version = "0.3", default-features = false } -once_cell = { version = "1", default-features = false, optional = true } +futures = { version = "0.3", default-features = false, features = ["async-await"] } +once_cell = { version = "1", default-features = false } log = "0.4" serde = { version = "1", features = ["derive", "alloc"], default-features = false } +typed-path = { version = "0.7", default-features = false } + +# native rt deps tracing = { version = "0.1", optional = true } flume = { version = "0.10", optional = true } parking_lot = { version = "0.12", optional = true } @@ -32,6 +35,7 @@ pathdiff = { version = "0.2", optional = true } async-semaphore = { version = "1", optional = true } slab = { version = "0.4", default-features = false } + [target.'cfg(windows)'.dependencies] windows = { version = "0.51", features = ["Win32_System_IO", "Win32_Foundation", "Win32_System_WindowsProgramming", "Win32_Storage_FileSystem", "Win32_Networking_WinSock"] } force-send-sync = "1" @@ -60,18 +64,17 @@ async-semaphore = "1" [target.'cfg(not(miri))'.dev-dependencies] tokio = { version = "1.24", features = ["rt", "rt-multi-thread", "fs", "io-util"] } -#[target.'cfg(unix)'.dev-dependencies] -#nuclei = "0.2" - [target.'cfg(target_os = "linux")'.dev-dependencies] rio = "0.9" # We need git version to compile on alpine glommio = { version = "0.8", git = "https://github.com/DataDog/glommio", rev = "517326bb2b63b6f6ddcf5deec7a283ee510f44df" } [features] -default = ["mio", "io-uring", "iocp", "native", "std"] -native = ["once_cell", "oneshot", "parking_lot", "flume", "tracing", "std"] -std = ["mfio/std", "once_cell/std", "once_cell/parking_lot"] +default = ["mio", "io-uring", "iocp", "native", "std", "virt"] +native = ["oneshot", "parking_lot", "flume", "tracing", "std"] +virt = [] +virt-sync = [] +std = ["mfio/std", "once_cell/std"] # technically iocp depends on native, but let's be in-line with other backends iocp = [] test_suite = ["tempdir", "pathdiff", "async-semaphore"] diff --git a/mfio-rt/src/__doctest.rs b/mfio-rt/src/__doctest.rs index fc75247..e4a54e5 100644 --- a/mfio-rt/src/__doctest.rs +++ b/mfio-rt/src/__doctest.rs @@ -1,8 +1,10 @@ +#[cfg(all(any(miri, test, feature = "virt"), not(feature = "native")))] +use crate::virt::VirtRt; #[cfg(feature = "native")] use crate::NativeRt; -#[cfg(feature = "native")] +#[cfg(any(miri, test, feature = "native", feature = "virt"))] use core::future::Future; -#[cfg(feature = "native")] +#[cfg(any(miri, test, feature = "native", feature = "virt"))] use mfio::backend::IoBackend; #[cfg(feature = "native")] @@ -16,3 +18,32 @@ pub fn run_each<'a, Func: Fn(&'a NativeRt) -> F, F: Future>(func: Func) { } } } + +#[cfg(all(any(miri, test, feature = "virt"), not(feature = "native")))] +pub fn run_each<'a, Func: Fn(&'a VirtRt) -> F, F: Future>(func: Func) { + use crate::{DirHandle, Fs, OpenOptions}; + use mfio::traits::*; + + const FILES: &[(&str, &str)] = &[ + ("Cargo.toml", include_str!("../Cargo.toml")), + ("src/lib.rs", include_str!("lib.rs")), + ]; + + let fs = &VirtRt::new(); + + fs.block_on(async { + let cd = fs.current_dir(); + cd.create_dir("src").await.unwrap(); + for (p, data) in FILES { + let fh = cd + .open_file(p, OpenOptions::new().create_new(true).write(true)) + .await + .unwrap(); + fh.write_all(0, data.as_bytes()).await.unwrap(); + } + }); + + // SAFETY: there isn't. The doctests shouldn't move the fs handle though. + let fs: &'a VirtRt = unsafe { &(*(fs as *const _)) }; + fs.block_on(func(fs)); +} diff --git a/mfio-rt/src/lib.rs b/mfio-rt/src/lib.rs index cad6e18..0129a07 100644 --- a/mfio-rt/src/lib.rs +++ b/mfio-rt/src/lib.rs @@ -18,20 +18,23 @@ use serde::{Deserialize, Serialize}; use std::net::{SocketAddr, ToSocketAddrs}; #[cfg(feature = "std")] -use std::path::{Path, PathBuf}; -#[cfg(not(feature = "std"))] -pub type Path = str; +pub use std::path::{Component, Path, PathBuf}; + +// We may later consider supporting non-unix paths in no_std scenarios, but currently, this is not +// the case, because TypedPath requires a lifetime argument. #[cfg(not(feature = "std"))] -pub type PathBuf = String; +pub use typed_path::{UnixComponent as Component, UnixPath as Path, UnixPathBuf as PathBuf}; #[cfg(feature = "native")] pub mod native; mod util; +#[cfg(any(feature = "virt", test, miri))] +pub mod virt; #[doc(hidden)] pub mod __doctest; -#[cfg(all(any(feature = "test_suite", test), feature = "std"))] +#[cfg(any(feature = "test_suite", test))] pub mod test_suite; #[cfg(feature = "native")] @@ -127,11 +130,11 @@ pub trait Fs: IoBackend { /// changed in this program. fn current_dir(&self) -> &Self::DirHandle<'_>; - fn open( - &self, - path: &Path, + fn open<'a>( + &'a self, + path: &'a Path, options: OpenOptions, - ) -> as DirHandle>::OpenFileFuture<'_> { + ) -> as DirHandle>::OpenFileFuture<'a> { self.current_dir().open_file(path, options) } } @@ -166,16 +169,17 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// use mfio_rt::{DirHandle, Fs}; - /// use std::path::Path; + /// // On no_std mfio_rt re-exports typed_path::UnixPath as Path + /// use mfio_rt::Path; /// /// let dir = fs.current_dir(); /// /// let path = dir.path().await.unwrap(); /// - /// assert_ne!(path, Path::new("/")); + /// assert_ne!(path, Path::new("/dev")); /// # }); /// ``` fn path(&self) -> Self::PathFuture<'_>; @@ -188,7 +192,7 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// use futures::StreamExt; /// use mfio::error::Error; @@ -226,7 +230,7 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// use mfio::traits::IoRead; /// use mfio_rt::{DirHandle, Fs, OpenOptions}; @@ -247,7 +251,11 @@ pub trait DirHandle: Sized { /// assert!(s.contains("mfio")); /// # }); /// ``` - fn open_file>(&self, path: P, options: OpenOptions) -> Self::OpenFileFuture<'_>; + fn open_file<'a, P: AsRef + ?Sized>( + &'a self, + path: &'a P, + options: OpenOptions, + ) -> Self::OpenFileFuture<'a>; /// Opens a directory. /// @@ -257,7 +265,7 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// use futures::StreamExt; /// use mfio::traits::IoRead; @@ -284,18 +292,18 @@ pub trait DirHandle: Sized { /// /// # }); /// ``` - fn open_dir>(&self, path: P) -> Self::OpenDirFuture<'_>; + fn open_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpenDirFuture<'a>; /// Retrieves file metadata. /// /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn metadata>(&self, path: P) -> Self::MetadataFuture<'_>; + fn metadata<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::MetadataFuture<'a>; /// Do an operation. /// @@ -304,25 +312,25 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn do_op>(&self, operation: DirOp

) -> Self::OpFuture<'_>; + fn do_op<'a, P: AsRef + ?Sized>(&'a self, operation: DirOp<&'a P>) -> Self::OpFuture<'a>; /// /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn set_permissions>( - &self, - path: P, + fn set_permissions<'a, P: AsRef + ?Sized>( + &'a self, + path: &'a P, permissions: Permissions, - ) -> Self::OpFuture<'_> { + ) -> Self::OpFuture<'a> { self.do_op(DirOp::SetPermissions { path, permissions }) } @@ -330,11 +338,11 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn remove_dir>(&self, path: P) -> Self::OpFuture<'_> { + fn remove_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpFuture<'a> { self.do_op(DirOp::RemoveDir { path }) } @@ -342,11 +350,11 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn remove_dir_all>(&self, path: P) -> Self::OpFuture<'_> { + fn remove_dir_all<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpFuture<'a> { self.do_op(DirOp::RemoveDirAll { path }) } @@ -354,11 +362,35 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] + /// # mfio_rt::__doctest::run_each(|fs| async { + /// # }); + /// ``` + fn create_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpFuture<'a> { + self.do_op(DirOp::CreateDir { path }) + } + + /// + /// # Examples + /// + /// ``` + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] + /// # mfio_rt::__doctest::run_each(|fs| async { + /// # }); + /// ``` + fn create_dir_all<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpFuture<'a> { + self.do_op(DirOp::CreateDirAll { path }) + } + + /// + /// # Examples + /// + /// ``` + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn remove_file>(&self, path: P) -> Self::OpFuture<'_> { + fn remove_file<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpFuture<'a> { self.do_op(DirOp::RemoveFile { path }) } @@ -366,11 +398,11 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn rename>(&self, from: P, to: P) -> Self::OpFuture<'_> { + fn rename<'a, P: AsRef + ?Sized>(&'a self, from: &'a P, to: &'a P) -> Self::OpFuture<'a> { self.do_op(DirOp::Rename { from, to }) } @@ -378,12 +410,12 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` // TODO: reflinking option - fn copy>(&self, from: P, to: P) -> Self::OpFuture<'_> { + fn copy<'a, P: AsRef + ?Sized>(&'a self, from: &'a P, to: &'a P) -> Self::OpFuture<'a> { self.do_op(DirOp::Copy { from, to }) } @@ -391,11 +423,15 @@ pub trait DirHandle: Sized { /// # Examples /// /// ``` - /// # #[cfg(feature = "std")] + /// # #[cfg(any(miri, feature = "std", feature = "virt"))] /// # mfio_rt::__doctest::run_each(|fs| async { /// # }); /// ``` - fn hard_link>(&self, from: P, to: P) -> Self::OpFuture<'_> { + fn hard_link<'a, P: AsRef + ?Sized>( + &'a self, + from: &'a P, + to: &'a P, + ) -> Self::OpFuture<'a> { self.do_op(DirOp::HardLink { from, to }) } @@ -410,6 +446,8 @@ pub enum DirOp> { SetPermissions { path: P, permissions: Permissions }, RemoveDir { path: P }, RemoveDirAll { path: P }, + CreateDir { path: P }, + CreateDirAll { path: P }, RemoveFile { path: P }, Rename { from: P, to: P }, Copy { from: P, to: P }, @@ -429,6 +467,12 @@ impl> DirOp

{ Self::RemoveDirAll { path } => DirOp::RemoveDirAll { path: path.as_ref(), }, + Self::CreateDir { path } => DirOp::CreateDir { + path: path.as_ref(), + }, + Self::CreateDirAll { path } => DirOp::CreateDirAll { + path: path.as_ref(), + }, Self::RemoveFile { path } => DirOp::RemoveFile { path: path.as_ref(), }, @@ -448,7 +492,43 @@ impl> DirOp

{ } } -impl + Copy> DirOp

{ +impl<'a, P: AsRef + ?Sized> DirOp<&'a P> { + pub fn into_path(self) -> DirOp<&'a Path> { + match self { + Self::SetPermissions { path, permissions } => DirOp::SetPermissions { + path: path.as_ref(), + permissions, + }, + Self::RemoveDir { path } => DirOp::RemoveDir { + path: path.as_ref(), + }, + Self::RemoveDirAll { path } => DirOp::RemoveDirAll { + path: path.as_ref(), + }, + Self::CreateDir { path } => DirOp::CreateDir { + path: path.as_ref(), + }, + Self::CreateDirAll { path } => DirOp::CreateDirAll { + path: path.as_ref(), + }, + Self::RemoveFile { path } => DirOp::RemoveFile { + path: path.as_ref(), + }, + Self::Rename { from, to } => DirOp::Rename { + from: from.as_ref(), + to: to.as_ref(), + }, + Self::Copy { from, to } => DirOp::Copy { + from: from.as_ref(), + to: to.as_ref(), + }, + Self::HardLink { from, to } => DirOp::HardLink { + from: from.as_ref(), + to: to.as_ref(), + }, + } + } + pub fn into_pathbuf(self) -> DirOp { match self { Self::SetPermissions { path, permissions } => DirOp::SetPermissions { @@ -461,6 +541,12 @@ impl + Copy> DirOp

{ Self::RemoveDirAll { path } => DirOp::RemoveDirAll { path: path.as_ref().into(), }, + Self::CreateDir { path } => DirOp::CreateDir { + path: path.as_ref().into(), + }, + Self::CreateDirAll { path } => DirOp::CreateDirAll { + path: path.as_ref().into(), + }, Self::RemoveFile { path } => DirOp::RemoveFile { path: path.as_ref().into(), }, @@ -479,12 +565,6 @@ impl + Copy> DirOp

{ } } - #[cfg(not(feature = "std"))] - pub fn into_string(self) -> DirOp { - self.into_pathbuf() - } - - #[cfg(feature = "std")] pub fn into_string(self) -> DirOp { match self { Self::SetPermissions { path, permissions } => DirOp::SetPermissions { @@ -497,6 +577,12 @@ impl + Copy> DirOp

{ Self::RemoveDirAll { path } => DirOp::RemoveDirAll { path: path.as_ref().to_string_lossy().into(), }, + Self::CreateDir { path } => DirOp::CreateDir { + path: path.as_ref().to_string_lossy().into(), + }, + Self::CreateDirAll { path } => DirOp::CreateDirAll { + path: path.as_ref().to_string_lossy().into(), + }, Self::RemoveFile { path } => DirOp::RemoveFile { path: path.as_ref().to_string_lossy().into(), }, @@ -569,14 +655,30 @@ impl From for Permissions { pub struct Metadata { pub permissions: Permissions, pub len: u64, - /// Modified time (since unix epoch) + /// Modified time (since unix epoch, or other point) pub modified: Option, - /// Accessed time (since unix epoch) + /// Accessed time (since unix epoch, or other point) pub accessed: Option, - /// Created time (since unix epoch) + /// Created time (since unix epoch, or other point) pub created: Option, } +impl Metadata { + pub fn empty_file(permissions: Permissions, created: Option) -> Self { + Self { + permissions, + len: 0, + modified: None, + accessed: None, + created, + } + } + + pub fn empty_dir(permissions: Permissions, created: Option) -> Self { + Self::empty_file(permissions, created) + } +} + pub trait FileHandle: AsyncRead + AsyncWrite {} impl + AsyncWrite> FileHandle for T {} diff --git a/mfio-rt/src/native/impls/iocp/tcp_stream.rs b/mfio-rt/src/native/impls/iocp/tcp_stream.rs index 92fc43c..b14dcc3 100644 --- a/mfio-rt/src/native/impls/iocp/tcp_stream.rs +++ b/mfio-rt/src/native/impls/iocp/tcp_stream.rs @@ -18,7 +18,6 @@ use crate::{Shutdown, TcpStreamHandle}; use ::windows::Win32::Foundation::HANDLE; use ::windows::Win32::Networking::WinSock::{ shutdown, WSAGetLastError, SD_BOTH, SD_RECEIVE, SD_SEND, SOCKET, WSABUF, WSAECONNRESET, - WSAENOTCONN, }; use ::windows::Win32::System::IO::CancelIoEx; use ::windows::Win32::System::IO::OVERLAPPED; @@ -228,7 +227,7 @@ impl StreamInner { let operation = Operation { header: hdr.into(), mode: OperationMode::StreamWrite(WsaOp { - bufs: (&queue[..]) as *const [_] as *const [WSABUF], + bufs: queue as *const [_] as *const [WSABUF], transferred: 0, flags: 0, sock_idx: idx, diff --git a/mfio-rt/src/native/mod.rs b/mfio-rt/src/native/mod.rs index 7369e29..7cd3e90 100644 --- a/mfio-rt/src/native/mod.rs +++ b/mfio-rt/src/native/mod.rs @@ -18,6 +18,9 @@ use crate::{ TcpStreamHandle, }; +#[cfg(test)] +use crate::{net_test_suite, test_suite}; + mod impls; macro_rules! fs_dispatch { @@ -502,6 +505,10 @@ impl NativeRt { pub fn cancel_all_ops(&self) { self.cwd.instance.cancel_all_ops() } + + pub fn set_cwd(&mut self, dir: PathBuf) { + self.cwd.dir = Some(dir); + } } impl From for NativeRt { @@ -573,7 +580,11 @@ impl DirHandle for NativeRtDir { /// /// This function accepts an absolute or relative path to a file for reading. If the path is /// relative, it is opened relative to this `DirHandle`. - fn open_file>(&self, path: P, options: OpenOptions) -> Self::OpenFileFuture<'_> { + fn open_file<'a, P: AsRef + ?Sized>( + &'a self, + path: &'a P, + options: OpenOptions, + ) -> Self::OpenFileFuture<'a> { let (tx, rx) = oneshot::channel(); if let Ok(path) = self.join_path(path) { @@ -597,7 +608,7 @@ impl DirHandle for NativeRtDir { /// /// This function accepts an absolute or relative path to a directory for reading. If the path /// is relative, it is opened relative to this `DirHandle`. - fn open_dir>(&self, path: P) -> Self::OpenDirFuture<'_> { + fn open_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpenDirFuture<'a> { let dir = self.join_path(path).map_err(from_io_error).and_then(|v| { if v.is_dir() { Ok(Self { @@ -615,7 +626,7 @@ impl DirHandle for NativeRtDir { ready(dir) } - fn metadata>(&self, path: P) -> Self::MetadataFuture<'_> { + fn metadata<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::MetadataFuture<'a> { let (tx, rx) = oneshot::channel(); if let Ok(path) = self.join_path(path) { @@ -633,7 +644,7 @@ impl DirHandle for NativeRtDir { /// Do an operation. /// /// This function performs an operation from the [`DirOp`] enum. - fn do_op>(&self, operation: DirOp

) -> Self::OpFuture<'_> { + fn do_op<'a, P: AsRef + ?Sized>(&'a self, operation: DirOp<&'a P>) -> Self::OpFuture<'a> { let (tx, rx) = oneshot::channel(); let _ = self.ops.send(RtBgOp::DirOp { @@ -771,6 +782,8 @@ impl RtBgOp { } DirOp::RemoveDir { path } => fs::remove_dir(path), DirOp::RemoveDirAll { path } => fs::remove_dir_all(path), + DirOp::CreateDir { path } => fs::create_dir(path), + DirOp::CreateDirAll { path } => fs::create_dir_all(path), DirOp::RemoveFile { path } => fs::remove_file(path), DirOp::Rename { from, to } => fs::rename(from, to), DirOp::Copy { from, to } => fs::copy(from, to).map(|_| ()), @@ -1102,3 +1115,53 @@ mod tests { } } } + +#[cfg(test)] +test_suite!(tests_default, |test_name, closure| { + let _ = ::env_logger::builder().is_test(true).try_init(); + let mut rt = crate::NativeRt::default(); + let rt = staticify(&mut rt); + let dir = TempDir::new(test_name).unwrap(); + rt.set_cwd(dir.path().to_path_buf()); + rt.run(move |rt| { + let run = TestRun::new(rt, dir); + closure(run) + }); +}); + +#[cfg(test)] +test_suite!(tests_all, |test_name, closure| { + let _ = ::env_logger::builder().is_test(true).try_init(); + for (name, rt) in crate::NativeRt::builder().enable_all().build_each() { + println!("{name}"); + if let Ok(mut rt) = rt { + let rt = staticify(&mut rt); + let dir = TempDir::new(test_name).unwrap(); + rt.set_cwd(dir.path().to_path_buf()); + rt.run(move |rt| { + let run = TestRun::new(rt, dir); + closure(run) + }); + } + } +}); + +#[cfg(test)] +net_test_suite!(net_tests_default, |closure| { + let _ = ::env_logger::builder().is_test(true).try_init(); + let mut rt = crate::NativeRt::default(); + let rt = staticify(&mut rt); + rt.run(closure); +}); + +#[cfg(test)] +net_test_suite!(net_tests_all, |closure| { + let _ = ::env_logger::builder().is_test(true).try_init(); + for (name, rt) in crate::NativeRt::builder().enable_all().build_each() { + println!("{name}"); + if let Ok(mut rt) = rt { + let rt = staticify(&mut rt); + rt.run(closure); + } + } +}); diff --git a/mfio-rt/src/test_suite.rs b/mfio-rt/src/test_suite.rs index 4c53329..096f159 100644 --- a/mfio-rt/src/test_suite.rs +++ b/mfio-rt/src/test_suite.rs @@ -1,16 +1,24 @@ -pub use crate::{DirHandle, Fs, OpenOptions, Shutdown, Tcp, TcpListenerHandle, TcpStreamHandle}; -use async_semaphore::Semaphore; +use crate::util::diff_paths; +pub use crate::{DirHandle, Fs, OpenOptions, Path, Shutdown}; +pub use alloc::{ + collections::BTreeSet, + format, + string::{String, ToString}, + vec, + vec::Vec, +}; pub use core::future::Future; pub use futures::StreamExt; pub use mfio::backend::IoBackend; pub use mfio::traits::{IoRead, IoWrite}; pub use once_cell::sync::Lazy; -pub use std::collections::BTreeSet; -pub use std::fs; -pub use std::path::Path; -use std::pin::pin; pub use tempdir::TempDir; +#[cfg(feature = "std")] +pub use crate::{Tcp, TcpListenerHandle, TcpStreamHandle}; +#[cfg(feature = "std")] +pub use std::fs; + const FILES: &[(&str, &str)] = &[ ("Cargo.toml", include_str!("../Cargo.toml")), ("src/lib.rs", include_str!("lib.rs")), @@ -37,11 +45,9 @@ const DIRECTORIES: &[&str] = &[ "p1/p2/p3/p4/p5/p6", ]; -/// Maximum number of concurrent TCP tests (listener, client) pairs at a time. -static TCP_SEM: Semaphore = Semaphore::new(16); - -static CTX: Lazy = Lazy::new(TestCtx::new); +pub static CTX: Lazy = Lazy::new(TestCtx::new); +#[cfg(not(miri))] const fn hash(mut x: u64) -> u64 { x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9u64); x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111ebu64); @@ -109,6 +115,14 @@ impl TestCtx { .collect() } + pub fn dirs(&self) -> &[String] { + &self.dirs + } + + pub fn files(&self) -> &[(String, Vec)] { + &self.files + } + pub fn all_dirs(&self) -> BTreeSet { let mut dirs = BTreeSet::new(); @@ -139,6 +153,7 @@ impl TestCtx { dirs } + #[cfg(feature = "std")] pub fn build_in_path(&self, path: &Path) { for d in &self.dirs { let _ = fs::create_dir_all(path.join(d)); @@ -151,308 +166,365 @@ impl TestCtx { fs::write(path.join(p), data).unwrap(); } } -} -pub struct NetTestRun<'a, T> { - ctx: &'static TestCtx, - rt: &'a T, -} + pub async fn build_in_fs(&self, fs: &impl Fs) { + let cdir = fs.current_dir(); -impl<'a, T: Tcp> NetTestRun<'a, T> { - pub fn new(rt: &'a T) -> Self { - Self { ctx: &*CTX, rt } - } - - pub async fn tcp_connect(&self) { - use std::net::TcpListener; - - let _sem = TCP_SEM.acquire().await; + for d in &self.dirs { + #[cfg(feature = "std")] + println!("Dir: {d:?}"); + let _ = cdir.create_dir_all(d).await; + } - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); + for (p, data) in &self.files { + #[cfg(feature = "std")] + println!("File: {p:?}"); + if let Some((d, _)) = p.rsplit_once('/') { + let _ = cdir.create_dir_all(d).await; + } + let file = cdir + .open_file(p, OpenOptions::new().create_new(true).write(true)) + .await + .unwrap(); + file.write_all(0, &data[..]).await.unwrap(); + } + } +} - let jh = std::thread::spawn(move || { - let _ = listener.accept().unwrap(); - }); +#[cfg(feature = "std")] +pub mod net { + use super::*; + use async_semaphore::Semaphore; + use core::pin::pin; - self.rt.connect(addr).await.unwrap(); + /// Maximum number of concurrent TCP tests (listener, client) pairs at a time. + static TCP_SEM: Semaphore = Semaphore::new(16); - jh.join().unwrap(); + pub struct NetTestRun<'a, T> { + ctx: &'static TestCtx, + rt: &'a T, } - pub async fn tcp_listen(&self) { - use std::net::TcpStream; + impl<'a, T: Tcp> NetTestRun<'a, T> { + pub fn new(rt: &'a T) -> Self { + Self { ctx: &*CTX, rt } + } - let _sem = TCP_SEM.acquire().await; + pub async fn tcp_connect(&self) { + use std::net::TcpListener; - let listener = self.rt.bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); + let _sem = TCP_SEM.acquire().await; - let jh = std::thread::spawn(move || { - let _ = TcpStream::connect(addr).unwrap(); - }); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); - let mut listener = pin!(listener); + let jh = std::thread::spawn(move || { + let _ = listener.accept().unwrap(); + }); - let v = listener.next().await.unwrap(); + self.rt.connect(addr).await.unwrap(); - jh.join().unwrap(); - } + jh.join().unwrap(); + } - pub fn tcp_receive(&self) -> impl Iterator + '_> + '_ { - use mfio::io::NoPos; - use std::net::TcpListener; + pub async fn tcp_listen(&self) { + use std::net::TcpStream; - self.ctx.files.iter().map(move |(name, data)| async move { let _sem = TCP_SEM.acquire().await; - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let listener = self.rt.bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - let (tx, rx) = flume::bounded(1); - let jh = std::thread::spawn(move || { - use std::io::Write; - let (mut sock, _) = listener.accept().unwrap(); - sock.write_all(data).unwrap(); - let _ = sock.shutdown(std::net::Shutdown::Both); - let _ = tx.send(()); + let _ = TcpStream::connect(addr).unwrap(); }); - let conn = self.rt.connect(addr).await.unwrap(); + let mut listener = pin!(listener); - let mut out = vec![]; - conn.read_to_end(NoPos::new(), &mut out).await.unwrap(); - assert!( - &out[..] == data, - "{name} does not match ({} vs {})", - out.len(), - data.len() - ); + let _ = listener.next().await.unwrap(); - let _ = rx.recv_async().await; jh.join().unwrap(); - }) - } + } - pub fn tcp_send(&self) -> impl Iterator + '_> + '_ { - use mfio::io::NoPos; - use std::net::TcpListener; + pub fn tcp_receive(&self) -> impl Iterator + '_> + '_ { + use mfio::io::NoPos; + use std::net::TcpListener; - self.ctx.files.iter().map(move |(name, data)| async move { - let _sem = TCP_SEM.acquire().await; + self.ctx.files.iter().map(move |(name, data)| async move { + let _sem = TCP_SEM.acquire().await; - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); - let (tx, rx) = flume::bounded(1); + let (tx, rx) = flume::bounded(1); - let jh = std::thread::spawn(move || { - use std::io::Read; - let (mut sock, _) = listener.accept().unwrap(); - let mut out = vec![]; - sock.read_to_end(&mut out).unwrap(); - let _ = tx.send(()); - out - }); + let jh = std::thread::spawn(move || { + use std::io::Write; + let (mut sock, _) = listener.accept().unwrap(); + sock.write_all(data).unwrap(); + let _ = sock.shutdown(std::net::Shutdown::Both); + let _ = tx.send(()); + }); - { let conn = self.rt.connect(addr).await.unwrap(); - conn.write_all(NoPos::new(), &data[..]).await.unwrap(); - core::mem::drop(conn); - } - let _ = rx.recv_async().await; + let mut out = vec![]; + conn.read_to_end(NoPos::new(), &mut out).await.unwrap(); + assert!( + &out[..] == data, + "{name} does not match ({} vs {})", + out.len(), + data.len() + ); + + let _ = rx.recv_async().await; + jh.join().unwrap(); + }) + } + + pub fn tcp_send(&self) -> impl Iterator + '_> + '_ { + use mfio::io::NoPos; + use std::net::TcpListener; - let ret = jh.join().unwrap(); - assert!( - &ret == data, - "{name} does not match ({} vs {})", - ret.len(), - data.len() - ); - }) - } + self.ctx.files.iter().map(move |(name, data)| async move { + let _sem = TCP_SEM.acquire().await; - pub fn tcp_echo_client(&self) -> impl Iterator + '_> + '_ { - use mfio::io::NoPos; - use std::net::TcpListener; + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); - self.ctx.files.iter().map(move |(name, data)| async move { - let _sem = TCP_SEM.acquire().await; + let (tx, rx) = flume::bounded(1); - let listener = TcpListener::bind("127.0.0.1:0").unwrap(); - let addr = listener.local_addr().unwrap(); + let jh = std::thread::spawn(move || { + use std::io::Read; + let (mut sock, _) = listener.accept().unwrap(); + let mut out = vec![]; + sock.read_to_end(&mut out).unwrap(); + let _ = tx.send(()); + out + }); - let (tx, rx) = flume::bounded(1); + { + let conn = self.rt.connect(addr).await.unwrap(); + conn.write_all(NoPos::new(), &data[..]).await.unwrap(); + core::mem::drop(conn); + } - let jh = std::thread::spawn(move || { - let (sock, _) = listener.accept().unwrap(); - log::trace!("Echo STD server start"); - std::io::copy(&mut &sock, &mut &sock).unwrap(); - log::trace!("Echo STD server end"); - let _ = tx.send(()); - }); + let _ = rx.recv_async().await; - let ret = { - let conn = self.rt.connect(addr).await.unwrap(); + let ret = jh.join().unwrap(); + assert!( + &ret == data, + "{name} does not match ({} vs {})", + ret.len(), + data.len() + ); + }) + } - let write = async { - conn.write_all(NoPos::new(), &data[..]).await.unwrap(); - log::trace!("Written"); - conn.shutdown(Shutdown::Write).unwrap(); - }; + pub fn tcp_echo_client(&self) -> impl Iterator + '_> + '_ { + use mfio::io::NoPos; + use std::net::TcpListener; + + self.ctx.files.iter().map(move |(name, data)| async move { + let _sem = TCP_SEM.acquire().await; - let read = async { - let mut ret = vec![]; - conn.read_to_end(NoPos::new(), &mut ret).await.unwrap(); - ret + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let addr = listener.local_addr().unwrap(); + + let (tx, rx) = flume::bounded(1); + + let jh = std::thread::spawn(move || { + let (sock, _) = listener.accept().unwrap(); + log::trace!("Echo STD server start"); + std::io::copy(&mut &sock, &mut &sock).unwrap(); + log::trace!("Echo STD server end"); + let _ = tx.send(()); + }); + + let ret = { + let conn = self.rt.connect(addr).await.unwrap(); + + let write = async { + conn.write_all(NoPos::new(), &data[..]).await.unwrap(); + log::trace!("Written"); + conn.shutdown(Shutdown::Write).unwrap(); + }; + + let read = async { + let mut ret = vec![]; + conn.read_to_end(NoPos::new(), &mut ret).await.unwrap(); + ret + }; + + futures::join!(write, read).1 }; - futures::join!(write, read).1 - }; + let _ = rx.recv_async().await; + jh.join().unwrap(); - let _ = rx.recv_async().await; - jh.join().unwrap(); + assert!( + &ret == data, + "{name} does not match ({} vs {})", + ret.len(), + data.len() + ); + }) + } - assert!( - &ret == data, - "{name} does not match ({} vs {})", - ret.len(), - data.len() - ); - }) - } + pub fn tcp_echo_server(&self) -> impl Iterator + '_> + '_ { + use core::mem::MaybeUninit; + use flume::SendError; + use mfio::io::{Read, StreamIoExt, VecPacket}; + use std::net::TcpStream; - pub fn tcp_echo_server(&self) -> impl Iterator + '_> + '_ { - use core::mem::MaybeUninit; - use flume::SendError; - use mfio::io::{Read, StreamIoExt, VecPacket}; - use std::net::TcpStream; + self.ctx.files.iter().map(move |(name, data)| async move { + let _sem = TCP_SEM.acquire().await; - self.ctx.files.iter().map(move |(name, data)| async move { - let _sem = TCP_SEM.acquire().await; + let listener = self.rt.bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); - let listener = self.rt.bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); + let (tx, rx) = flume::bounded(1); - let (tx, rx) = flume::bounded(1); + let jh = std::thread::spawn(move || { + use std::io::{Read, Write}; + let sock = TcpStream::connect(addr).unwrap(); + let sock = std::sync::Arc::new(sock); - let jh = std::thread::spawn(move || { - use std::io::{Read, Write}; - let sock = TcpStream::connect(addr).unwrap(); - let sock = std::sync::Arc::new(sock); - - let jh = std::thread::spawn({ - let sock = sock.clone(); - move || { - (&mut &*sock).write_all(data).unwrap(); - sock.shutdown(Shutdown::Write.into()).unwrap(); - } - }); + let jh = std::thread::spawn({ + let sock = sock.clone(); + move || { + (&mut &*sock).write_all(data).unwrap(); + sock.shutdown(Shutdown::Write.into()).unwrap(); + } + }); - let mut out = vec![]; - let _ = (&mut &*sock).read_to_end(&mut out); + let mut out = vec![]; + let _ = (&mut &*sock).read_to_end(&mut out); - jh.join().unwrap(); - let _ = tx.send(()); + jh.join().unwrap(); + let _ = tx.send(()); - out - }); + out + }); - let mut listener = pin!(listener); + let mut listener = pin!(listener); - let (sock, _) = listener.next().await.unwrap(); + let (sock, _) = listener.next().await.unwrap(); - // TODO: we need a much simpler std::io::copy equivalent... - let (rtx, rrx) = flume::bounded(4); - let (mtx, mrx) = flume::bounded(4); + // TODO: we need a much simpler std::io::copy equivalent... + let (rtx, rrx) = flume::bounded(4); + let (mtx, mrx) = flume::bounded(4); - for i in 0..rtx.capacity().unwrap() { - let _ = rtx.send_async((i, vec![MaybeUninit::uninit(); 64])).await; - } + for i in 0..rtx.capacity().unwrap() { + let _ = rtx.send_async((i, vec![MaybeUninit::uninit(); 64])).await; + } - let read = { - let sock = &sock; - async move { - rrx.stream() - .then(|(i, mut v)| async move { - v.resize(v.len() * 2, MaybeUninit::uninit()); - let p = VecPacket::from(v); + let read = { + let sock = &sock; + async move { + rrx.stream() + .then(|(i, mut v)| async move { + v.resize(v.len() * 2, MaybeUninit::uninit()); + let p = VecPacket::from(v); + let p = sock.stream_io(p).await; + let len = p.simple_contiguous_slice().unwrap().len(); + let mut v = p.take(); + let v = unsafe { + v.set_len(len); + core::mem::transmute::>, Vec>(v) + }; + if v.is_empty() { + log::trace!("Empty @ {i}"); + Err(SendError((i, v))) + } else { + log::trace!("Forward {i}"); + Ok((i, v)) + } + }) + .take_while(|v| core::future::ready(v.is_ok())) + .forward(mtx.sink()) + .await + } + }; + + let write = async { + let sock = &sock; + let rtx = &rtx; + mrx.stream() + .for_each(|(i, v)| async move { + let p = VecPacket::::from(v); let p = sock.stream_io(p).await; - let len = p.simple_contiguous_slice().unwrap().len(); let mut v = p.take(); let v = unsafe { - v.set_len(len); - core::mem::transmute::>, Vec>(v) + v.set_len(v.capacity()); + core::mem::transmute::, Vec>>(v) }; - if v.is_empty() { - log::trace!("Empty @ {i}"); - Err(SendError((i, v))) - } else { - log::trace!("Forward {i}"); - Ok((i, v)) - } + log::trace!("Forward back {i}"); + // We can't use stream.forward(), because that might cancel the .then() in + // unfinished state. + let _ = rtx.send_async((i, v)).await; }) - .take_while(|v| core::future::ready(v.is_ok())) - .forward(mtx.sink()) .await - } - }; - - let write = async { - let sock = &sock; - let rtx = &rtx; - mrx.stream() - .for_each(|(i, v)| async move { - let p = VecPacket::::from(v); - let p = sock.stream_io(p).await; - let mut v = p.take(); - let v = unsafe { - v.set_len(v.capacity()); - core::mem::transmute::, Vec>>(v) - }; - log::trace!("Forward back {i}"); - // We can't use stream.forward(), because that might cancel the .then() in - // unfinished state. - let _ = rtx.send_async((i, v)).await; - }) - .await - }; - - let _ = futures::join!(read, write); - log::trace!("Echo server"); - core::mem::drop(sock); - - let _ = rx.recv_async().await; - let ret = jh.join().unwrap(); - - assert!( - &ret == data, - "{name} does not match ({} vs {})", - ret.len(), - data.len() - ); - }) + }; + + let _ = futures::join!(read, write); + log::trace!("Echo server"); + core::mem::drop(sock); + + let _ = rx.recv_async().await; + let ret = jh.join().unwrap(); + + assert!( + &ret == data, + "{name} does not match ({} vs {})", + ret.len(), + data.len() + ); + }) + } } } -pub struct TestRun<'a, T> { +pub struct TestRun<'a, T, D> { ctx: &'a TestCtx, rt: &'a T, - dir: TempDir, + _drop_guard: D, } -impl<'a, T: Fs> TestRun<'a, T> { +#[cfg(feature = "std")] +impl<'a, T: Fs> TestRun<'a, T, TempDir> { pub fn new(rt: &'a T, dir: TempDir) -> Self { CTX.build_in_path(dir.path()); + Self { + rt, + _drop_guard: dir, + ctx: &CTX, + } + } +} - Self { rt, dir, ctx: &CTX } +impl<'a, T: Fs, D> TestRun<'a, T, D> { + pub async fn built_by_rt(rt: &'a T, drop_guard: D) -> TestRun<'a, T, D> { + CTX.build_in_fs(rt).await; + Self { + rt, + _drop_guard: drop_guard, + ctx: &CTX, + } + } + + pub fn assume_built(rt: &'a T, drop_guard: D) -> TestRun<'a, T, D> { + Self { + rt, + _drop_guard: drop_guard, + ctx: &CTX, + } } pub fn files_equal(&self) -> impl Iterator + '_> + '_ { self.ctx.files.iter().map(move |(p, data)| async move { - let path = &self.dir.path().join(p); + let cur_dir = self.rt.current_dir().path().await.unwrap(); + let path = &cur_dir.join(p); let fh = self .rt .open(path, OpenOptions::new().read(true)) @@ -466,7 +538,8 @@ impl<'a, T: Fs> TestRun<'a, T> { pub fn files_equal_rel(&self) -> impl Iterator + '_> + '_ { self.ctx.files.iter().map(move |(p, data)| async move { - let path = &self.dir.path().join(p); + let cur_dir = self.rt.current_dir().path().await.unwrap(); + let path = &cur_dir.join(p); let dh = self .rt .current_dir() @@ -496,11 +569,16 @@ impl<'a, T: Fs> TestRun<'a, T> { pub fn writes_equal<'b>( &'b self, - tdir: &'b TempDir, + tdir: &'b Path, ) -> impl Iterator + 'b> + 'b { self.ctx.files.iter().map(move |(p, data)| async move { - let path = &tdir.path().join(p); - let _ = fs::create_dir_all(path.parent().unwrap()); + let tdir = self.rt.current_dir().path().await.unwrap().join(tdir); + let path = &tdir.join(p); + self.rt + .current_dir() + .create_dir_all(path.parent().unwrap()) + .await + .unwrap(); let fh = self .rt @@ -509,18 +587,35 @@ impl<'a, T: Fs> TestRun<'a, T> { .unwrap(); fh.write_all(0, &data[..]).await.unwrap(); - let buf = fs::read(path).unwrap(); + + core::mem::drop(fh); + + let fh = self + .rt + .open(path, OpenOptions::new().read(true)) + .await + .unwrap(); + + let mut buf = vec![]; + fh.read_to_end(0, &mut buf).await.unwrap(); + + assert_eq!(core::str::from_utf8(&buf), core::str::from_utf8(data)); assert!(&buf == data, "File {p} does not match!"); }) } pub fn writes_equal_rel<'b>( &'b self, - tdir: &'b TempDir, + tdir: &'b Path, ) -> impl Iterator + 'b> + 'b { self.ctx.files.iter().map(move |(p, data)| async move { - let path = &tdir.path().join(p); - let _ = fs::create_dir_all(path.parent().unwrap()); + let tdir = self.rt.current_dir().path().await.unwrap().join(tdir); + let path = &tdir.join(p); + self.rt + .current_dir() + .create_dir_all(path.parent().unwrap()) + .await + .unwrap(); let dh = self .rt @@ -529,16 +624,25 @@ impl<'a, T: Fs> TestRun<'a, T> { .await .unwrap(); + let filename = path.file_name().unwrap(); + let fh = dh - .open_file( - path.file_name().unwrap(), - OpenOptions::new().create(true).write(true), - ) + .open_file(filename, OpenOptions::new().create(true).write(true)) .await .unwrap(); fh.write_all(0, &data[..]).await.unwrap(); - let buf = fs::read(path).unwrap(); + + core::mem::drop(fh); + + let fh = dh + .open_file(filename, OpenOptions::new().read(true)) + .await + .unwrap(); + + let mut buf = vec![]; + fh.read_to_end(0, &mut buf).await.unwrap(); + assert!(&buf == data, "File {p} does not match!"); }) } @@ -547,8 +651,9 @@ impl<'a, T: Fs> TestRun<'a, T> { let all_dirs = self.ctx.all_dirs(); log::error!("{all_dirs:?}"); all_dirs.into_iter().map(move |d| async move { + let cur_path = self.rt.current_dir().path().await.unwrap(); log::error!("Join with {d}"); - let path = &self.dir.path().join(&d); + let path = &cur_path.join(&d); log::error!("Open dir: {path:?}"); let dh = self .rt @@ -574,19 +679,19 @@ impl<'a, T: Fs> TestRun<'a, T> { self.ctx.all_dirs().into_iter().map(move |d2| { let d1 = d1.clone(); async move { - let path1 = &self.dir.path().join(&d1); - let path2 = &self.dir.path().join(&d2); + let cur_path = curdir.path().await.unwrap(); + let path1 = &cur_path.join(&d1); + let path2 = &cur_path.join(&d2); - let relpath1 = pathdiff::diff_paths(path1, path2).unwrap(); - let relpath2 = pathdiff::diff_paths(&d1, &d2).unwrap(); + let relpath1 = diff_paths(path1, path2).unwrap(); + let relpath2 = diff_paths(&d1, &d2).unwrap(); assert_eq!(&relpath1, &relpath2); let dh1 = curdir.open_dir(path1).await.unwrap(); let dh2 = curdir.open_dir(path2).await.unwrap(); let relpath3 = - pathdiff::diff_paths(dh1.path().await.unwrap(), dh2.path().await.unwrap()) - .unwrap(); + diff_paths(dh1.path().await.unwrap(), dh2.path().await.unwrap()).unwrap(); assert_eq!(&relpath1, &relpath3); } @@ -595,130 +700,133 @@ impl<'a, T: Fs> TestRun<'a, T> { } } -#[macro_export] -macro_rules! test_suite { - ($test_ident:ident, $fs_builder:expr) => { - #[cfg(test)] - #[allow(clippy::redundant_closure_call)] - mod $test_ident { - use $crate::test_suite::*; +async fn seq(i: impl Iterator + '_> + '_) { + for i in i { + i.await; + } +} - async fn seq(i: impl Iterator + '_> + '_) { - for i in i { - i.await; - } - } +async fn con(i: impl Iterator + '_> + '_) { + let unordered = i.collect::>(); + unordered.count().await; +} - async fn con(i: impl Iterator + '_> + '_) { - let unordered = i.collect::>(); - unordered.count().await; - } +pub mod fs_tests { + use super::*; + + pub async fn all_tests_seq(run: TestRun<'_, impl Fs, impl Sized>) { + seq(run.files_equal()).await; + seq(run.files_equal_rel()).await; + seq(run.dirs_equal()).await; + seq(run.walk_dirs()).await; + let tdir = Path::new("mfio-testsuite-writes"); + let tdir2 = Path::new("mfio-testsuite-writes2"); + seq(run.writes_equal(tdir)).await; + seq(run.writes_equal_rel(tdir2)).await; + } - fn staticify(val: &mut T) -> &'static mut T { - unsafe { core::mem::transmute(val) } - } + pub async fn all_tests_con(run: TestRun<'_, impl Fs, impl Sized>) { + futures::join! { + con(run.files_equal()), + con(run.files_equal_rel()), + con(run.dirs_equal()), + con(run.walk_dirs()), + }; + // We cannot put writes concurrently, because we are creating new directories, which are + // not expected by other tests. + let tdir = Path::new("mfio-testsuite-writes"); + let tdir2 = Path::new("mfio-testsuite-writes2"); + futures::join! { + seq(run.writes_equal(tdir)), + seq(run.writes_equal_rel(tdir2)), + }; + } - #[cfg(not(miri))] - #[test] - fn all_tests_seq() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let tdir = TempDir::new("mfio-testsuite-writes").unwrap(); - let tdir2 = TempDir::new("mfio-testsuite-writes").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.files_equal()).await; - seq(run.files_equal_rel()).await; - seq(run.dirs_equal()).await; - seq(run.walk_dirs()).await; - seq(run.writes_equal(&tdir)).await; - seq(run.writes_equal_rel(&tdir2)).await; - }); - } + pub async fn files_equal(run: TestRun<'_, impl Fs, impl Sized>) { + seq(run.files_equal()).await; + } - #[cfg(not(miri))] - #[test] - fn all_tests_con() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let tdir = TempDir::new("mfio-testsuite-writes").unwrap(); - let tdir2 = TempDir::new("mfio-testsuite-writes").unwrap(); - let run = TestRun::new(rt, dir); - futures::join! { - con(run.files_equal()), - con(run.files_equal_rel()), - con(run.dirs_equal()), - con(run.walk_dirs()), - seq(run.writes_equal(&tdir)), - seq(run.writes_equal_rel(&tdir2)), - } - }); - } + pub async fn files_equal_rel(run: TestRun<'_, impl Fs, impl Sized>) { + seq(run.files_equal_rel()).await; + } - #[test] - fn files_equal() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.files_equal()).await; - }); - } + pub async fn dirs_equal(run: TestRun<'_, impl Fs, impl Sized>) { + seq(run.dirs_equal()).await; + } - #[test] - fn files_equal_rel() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.files_equal_rel()).await; - }); - } + pub async fn walk_dirs(run: TestRun<'_, impl Fs, impl Sized>) { + seq(run.walk_dirs()).await; + } - #[test] - fn dirs_equal() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.dirs_equal()).await; - }); - } + pub async fn writes_equal(run: TestRun<'_, impl Fs, impl Sized>) { + let tdir = Path::new("mfio-testsuite-writes"); + seq(run.writes_equal(tdir)).await; + } - #[test] - fn walk_dirs() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.walk_dirs()).await; - }); - } + pub async fn writes_equal_rel(run: TestRun<'_, impl Fs, impl Sized>) { + let tdir = Path::new("mfio-testsuite-writes"); + seq(run.writes_equal_rel(tdir)).await; + } +} - #[test] - fn writes_equal() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let tdir = TempDir::new("mfio-testsuite-writes").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.writes_equal(&tdir)).await; - }); +#[macro_export] +macro_rules! test_suite_base { + ($test_ident:ident, $fs_builder:expr, $($(#[cfg($meta:meta)])* $test:ident),*) => { + #[cfg(test)] + #[allow(clippy::redundant_closure_call)] + mod $test_ident { + use $crate::test_suite::*; + + fn staticify(val: &mut T) -> &'static mut T { + unsafe { core::mem::transmute(val) } } - #[test] - fn writes_equal_rel() { - $fs_builder(|rt| async move { - let dir = TempDir::new("mfio-testsuite").unwrap(); - let tdir = TempDir::new("mfio-testsuite-writes").unwrap(); - let run = TestRun::new(rt, dir); - seq(run.writes_equal_rel(&tdir)).await; - }); + macro_rules! impl_test { + ($name:ident) => { + #[test] + fn $name() { + let builder: fn(&'static str, fn(TestRun<'static, _, _>) -> _) = $fs_builder; + builder("mfio-testsuite", fs_tests::$name); + } + } } + + $( + $(#[cfg($meta)])* + impl_test!($test); + )* } }; } +#[macro_export] +macro_rules! test_suite { + ($test_ident:ident, $fs_builder:expr) => { + $crate::test_suite_base!( + $test_ident, + $fs_builder, + #[cfg(not(miri))] + all_tests_seq, + #[cfg(not(miri))] + all_tests_con, + files_equal, + files_equal_rel, + dirs_equal, + walk_dirs, + writes_equal, + writes_equal_rel + ); + }; +} + +#[cfg(feature = "std")] #[macro_export] macro_rules! net_test_suite { ($test_ident:ident, $fs_builder:expr) => { #[cfg(test)] #[allow(clippy::redundant_closure_call)] mod $test_ident { + use net::*; use $crate::test_suite::*; async fn seq(i: impl Iterator + '_> + '_) { @@ -828,43 +936,3 @@ macro_rules! net_test_suite { } }; } - -#[cfg(feature = "native")] -test_suite!(tests_default, |closure| { - let _ = ::env_logger::builder().is_test(true).try_init(); - let mut rt = crate::NativeRt::default(); - let rt = staticify(&mut rt); - rt.run(closure); -}); - -#[cfg(feature = "native")] -test_suite!(tests_all, |closure| { - let _ = ::env_logger::builder().is_test(true).try_init(); - for (name, rt) in crate::NativeRt::builder().enable_all().build_each() { - println!("{name}"); - if let Ok(mut rt) = rt { - let rt = staticify(&mut rt); - rt.run(closure); - } - } -}); - -#[cfg(feature = "native")] -net_test_suite!(net_tests_default, |closure| { - let _ = ::env_logger::builder().is_test(true).try_init(); - let mut rt = crate::NativeRt::default(); - let rt = staticify(&mut rt); - rt.run(closure); -}); - -#[cfg(feature = "native")] -net_test_suite!(net_tests_all, |closure| { - let _ = ::env_logger::builder().is_test(true).try_init(); - for (name, rt) in crate::NativeRt::builder().enable_all().build_each() { - println!("{name}"); - if let Ok(mut rt) = rt { - let rt = staticify(&mut rt); - rt.run(closure); - } - } -}); diff --git a/mfio-rt/src/util.rs b/mfio-rt/src/util.rs index 5c47d9d..4e3a939 100644 --- a/mfio-rt/src/util.rs +++ b/mfio-rt/src/util.rs @@ -1,4 +1,7 @@ -use alloc::{boxed::Box, vec::Vec}; +#![cfg_attr(not(feature = "std"), allow(dead_code))] + +use crate::{Component, Path, PathBuf}; +use alloc::{boxed::Box, vec, vec::Vec}; use core::mem::MaybeUninit; use mfio::error::{Error, Location, State, Subject, INTERNAL_ERROR}; use mfio::io::*; @@ -6,6 +9,76 @@ use mfio::io::*; #[cfg(feature = "std")] pub mod stream; +/// Compute path difference +/// +/// This was taken from `pathdiff` crate, but made compatible with `typed-path` paths. +pub fn diff_paths(path: P, base: B) -> Option +where + P: AsRef, + B: AsRef, +{ + let path = path.as_ref(); + let base = base.as_ref(); + + if path.is_absolute() != base.is_absolute() { + if path.is_absolute() { + Some(PathBuf::from(path)) + } else { + None + } + } else { + let mut ita = path.components(); + let mut itb = base.components(); + let mut comps: Vec = vec![]; + loop { + match (ita.next(), itb.next()) { + (None, None) => break, + (Some(a), None) => { + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + (None, _) => comps.push(Component::ParentDir), + (Some(a), Some(b)) if comps.is_empty() && a == b => (), + (Some(a), Some(b)) if b == Component::CurDir => comps.push(a), + (Some(_), Some(b)) if b == Component::ParentDir => return None, + (Some(a), Some(_)) => { + comps.push(Component::ParentDir); + for _ in itb { + comps.push(Component::ParentDir); + } + comps.push(a); + comps.extend(ita.by_ref()); + break; + } + } + } + + Some( + comps + .iter() + .map(|c| { + #[cfg(feature = "std")] + let r = c.as_os_str(); + #[cfg(not(feature = "std"))] + let r: &[u8] = c.as_ref(); + r + }) + .collect(), + ) + } +} + +pub fn path_filename_str(path: &Path) -> Option<&str> { + let filename = path.file_name()?; + #[cfg(feature = "std")] + let filename = filename.to_str()?; + #[cfg(not(feature = "std"))] + let filename = core::str::from_utf8(filename).ok()?; + + Some(filename) +} + pub fn io_err(state: State) -> Error { Error { code: INTERNAL_ERROR, diff --git a/mfio-rt/src/virt/mod.rs b/mfio-rt/src/virt/mod.rs new file mode 100644 index 0000000..3a14f86 --- /dev/null +++ b/mfio-rt/src/virt/mod.rs @@ -0,0 +1,1215 @@ +//! Virtual Runtime +//! +//! This module implements a virtual runtime (and in-memory filesystem) that can be used as basis +//! for no_std implementations. +//! +//! Note that at the current moment this can be considered more as a toy example, rather than fully +//! featured implementation. + +use alloc::{collections::BTreeMap, string::ToString, vec, vec::Vec}; +use core::future::{pending, ready, Future, Ready}; +use core::mem::drop; +use core::ops::Bound; +use core::pin::Pin; +use core::task::{Context, Poll}; +use futures::Stream; + +use log::*; +use slab::Slab; + +use mfio::backend::{BackendContainer, BackendHandle, DynBackend, IoBackend, PollingHandle}; +use mfio::error::Result; +use mfio::io::*; +use mfio::mferr; +use mfio::stdeq::Seekable; + +use crate::{ + util::path_filename_str, Component, DirEntry, DirHandle, DirOp, FileType, Fs, Metadata, + OpenOptions, Path, PathBuf, Permissions, +}; + +#[cfg(not(feature = "virt-sync"))] +use alloc::rc::Rc; +#[cfg(feature = "virt-sync")] +use alloc::sync::Arc as Rc; + +#[cfg(feature = "virt-sync")] +use mfio::locks::RwLock; + +#[cfg(not(feature = "virt-sync"))] +use core::cell::{Ref, RefCell, RefMut}; +#[cfg(not(feature = "virt-sync"))] +struct RwLock(RefCell); + +#[cfg(not(feature = "virt-sync"))] +impl RwLock { + fn new(v: T) -> Self { + Self(RefCell::new(v)) + } + + fn read(&self) -> Ref { + self.0.borrow() + } + + fn write(&self) -> RefMut { + self.0.borrow_mut() + } +} + +type Shared = Rc>; +type InodeId = usize; + +pub struct VirtRt { + cwd: VirtDir, + backend: BackendContainer, +} + +impl Default for VirtRt { + fn default() -> Self { + Self::new() + } +} + +impl VirtRt { + pub fn new() -> Self { + Self { + cwd: VirtDir::default(), + backend: BackendContainer::new_dyn(pending()), + } + } + + pub fn build<'a>( + dirs: impl Iterator + 'a, + files: impl Iterator + 'a, + ) -> Self { + Self { + cwd: VirtDir::from_fs(VirtFs::build(dirs, files).into()), + backend: BackendContainer::new_dyn(pending()), + } + } +} + +impl IoBackend for VirtRt { + type Backend = DynBackend; + + fn polling_handle(&self) -> Option { + None + } + + fn get_backend(&self) -> BackendHandle { + self.backend.acquire(None) + } +} + +impl Fs for VirtRt { + type DirHandle<'a> = VirtDir; + + fn current_dir(&self) -> &Self::DirHandle<'_> { + &self.cwd + } +} + +pub struct VirtDir { + inode: Shared, + fs: Rc, +} + +impl Default for VirtDir { + fn default() -> Self { + Self::from_fs(Rc::new(VirtFs::default())) + } +} + +impl VirtDir { + fn from_fs(fs: Rc) -> Self { + Self { + inode: fs.root_dir.clone(), + fs, + } + } + + // Internal sync implementations of directory-relative fs operations + + fn get_path(&self) -> Result { + Inode::path(self.inode.clone(), &self.fs.inodes.read()) + .ok_or_else(|| mferr!(Path, Invalid, Filesystem)) + } + + fn do_open_file( + &self, + path: &Path, + options: OpenOptions, + ) -> Result<::FileHandle> { + path.parent() + .and_then(|path| { + let inodes = self.fs.inodes.read(); + Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, path, &inodes) + }) + // TODO: do we want to filter here, since we are returning None later anyways. The only + // difference here is that we are doing so with read-only lock. + .filter(|dir| matches!(dir.read().entry, InodeData::Dir(_))) + .ok_or(mferr!(Directory, NotFound, Filesystem)) + .and_then(|dir| { + let mut dir_guard = dir.write(); + let dir_guard = &mut *dir_guard; + + let InodeData::Dir(dir_entry) = &mut dir_guard.entry else { + unreachable!(); + }; + + let Some(parent_id) = dir_guard.id else { + return Err(mferr!(Path, Removed, Filesystem)); + }; + + let filename = path_filename_str(path).ok_or(mferr!(Path, Invalid, Filesystem))?; + + // creating new file may get triggered from 2 branches + let new_file = |entries: &mut BTreeMap<_, _>| { + let mut inodes = self.fs.inodes.write(); + let entry = inodes.vacant_entry(); + let id = entry.key(); + + let name: Rc = filename.into(); + + let inode = entry + .insert(Rc::new(RwLock::new(Inode { + id: Some(id), + parent_link: Some(ParentLink { + name: name.clone(), + parent: parent_id, + }), + entry: InodeData::File(VirtFileInner { data: vec![] }), + metadata: Metadata::empty_file(Permissions {}, None), + }))) + .clone(); + + entries.insert(name.clone(), id); + + Ok(VirtFile { inode, options }) + }; + + // Verify open options + if options.create_new { + if options.write && !dir_entry.entries.contains_key(filename) { + new_file(&mut dir_entry.entries) + } else { + Err(mferr!(File, AlreadyExists, Filesystem)) + } + } else if let Some(&id) = dir_entry.entries.get(filename) { + let inodes = self.fs.inodes.read(); + // Different from file not found, because it's the whole inode that's gone, and + // this is not expected. + let inode = inodes + .get(id) + .ok_or(mferr!(Entry, NotFound, Filesystem))? + .clone(); + + // Verify that we've got a file in both of the branches, except we are holding + // different types of locks (read vs write). + if options.truncate { + if options.write { + let mut inode = inode.write(); + let InodeData::File(file) = &mut inode.entry else { + return Err(mferr!(Entry, Invalid, Filesystem)); + }; + file.data.clear(); + inode.metadata.len = 0; + } else { + return Err(mferr!(Argument, Unsupported, Filesystem)); + } + } else if !matches!(inode.read().entry, InodeData::File(_)) { + return Err(mferr!(Entry, Invalid, Filesystem)); + } + + Ok(VirtFile { inode, options }) + } else if options.create && options.write { + new_file(&mut dir_entry.entries) + } else { + Err(mferr!(File, NotFound, Filesystem)) + } + }) + .map(Seekable::from) + } + + fn set_permissions(&self, path: &Path, permissions: Permissions) -> Result<()> { + let inodes = self.fs.inodes.read(); + let node = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, path, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + let mut node = node.write(); + // TODO: check whether the caller is authorized + node.metadata.permissions = permissions; + // TODO: change mtime + Ok(()) + } + + /// Unlink from given parent + /// + /// # Panics + /// + /// This function panics if `parent` is not the parent of `node`. + fn unlink_with_parent(node: &mut Inode, parent_node: &mut Inode) { + if let Some(ParentLink { name, parent }) = node.parent_link.take() { + assert_eq!(parent, parent_node.id.unwrap()); + let InodeData::Dir(parent) = &mut parent_node.entry else { + panic!("Parent changed from dir to file") + }; + parent.entries.remove(&name); + } + } + + fn unlink(node: &mut Inode, inodes: &mut Slab>) { + if let Some(ParentLink { parent, .. }) = &node.parent_link { + let parent = inodes + .get(*parent) + .expect("No parent inode found. This indicates buggy unlinking") + .clone(); + let mut parent = parent.write(); + Self::unlink_with_parent(node, &mut parent) + } + } + + fn remove_dir(&self, path: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + let node = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, path, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + let mut node = node.write(); + + let InodeData::Dir(dir) = &node.entry else { + return Err(mferr!(Entry, Invalid, Filesystem)); + }; + + if !dir.entries.is_empty() { + return Err(mferr!(Directory, InUse, Filesystem)); + } + + if let Some(id) = node.id.take() { + inodes.remove(id); + } + + Self::unlink(&mut node, &mut inodes); + + Ok(()) + } + + fn remove_dir_all(&self, path: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + let node = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, path, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + let mut stack = vec![node]; + + while let Some(head) = stack.pop() { + let mut node = head.write(); + + let InodeData::Dir(dir) = &mut node.entry else { + return Err(mferr!(Entry, Invalid, Filesystem)); + }; + + while let Some((_, id)) = dir.entries.pop_last() { + let inode = inodes.get(id).expect("entry exists, with no inode").clone(); + let mut inode_guard = inode.write(); + match inode_guard.entry { + InodeData::File(_) => { + inodes.remove(inode_guard.id.take().expect("parent does not exist")); + } + InodeData::Dir(_) => { + drop(inode_guard); + stack.push(head.clone()); + stack.push(inode); + break; + } + } + } + + if dir.entries.is_empty() { + if let Some(id) = node.id.take() { + inodes.remove(id); + } + + Self::unlink(&mut node, &mut inodes); + } + } + + Ok(()) + } + + fn create_dir(&self, path: &Path) -> Result<()> { + let filename = path_filename_str(path).ok_or(mferr!(Path, Invalid, Filesystem))?; + + let mut inodes = self.fs.inodes.write(); + let node = Inode::walk_rel( + self.inode.clone(), + &self.fs.root_dir, + path.parent().ok_or(mferr!(Path, NotFound, Filesystem))?, + &inodes, + ) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + let mut node = node.write(); + + let Some(parent) = node.id else { + return Err(mferr!(Path, Removed, Filesystem)); + }; + + let InodeData::Dir(parent_dir) = &mut node.entry else { + return Err(mferr!(Path, NotFound, Filesystem)); + }; + + if parent_dir.entries.contains_key(filename) { + Err(mferr!(Directory, AlreadyExists, Filesystem)) + } else { + let filename: Rc = Rc::from(filename); + + let entry = inodes.vacant_entry(); + let id = entry.key(); + entry.insert(Rc::new(RwLock::new(Inode { + id: Some(id), + metadata: Metadata::empty_dir(Permissions {}, None), + entry: InodeData::Dir(VirtDirInner { + entries: Default::default(), + }), + parent_link: Some(ParentLink { + name: filename.clone(), + parent, + }), + }))); + + parent_dir.entries.insert(filename, id); + + Ok(()) + } + } + + fn create_dir_all(&self, path: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + let mut cur_node = self.inode.clone(); + + let components = path.components(); + + for component in components { + let mut cur_inode = cur_node.write(); + + let Some(parent) = cur_inode.id else { + return Err(mferr!(Path, Removed, Filesystem)); + }; + + let next_node = match component { + Component::RootDir => self.fs.root_dir.clone(), + Component::ParentDir => { + // If we are at the root, then going up means cycling back. + if let Some(link) = &cur_inode.parent_link { + inodes + .get(link.parent) + .ok_or(mferr!(Path, Invalid, Filesystem))? + .clone() + } else { + continue; + } + } + Component::Normal(n) => { + #[cfg(feature = "std")] + let n = n.to_str().ok_or(mferr!(Path, Invalid, Filesystem))?; + #[cfg(not(feature = "std"))] + let n = + core::str::from_utf8(n).map_err(|_| mferr!(Path, Invalid, Filesystem))?; + + let InodeData::Dir(d) = &mut cur_inode.entry else { + return Err(mferr!(Path, Invalid, Filesystem)); + }; + let inode = d.entries.get(n); + + if let Some(&inode) = inode { + inodes + .get(inode) + .ok_or(mferr!(Path, Invalid, Filesystem))? + .clone() + } else { + let entry = inodes.vacant_entry(); + let id = entry.key(); + let name: Rc = Rc::from(n); + d.entries.insert(name.clone(), id); + let entry = entry + .insert(Rc::new(RwLock::new(Inode { + id: Some(id), + parent_link: Some(ParentLink { parent, name }), + entry: InodeData::Dir(VirtDirInner { + entries: Default::default(), + }), + metadata: Metadata::empty_dir(Permissions {}, None), + }))) + .clone(); + entry + } + } + _ => continue, + }; + + drop(cur_inode); + cur_node = next_node; + } + + Ok(()) + } + + fn remove_file(&self, path: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + let node = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, path, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + let mut node = node.write(); + + let InodeData::File(_) = &node.entry else { + return Err(mferr!(Entry, Invalid, Filesystem)); + }; + + if let Some(id) = node.id.take() { + inodes.remove(id); + } + + Self::unlink(&mut node, &mut inodes); + + Ok(()) + } + + fn rename(&self, from: &Path, to: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + + let from = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, from, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + let to_parent = to.parent().ok_or(mferr!(Path, Unavailable, Filesystem))?; + let to_filename = to.file_name().ok_or(mferr!(Path, Invalid, Filesystem))?; + #[cfg(feature = "std")] + let to_filename = to_filename + .to_str() + .ok_or(mferr!(Path, Invalid, Filesystem))?; + #[cfg(not(feature = "std"))] + let to_filename = + core::str::from_utf8(to_filename).map_err(|_| mferr!(Path, Invalid, Filesystem))?; + + let to_parent = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, to_parent, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + // We need to make sure that `to_parent` is not child of `from`. + // I wonder, what if we didn't check for this? We could create "hidden islands" of sorts. + { + let mut tmp = from.clone(); + while let Some(node) = + Inode::walk_rel(tmp, &self.fs.root_dir, Path::new("../"), &inodes) + { + if Rc::as_ptr(&node) == Rc::as_ptr(&to_parent) { + return Err(mferr!(Path, Unavailable, Filesystem)); + } else if Rc::as_ptr(&node) == Rc::as_ptr(&self.fs.root_dir) { + break; + } else { + tmp = node; + } + } + } + + let mut to_parent = to_parent.write(); + let InodeData::Dir(to_parent_dir) = &mut to_parent.entry else { + return Err(mferr!(Path, Invalid, Filesystem)); + }; + + let mut from = from.write(); + + // Remove old entry, if we can + // This way we can also reuse the old arc for the name, without performing a new allocation + let to_filename = + if let Some((name, entry)) = to_parent_dir.entries.get_key_value(to_filename) { + let entry = inodes + .get(*entry) + .ok_or(mferr!(Path, Unavailable, Filesystem))? + .clone(); + let mut entry = entry.write(); + if let InodeData::Dir(v) = &entry.entry { + if !v.entries.is_empty() || matches!(from.entry, InodeData::File(_)) { + return Err(mferr!(Directory, InUse, Filesystem)); + } + } + let name = name.clone(); + Self::unlink_with_parent(&mut entry, &mut to_parent); + name + } else { + Rc::from(to_filename) + }; + + let InodeData::Dir(to_parent_dir) = &mut to_parent.entry else { + unreachable!() + }; + + Self::unlink(&mut from, &mut inodes); + + // Finally, relink to the parent + to_parent_dir.entries.insert( + to_filename.clone(), + from.id + .expect("moving a file with no inode, this shouldn't happen"), + ); + + from.parent_link = Some(ParentLink { + name: to_filename, + parent: to_parent + .id + .expect("parent dir with no inode, this shouldn't happen"), + }); + + Ok(()) + } + + fn copy(&self, from: &Path, to: &Path) -> Result<()> { + let mut inodes = self.fs.inodes.write(); + + let from = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, from, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + let to_parent = to.parent().ok_or(mferr!(Path, Unavailable, Filesystem))?; + let to_filename = to.file_name().ok_or(mferr!(Path, Invalid, Filesystem))?; + #[cfg(feature = "std")] + let to_filename = to_filename + .to_str() + .ok_or(mferr!(Path, Invalid, Filesystem))?; + #[cfg(not(feature = "std"))] + let to_filename = + core::str::from_utf8(to_filename).map_err(|_| mferr!(Path, Invalid, Filesystem))?; + + let to_parent = Inode::walk_rel(self.inode.clone(), &self.fs.root_dir, to_parent, &inodes) + .ok_or(mferr!(Path, NotFound, Filesystem))?; + + let mut to_parent = to_parent.write(); + let InodeData::Dir(to_parent_dir) = &mut to_parent.entry else { + return Err(mferr!(Path, Invalid, Filesystem)); + }; + + let mut from = from.write(); + let InodeData::File(from_file) = &mut from.entry else { + return Err(mferr!(Path, Invalid, Filesystem)); + }; + + // Remove old entry, if we can + // This way we can also reuse the old arc for the name, without performing a new allocation + let to_filename = + if let Some((name, entry)) = to_parent_dir.entries.get_key_value(to_filename) { + let entry = inodes + .get(*entry) + .ok_or(mferr!(Path, Unavailable, Filesystem))? + .clone(); + let mut entry = entry.write(); + if let InodeData::Dir(v) = &entry.entry { + if !v.entries.is_empty() { + return Err(mferr!(Directory, InUse, Filesystem)); + } + } + let name = name.clone(); + Self::unlink_with_parent(&mut entry, &mut to_parent); + name + } else { + Rc::from(to_filename) + }; + + let Some(parent) = to_parent.id else { + return Err(mferr!(Path, Removed, Filesystem)); + }; + + let InodeData::Dir(to_parent_dir) = &mut to_parent.entry else { + unreachable!() + }; + + let new_entry = inodes.vacant_entry(); + let id = new_entry.key(); + new_entry.insert(Rc::new(RwLock::new(Inode { + entry: InodeData::File(VirtFileInner { + data: from_file.data.clone(), + }), + id: Some(id), + parent_link: Some(ParentLink { + name: to_filename.clone(), + parent, + }), + // TODO: we need to update time here (when we have access to time). + metadata: from.metadata.clone(), + }))); + + // Finally, link up to the parent + to_parent_dir.entries.insert(to_filename.clone(), id); + + Ok(()) + } +} + +impl DirHandle for VirtDir { + type FileHandle = Seekable; + type OpenFileFuture<'a> = OpenFileFuture<'a>; + type PathFuture<'a> = Ready>; + type OpenDirFuture<'a> = Ready>; + type ReadDir<'a> = ReadDir<'a>; + type ReadDirFuture<'a> = Ready>>; + type MetadataFuture<'a> = Ready>; + type OpFuture<'a> = OpFuture<'a>; + + /// Gets the absolute path to this `DirHandle`. + fn path(&self) -> Self::PathFuture<'_> { + ready(self.get_path()) + } + + /// Reads the directory contents. + /// + /// Iterating directories has the complexity of `O(n log(n))`, because we are not maintaining + /// the cursor to the directory. + /// + /// TODO: do not go back for entries after every iteration, instead, cache up a few entries. + fn read_dir(&self) -> Self::ReadDirFuture<'_> { + ready(Ok(ReadDir { + dir: self, + cur: None, + })) + } + + /// Opens a file. + /// + /// This function accepts an absolute or relative path to a file for reading. If the path is + /// relative, it is opened relative to this `DirHandle`. + fn open_file<'a, P: AsRef + ?Sized>( + &'a self, + path: &'a P, + options: OpenOptions, + ) -> Self::OpenFileFuture<'a> { + OpenFileFuture { + dir: self, + path: path.as_ref(), + options, + } + } + + /// Opens a directory. + /// + /// This function accepts an absolute or relative path to a directory for reading. If the path + /// is relative, it is opened relative to this `DirHandle`. + fn open_dir<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::OpenDirFuture<'a> { + // We do not write anything here, therefore + let inode = { + let inodes = self.fs.inodes.read(); + Inode::walk_rel( + self.inode.clone(), + &self.fs.root_dir, + path.as_ref(), + &inodes, + ) + }; + + let ret = inode + // TODO: do we want to filter here, since we are returning None later anyways. The only + // difference here is that we are doing so with read-only lock. + .filter(|dir| matches!(dir.read().entry, InodeData::Dir(_))) + .ok_or(mferr!(Directory, NotFound, Filesystem)) + .map(|inode| VirtDir { + inode, + fs: self.fs.clone(), + }); + + ready(ret) + } + + fn metadata<'a, P: AsRef + ?Sized>(&'a self, path: &'a P) -> Self::MetadataFuture<'a> { + let inodes = self.fs.inodes.read(); + + let ret = Inode::walk_rel( + self.inode.clone(), + &self.fs.root_dir, + path.as_ref(), + &inodes, + ) + .map(|v| v.read().metadata.clone()) + .ok_or(mferr!(Entry, NotFound, Filesystem)); + + ready(ret) + } + + /// Do an operation. + /// + /// This function performs an operation from the [`DirOp`] enum. + fn do_op<'a, P: AsRef + ?Sized>(&'a self, operation: DirOp<&'a P>) -> Self::OpFuture<'a> { + OpFuture { + dir: self, + operation: Some(operation.into_path()), + } + } +} + +// VirtDir's DirHandle futures: +// TODO: Once impl_trait_in_assoc_type is stabilized (https://github.com/rust-lang/rust/issues/63063), +// we can remove these in favor of async fns in traits. + +pub struct OpFuture<'a> { + dir: &'a VirtDir, + operation: Option>, +} + +impl Future for OpFuture<'_> { + type Output = Result<()>; + + fn poll(mut self: Pin<&mut Self>, _: &mut Context) -> Poll { + //let this = unsafe { self.get_unchecked_mut() } + //Poll::Ready(self.dir.do_open_file(self.path, self.options)) + let ret = match self.operation.take().unwrap() { + DirOp::SetPermissions { path, permissions } => { + self.dir.set_permissions(path, permissions) + } + DirOp::RemoveDir { path } => self.dir.remove_dir(path), + DirOp::RemoveDirAll { path } => self.dir.remove_dir_all(path), + DirOp::CreateDir { path } => self.dir.create_dir(path), + DirOp::CreateDirAll { path } => self.dir.create_dir_all(path), + DirOp::RemoveFile { path } => self.dir.remove_file(path), + DirOp::Rename { from, to } => self.dir.rename(from, to), + DirOp::Copy { from, to } => self.dir.copy(from, to), + DirOp::HardLink { .. } => Err(mferr!(Operation, NotImplemented, Filesystem)), + }; + + Poll::Ready(ret) + } +} + +pub struct OpenFileFuture<'a> { + dir: &'a VirtDir, + path: &'a Path, + options: OpenOptions, +} + +impl Future for OpenFileFuture<'_> { + type Output = Result<::FileHandle>; + + fn poll(self: Pin<&mut Self>, _: &mut Context) -> Poll { + Poll::Ready(self.dir.do_open_file(self.path, self.options)) + } +} + +pub struct ReadDir<'a> { + dir: &'a VirtDir, + cur: Option>, +} + +impl Stream for ReadDir<'_> { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, _: &mut Context) -> Poll> { + let this = unsafe { self.get_unchecked_mut() }; + + let dir = this.dir.inode.read(); + + let InodeData::Dir(dir) = &dir.entry else { + return Poll::Ready(None); + }; + + let next = if let Some(cur) = this.cur.clone() { + dir.entries + .range((Bound::Excluded(cur), Bound::Unbounded)) + .next() + } else { + dir.entries.iter().next() + }; + + Poll::Ready(next.map(|(k, v)| { + this.cur = Some(k.clone()); + this.dir + .fs + .inodes + .read() + .get(*v) + .map(|v| v.read()) + .as_ref() + .map(|v| v.entry.to_dir_entry(k.clone())) + .ok_or(mferr!(Entry, Corrupted, Filesystem)) + })) + } +} + +// Core filesystem container: + +pub struct VirtFs { + inodes: RwLock>>, + root_dir: Shared, +} + +impl VirtFs { + /// Build a filesystem from directory and file list. + /// + /// Builds a filesystem instance from given data. This is generally more efficient than + /// manually creating all entries (without caching), because the build step is able to directly + /// access all inodes. + pub fn build<'a>( + dirs: impl Iterator + 'a, + files: impl Iterator + 'a, + ) -> Self { + let mut inode_cache = BTreeMap::new(); + + let mut inodes = Slab::new(); + + let entry = inodes.vacant_entry(); + let id = entry.key(); + + let root_dir = Rc::new(RwLock::new(Inode { + id: Some(id), + parent_link: None, + entry: InodeData::Dir(VirtDirInner { + entries: Default::default(), + }), + metadata: Metadata::empty_dir(Permissions {}, None), + })); + + entry.insert(root_dir.clone()); + + let insert_entry = |inodes: &mut Slab<_>, parent_id, p: &str, entry, metadata| { + let new_entry = inodes.vacant_entry(); + let id = new_entry.key(); + + let name: Rc = p.into(); + + new_entry.insert(Rc::new(RwLock::new(Inode { + id: Some(id), + parent_link: Some(ParentLink { + parent: parent_id, + name: name.clone(), + }), + entry, + metadata, + }))); + + let parent = inodes.get(parent_id).unwrap(); + let mut parent = parent.write(); + let InodeData::Dir(d) = &mut parent.entry else { + unreachable!() + }; + d.entries.insert(name, id); + + id + }; + + for dir in dirs { + let mut d = String::new(); + // Root inode ID + let mut parent_id = id; + for p in dir.split('/') { + if !d.is_empty() { + d.push('/'); + } + d.push_str(p); + + parent_id = *inode_cache.entry(d.clone()).or_insert_with(|| { + insert_entry( + &mut inodes, + parent_id, + p, + InodeData::Dir(VirtDirInner::default()), + Metadata::empty_dir(Permissions {}, None), + ) + }); + } + } + + for (path, data) in files { + let mut d = String::new(); + // Root inode ID + let mut parent_id = id; + for p in path.split('/') { + if !d.is_empty() { + d.push('/'); + } + d.push_str(p); + + parent_id = *inode_cache.entry(d.clone()).or_insert_with(|| { + insert_entry( + &mut inodes, + parent_id, + p, + InodeData::Dir(VirtDirInner::default()), + Metadata::empty_dir(Permissions {}, None), + ) + }); + } + + let inode = inode_cache.get(path).unwrap(); + let inode = inodes.get(*inode).unwrap(); + let mut inode = inode.write(); + + { + let InodeData::Dir(v) = &inode.entry else { + unreachable!() + }; + assert!(v.entries.is_empty()); + } + + inode.metadata.len = data.len() as _; + inode.entry = InodeData::File(VirtFileInner { data: data.into() }); + } + + let inodes = RwLock::new(inodes); + + Self { inodes, root_dir } + } +} + +impl Default for VirtFs { + fn default() -> Self { + let mut inodes = Slab::new(); + + let entry = inodes.vacant_entry(); + let id = entry.key(); + + let root_dir = Rc::new(RwLock::new(Inode { + id: Some(id), + parent_link: None, + entry: InodeData::Dir(VirtDirInner { + entries: Default::default(), + }), + metadata: Metadata::empty_dir(Permissions {}, None), + })); + + entry.insert(root_dir.clone()); + + let inodes = RwLock::new(inodes); + + Self { inodes, root_dir } + } +} + +// File implementation: + +pub struct VirtFile { + inode: Shared, + options: OpenOptions, +} + +impl PacketIo for VirtFile { + fn send_io(&self, pos: u64, pkt: BoundPacketView) { + if self.options.read { + let node = self.inode.read(); + + let InodeData::File(file) = &node.entry else { + core::mem::drop(node); + pkt.error(mferr!(File, Unreadable, Filesystem)); + return; + }; + + if pos < file.data.len() as u64 { + let split_pos = (file.data.len() as u64).saturating_sub(pos); + if split_pos < pkt.len() { + let (a, b) = pkt.split_at(split_pos); + + let transferred = + unsafe { a.transfer_data(file.data[(pos as usize)..].as_ptr().cast()) }; + + core::mem::drop(node); + core::mem::drop(transferred); + b.error(mferr!(Position, Outside, Filesystem)); + } else { + let transferred = + unsafe { pkt.transfer_data(file.data[(pos as usize)..].as_ptr().cast()) }; + core::mem::drop(node); + core::mem::drop(transferred); + } + } else { + core::mem::drop(node); + pkt.error(mferr!(Position, Outside, Filesystem)); + } + } else { + pkt.error(mferr!(Io, PermissionDenied, Filesystem)); + } + } +} + +impl PacketIo for VirtFile { + fn send_io(&self, pos: u64, pkt: BoundPacketView) { + if self.options.write { + let mut node = self.inode.write(); + + let InodeData::File(file) = &mut node.entry else { + core::mem::drop(node); + pkt.error(mferr!(File, Unreadable, Filesystem)); + return; + }; + + // TODO: we may want to have sparse files + let needed_len = (pos + pkt.len()) as usize; + if needed_len > file.data.len() { + file.data.resize(needed_len, 0); + } + + let transferred = + unsafe { pkt.transfer_data(file.data[(pos as usize)..].as_mut_ptr().cast()) }; + + core::mem::drop(node); + core::mem::drop(transferred); + } else { + pkt.error(mferr!(Io, PermissionDenied, Filesystem)); + } + } +} + +// Internal data structures: + +struct Inode { + // handles to this Inode's arc. Therefore, we need to signify somehow that this inode was + // already deleted, and that it cannot be relinked anywhere. + id: Option, + parent_link: Option, + entry: InodeData, + metadata: Metadata, +} + +impl Inode { + /// Walks the filesystem to reach a specified directory. + /// + /// Returns `None` if the resulting path, or given inode are not directories. + fn walk_rel( + mut cur_node: Shared, + root_dir: &Shared, + path: &Path, + inodes: &Slab>, + ) -> Option> { + trace!("Walk rel: {path:?}"); + + let components = path.components(); + + for component in components { + let inode = cur_node.read(); + + let next_node = match component { + Component::RootDir => root_dir.clone(), + Component::ParentDir => { + // If we are at the root, then going up means cycling back. + if let Some(link) = &inode.parent_link { + inodes.get(link.parent)?.clone() + } else { + continue; + } + } + Component::Normal(n) => { + #[cfg(feature = "std")] + let n = n.to_str()?; + #[cfg(not(feature = "std"))] + let n = core::str::from_utf8(n).ok()?; + + let InodeData::Dir(d) = &inode.entry else { + return None; + }; + + trace!("Entries: {:?}", d.entries); + + let inode = *d.entries.get(n)?; + + inodes.get(inode)?.clone() + } + _ => continue, + }; + + drop(inode); + cur_node = next_node; + } + + Some(cur_node) + } + + fn path(mut cur_node: Shared, inodes: &Slab>) -> Option { + let mut segments = vec![]; + + loop { + let cn = cur_node.read(); + + if let Some(link) = &cn.parent_link { + segments.push(link.name.clone()); + let parent = inodes.get(link.parent)?.clone(); + drop(cn); + cur_node = parent; + } else { + break; + } + } + + let mut res = PathBuf::new(); + + while let Some(seg) = segments.pop() { + res.push(&*seg); + } + + Some(res) + } +} + +#[derive(Clone)] +struct ParentLink { + name: Rc, + parent: InodeId, +} + +enum InodeData { + File(VirtFileInner), + Dir(VirtDirInner), +} + +impl InodeData { + fn to_dir_entry(&self, name: Rc) -> DirEntry { + DirEntry { + name: name.to_string(), + ty: match self { + Self::File(_) => FileType::File, + Self::Dir(_) => FileType::Directory, + }, + } + } +} + +#[derive(Default)] +struct VirtDirInner { + entries: BTreeMap, InodeId>, +} + +struct VirtFileInner { + // TODO: support sparse files + data: Vec, +} +#[cfg(not(miri))] +macro_rules! test_suite { + ($($tt:tt)*) => { + #[cfg(test)] + $crate::test_suite!($($tt)*); + } +} + +// Walking directories is currently extremely slow. So let's not do O(n^2) tests for now. +#[cfg(miri)] +macro_rules! test_suite { + ($($tt:tt)*) => { + #[cfg(test)] + $crate::test_suite_base!( + $($tt)*, + files_equal, + files_equal_rel, + dirs_equal, + writes_equal, + writes_equal_rel + ); + } +} + +test_suite!(tests_default, |_, closure| { + use super::VirtRt; + #[cfg(not(miri))] + let _ = ::env_logger::builder().is_test(true).try_init(); + let mut rt = VirtRt::build( + CTX.dirs().iter().map(|v| v.as_str()), + CTX.files().iter().map(|(n, d)| (n.as_str(), &d[..])), + ); + let rt = staticify(&mut rt); + + pub fn run<'a, Func: FnOnce(&'a VirtRt) -> F, F: Future>( + fs: &'a mut VirtRt, + func: Func, + ) -> F::Output { + fs.block_on(func(fs)) + } + + run(rt, |rt| async move { + let run = TestRun::assume_built(rt, ()); + closure(run).await + }); +}); diff --git a/mfio/src/error.rs b/mfio/src/error.rs index d612ca4..1db514b 100644 --- a/mfio/src/error.rs +++ b/mfio/src/error.rs @@ -260,6 +260,8 @@ ienum! { Import, Section, Backend, + Entry, + Operation, Other, } } @@ -296,6 +298,8 @@ ienum! { Nop, UnexpectedEof, InUse, + Corrupted, + Removed, Other, } } diff --git a/mfio/src/io/packet/mod.rs b/mfio/src/io/packet/mod.rs index e1906bf..94d7e60 100644 --- a/mfio/src/io/packet/mod.rs +++ b/mfio/src/io/packet/mod.rs @@ -1061,6 +1061,7 @@ unsafe impl OpaqueStore for FullPacket where Self: 'a, { + #[allow(clippy::useless_conversion)] self.into() } diff --git a/mfio/src/lib.rs b/mfio/src/lib.rs index 2ca77ea..7fd08c6 100644 --- a/mfio/src/lib.rs +++ b/mfio/src/lib.rs @@ -163,6 +163,7 @@ extern crate alloc; +#[allow(unused_imports)] pub(crate) mod std_prelude { #[cfg(not(feature = "std"))] pub use ::alloc::{boxed::Box, vec, vec::Vec};