diff --git a/Cargo.lock b/Cargo.lock index d0a3ebe..e0f5504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3979,6 +3979,21 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtualfs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "cfg-if", + "chrono", + "futures", + "getset", + "pretty-error-debug", + "thiserror 2.0.6", + "tokio", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 16b02c9..07c30be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "ipldstore", "cryptdag", "did-wk", + "virtualfs", ] resolver = "2" @@ -66,3 +67,4 @@ serde_ipld_dagcbor = "0.6" sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] } regex = "1.10" async-recursion = "1.1" +cfg-if = "1.0" diff --git a/monocore/lib/error.rs b/monocore/lib/error.rs index dd68441..e2d572f 100644 --- a/monocore/lib/error.rs +++ b/monocore/lib/error.rs @@ -1,6 +1,6 @@ +use ipldstore::{ipld, StoreError}; use monofs::FsError; use monoutils::MonoutilsError; -use ipldstore::{ipld, StoreError}; use nix::errno::Errno; use sqlx::migrate::MigrateError; use std::{ diff --git a/monocore/lib/management/rootfs.rs b/monocore/lib/management/rootfs.rs index 1b303ec..9ca2d3b 100644 --- a/monocore/lib/management/rootfs.rs +++ b/monocore/lib/management/rootfs.rs @@ -1,16 +1,19 @@ use async_recursion::async_recursion; use chrono::{TimeZone, Utc}; -use ipldstore::ipld::ipld::Ipld; -use ipldstore::{ipld::cid::Cid, IpldStore}; +use ipldstore::{ + ipld::{cid::Cid, ipld::Ipld}, + IpldStore, +}; use monofs::filesystem::{ Dir, Entity, File, Metadata, SymPathLink, UNIX_ATIME_KEY, UNIX_GID_KEY, UNIX_MODE_KEY, UNIX_MTIME_KEY, UNIX_UID_KEY, }; -use std::fs; -use std::os::unix::fs::{MetadataExt, PermissionsExt}; -use std::path::Path; -use tokio::fs::File as TokioFile; -use tokio::io::BufReader; +use std::{ + fs, + os::unix::fs::{MetadataExt, PermissionsExt}, + path::Path, +}; +use tokio::{fs::File as TokioFile, io::BufReader}; use crate::MonocoreResult; diff --git a/monofs/examples/file.rs b/monofs/examples/file.rs index aadf9fa..d7c7db1 100644 --- a/monofs/examples/file.rs +++ b/monofs/examples/file.rs @@ -21,8 +21,8 @@ //! cargo run --example file //! ``` -use monofs::filesystem::File; use ipldstore::{MemoryStore, Storable}; +use monofs::filesystem::File; use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader}; //-------------------------------------------------------------------------------------------------- @@ -49,7 +49,6 @@ async fn main() -> anyhow::Result<()> { drop(output_stream); println!("Wrote content to file"); - // Read content from the file let input_stream = file.get_input_stream().await?; let mut buffer = Vec::new(); diff --git a/monofs/lib/error.rs b/monofs/lib/error.rs index 171bbe5..5653cb2 100644 --- a/monofs/lib/error.rs +++ b/monofs/lib/error.rs @@ -46,7 +46,7 @@ pub enum FsError { PathNotFound(String), /// Custom error. - #[error("Custom error: {0}")] + #[error(transparent)] Custom(#[from] AnyError), // /// DID related error. diff --git a/monofs/lib/filesystem/dir/segment.rs b/monofs/lib/filesystem/dir/segment.rs index f24408b..18b3a8e 100644 --- a/monofs/lib/filesystem/dir/segment.rs +++ b/monofs/lib/filesystem/dir/segment.rs @@ -125,6 +125,11 @@ impl TryFrom<&str> for Utf8UnixPathSegment { return Err(FsError::InvalidPathComponent(value.to_string())); } + // Reject input string if it contains the '/' separator + if value.contains('/') { + return Err(FsError::InvalidPathComponent(value.to_string())); + } + let component = Utf8UnixComponent::try_from(value) .map_err(|_| FsError::InvalidPathComponent(value.to_string()))?; diff --git a/monofs/lib/server/server.rs b/monofs/lib/server/server.rs index f21ec85..95d43a6 100644 --- a/monofs/lib/server/server.rs +++ b/monofs/lib/server/server.rs @@ -6,6 +6,10 @@ use crate::store::FlatFsStore; use super::MonofsNFS; +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + /// A server that provides NFS access to a content-addressed store. /// This server uses a flat filesystem store as its backing store. #[derive(Debug, Getters)] @@ -21,6 +25,10 @@ pub struct MonofsServer { port: u32, } +//-------------------------------------------------------------------------------------------------- +// Methods +//-------------------------------------------------------------------------------------------------- + impl MonofsServer { /// Creates a new MonofsServer with the given store path and host:port. pub fn new(store_dir: impl Into, host: impl Into, port: u32) -> Self { @@ -45,23 +53,3 @@ impl MonofsServer { Ok(()) } } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - #[tokio::test] - async fn test_monofsserver_creation() { - let temp_dir = TempDir::new().unwrap(); - let server = MonofsServer::new( - temp_dir.path().to_path_buf(), - "127.0.0.1", - 0, // Use port 0 for testing - ); - - assert_eq!(server.store_dir, temp_dir.path()); - assert_eq!(server.host, "127.0.0.1"); - assert_eq!(server.port, 0); - } -} diff --git a/virtualfs/Cargo.toml b/virtualfs/Cargo.toml new file mode 100644 index 0000000..293c27c --- /dev/null +++ b/virtualfs/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "virtualfs" +version = "0.1.0" +description = "`virtualfs` is a library for virtual file systems." +authors.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true + +[lib] +name = "virtualfs" +path = "lib/lib.rs" + +[dependencies] +async-trait.workspace = true +pretty-error-debug.workspace = true +thiserror.workspace = true +anyhow.workspace = true +tokio.workspace = true +futures.workspace = true +chrono.workspace = true +getset.workspace = true +cfg-if.workspace = true diff --git a/virtualfs/lib/error.rs b/virtualfs/lib/error.rs new file mode 100644 index 0000000..a2b3c3b --- /dev/null +++ b/virtualfs/lib/error.rs @@ -0,0 +1,138 @@ +use std::{ + error::Error, + fmt::{self, Display}, + io, + path::PathBuf, +}; + +use thiserror::Error; + +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// The result of a file system operation. +pub type VfsResult = Result; + +/// An error that occurred during a file system operation. +#[derive(pretty_error_debug::Debug, Error)] +pub enum VfsError { + /// The parent directory does not exist + #[error("parent directory does not exist: {0}")] + ParentDirectoryNotFound(PathBuf), + + /// The path already exists + #[error("path already exists: {0}")] + AlreadyExists(PathBuf), + + /// The path does not exist + #[error("path does not exist: {0}")] + NotFound(PathBuf), + + /// The path is not a directory + #[error("path is not a directory: {0}")] + NotADirectory(PathBuf), + + /// The path is not a file + #[error("path is not a file: {0}")] + NotAFile(PathBuf), + + /// The path is not a symlink + #[error("path is not a symlink: {0}")] + NotASymlink(PathBuf), + + /// Invalid offset for read/write operation + #[error("invalid offset {offset} for path: {path}")] + InvalidOffset { + /// The path of the file + path: PathBuf, + + /// The offset that is invalid + offset: u64, + }, + + /// Insufficient permissions to perform the operation + #[error("insufficient permissions for operation on: {0}")] + PermissionDenied(PathBuf), + + /// The filesystem is read-only + #[error("filesystem is read-only")] + ReadOnlyFilesystem, + + /// Invalid symlink target + #[error("invalid symlink target: {0}")] + InvalidSymlinkTarget(PathBuf), + + /// Empty path segment + #[error("empty path segment")] + EmptyPathSegment, + + /// Invalid path component (e.g. ".", "..", "/") + #[error("invalid path component: {0}")] + InvalidPathComponent(String), + + /// IO error during filesystem operation + #[error("io error: {0}")] + Io(#[from] io::Error), + + /// Custom error. + #[error(transparent)] + Custom(#[from] AnyError), +} + +/// An error that can represent any error. +#[derive(Debug)] +pub struct AnyError { + error: anyhow::Error, +} + +//-------------------------------------------------------------------------------------------------- +// Methods +//-------------------------------------------------------------------------------------------------- + +impl VfsError { + /// Creates a new `Err` result. + pub fn custom(error: impl Into) -> VfsError { + VfsError::Custom(AnyError { + error: error.into(), + }) + } +} + +impl AnyError { + /// Downcasts the error to a `T`. + pub fn downcast(&self) -> Option<&T> + where + T: Display + fmt::Debug + Send + Sync + 'static, + { + self.error.downcast_ref::() + } +} + +//-------------------------------------------------------------------------------------------------- +// Functions +//-------------------------------------------------------------------------------------------------- + +/// Creates an `Ok` `VfsResult`. +#[allow(non_snake_case)] +pub fn Ok(value: T) -> VfsResult { + Result::Ok(value) +} + +//-------------------------------------------------------------------------------------------------- +// Trait Implementations +//-------------------------------------------------------------------------------------------------- + +impl PartialEq for AnyError { + fn eq(&self, other: &Self) -> bool { + self.error.to_string() == other.error.to_string() + } +} + +impl Display for AnyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.error) + } +} + +impl Error for AnyError {} diff --git a/virtualfs/lib/filesystem.rs b/virtualfs/lib/filesystem.rs new file mode 100644 index 0000000..8a91af9 --- /dev/null +++ b/virtualfs/lib/filesystem.rs @@ -0,0 +1,229 @@ +use crate::{Metadata, PathSegment, VfsResult}; + +use std::{ + path::{Path, PathBuf}, + pin::Pin, +}; + +use async_trait::async_trait; +use tokio::io::AsyncRead; + +//-------------------------------------------------------------------------------------------------- +// Traits +//-------------------------------------------------------------------------------------------------- + +/// A trait that defines the interface for a virtual file system implementation. +/// +/// The `VirtualFileSystem` trait provides a set of asynchronous operations for interacting with +/// files and directories in an abstract file system. This abstraction allows for different +/// implementations such as in-memory filesystems, overlay filesystems, or traditional disk-based +/// filesystems while maintaining a consistent interface. +#[async_trait] +pub trait VirtualFileSystem { + /// Checks if a file or directory exists at the specified path. + /// + /// ## Arguments + /// + /// * `path` - The path to check for existence + /// + /// ## Returns + /// + /// * `Ok(true)` if the path exists + /// * `Ok(false)` if the path does not exist + /// * `Err` if the check operation fails + async fn exists(&self, path: impl AsRef + Send + Sync) -> VfsResult; + + /// Creates a new empty file at the specified path. + /// + /// ## Arguments + /// + /// * `path` - The path where the file should be created + /// * `exists_ok` - If true, does not error when the file already exists + /// + /// ## Errors + /// + /// Returns an error if: + /// - The parent directory doesn't exist + /// - The file already exists and `exists_ok` is false + /// - Insufficient permissions + async fn create_file( + &self, + path: impl AsRef + Send + Sync, + exists_ok: bool, + ) -> VfsResult<()>; + + /// Creates a new directory at the specified path. + /// + /// ## Arguments + /// + /// * `path` - The path where the directory should be created + /// + /// ## Errors + /// + /// Returns an error if: + /// - The parent directory doesn't exist + /// - A file or directory already exists at the path + /// - Insufficient permissions + async fn create_directory(&self, path: impl AsRef + Send + Sync) -> VfsResult<()>; + + /// Creates a symbolic link at the specified path pointing to the target. + /// + /// ## Arguments + /// + /// * `path` - The path where the symlink should be created + /// * `target` - The path that the symlink should point to + /// + /// ## Errors + /// + /// Returns an error if: + /// - The parent directory doesn't exist + /// - A file or directory already exists at the path + /// - The target is invalid + /// - Insufficient permissions + async fn create_symlink( + &self, + path: impl AsRef + Send + Sync, + target: impl AsRef + Send + Sync, + ) -> VfsResult<()>; + + /// Reads data from a file starting at the specified offset. + /// + /// ## Arguments + /// + /// * `path` - The path of the file to read from + /// * `offset` - The byte offset where reading should start + /// * `length` - The number of bytes to read + /// + /// ## Returns + /// + /// Returns a pinned `AsyncRead` implementation that can be used to read the file data. + /// + /// ## Errors + /// + /// Returns an error if: + /// - The file doesn't exist + /// - The offset is beyond the end of the file + /// - Insufficient permissions + async fn read_file( + &self, + path: impl AsRef + Send + Sync, + offset: u64, + length: u64, + ) -> VfsResult>>; + + /// Lists the contents of a directory. + /// + /// ## Arguments + /// + /// * `path` - The path of the directory to read + /// + /// ## Returns + /// + /// Returns an iterator over the paths of entries in the directory. + /// + /// ## Errors + /// + /// Returns an error if: + /// - The path doesn't exist + /// - The path is not a directory + /// - Insufficient permissions + async fn read_directory( + &self, + path: impl AsRef + Send + Sync, + ) -> VfsResult + Send + Sync + 'static>>; + + /// Reads the target of a symbolic link. + /// + /// ## Arguments + /// + /// * `path` - The path of the symlink to read + /// + /// ## Returns + /// + /// Returns the target path of the symlink. + async fn read_symlink(&self, path: impl AsRef + Send + Sync) -> VfsResult; + + /// Gets the metadata of a file or directory. + /// + /// ## Arguments + /// + /// * `path` - The path of the file or directory to get metadata for + /// + /// ## Returns + /// + /// Returns the metadata of the file or directory. + async fn get_metadata(&self, path: impl AsRef + Send + Sync) -> VfsResult; + + /// Writes data to a file starting at the specified offset. + /// + /// ## Arguments + /// + /// * `path` - The path of the file to write to + /// * `offset` - The byte offset where writing should start + /// * `data` - An `AsyncRead` implementation providing the data to write + /// + /// ## Errors + /// + /// Returns an error if: + /// - The file doesn't exist + /// - The offset is invalid + /// - Insufficient permissions + /// - The filesystem is read-only + async fn write_file( + &self, + path: impl AsRef + Send + Sync, + offset: u64, + data: impl AsyncRead + Send + Sync + 'static, + ) -> VfsResult<()>; + + /// Removes a file from the filesystem. + /// + /// ## Arguments + /// + /// * `path` - The path of the file to remove + /// + /// ## Errors + /// + /// Returns an error if: + /// - The path doesn't exist + /// - The path is a directory (use `remove_directory` instead) + /// - Insufficient permissions + /// - The filesystem is read-only + async fn remove(&self, path: impl AsRef + Send + Sync) -> VfsResult<()>; + + /// Removes a directory and all its contents from the filesystem. + /// + /// ## Arguments + /// + /// * `path` - The path of the directory to remove + /// + /// ## Errors + /// + /// Returns an error if: + /// - The path doesn't exist + /// - The path is not a directory + /// - Insufficient permissions + /// - The filesystem is read-only + async fn remove_directory(&self, path: impl AsRef + Send + Sync) -> VfsResult<()>; + + /// Renames (moves) a file or directory to a new location. + /// + /// ## Arguments + /// + /// * `old_path` - The current path of the file or directory + /// * `new_path` - The desired new path + /// + /// ## Errors + /// + /// Returns an error if: + /// - The source path doesn't exist + /// - The destination path already exists + /// - The parent directory of the destination doesn't exist + /// - Insufficient permissions + /// - The filesystem is read-only + async fn rename( + &self, + old_path: impl AsRef + Send + Sync, + new_path: impl AsRef + Send + Sync, + ) -> VfsResult<()>; +} diff --git a/virtualfs/lib/implementations/memoryfs.rs b/virtualfs/lib/implementations/memoryfs.rs new file mode 100644 index 0000000..6512de8 --- /dev/null +++ b/virtualfs/lib/implementations/memoryfs.rs @@ -0,0 +1,1885 @@ +use std::{ + collections::HashMap, + path::{Component, Path, PathBuf}, + pin::Pin, + sync::Arc, + task::{Context, Poll}, +}; + +use async_trait::async_trait; +use getset::Getters; +use tokio::{io::AsyncRead, sync::RwLock}; + +use crate::{Metadata, ModeType, PathSegment, VfsError, VfsResult, VirtualFileSystem}; + +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// An in-memory implementation of a virtual file system. +/// +/// This implementation stores all files and directories in memory, making it useful for +/// testing and temporary file systems that don't need persistence. +#[derive(Debug, Clone, Getters)] +#[getset(get = "pub with_prefix")] +pub struct MemoryFileSystem { + /// The root directory of the file system + root_dir: Arc>, +} + +/// Represents a directory in the memory file system. +/// +/// A directory contains a collection of entries, where each entry is identified by a path segment +/// and can be either another directory, a file, or a symbolic link. +#[derive(Debug, Clone, Getters)] +#[getset(get = "pub with_prefix")] +pub struct Dir { + /// Metadata associated with the directory + metadata: Metadata, + + /// Map of path segments to directory entries + entries: HashMap, +} + +/// Represents a file in the memory file system. +/// +/// A file contains metadata and its content as a byte vector. +#[derive(Debug, Clone, Getters)] +#[getset(get = "pub with_prefix")] +pub struct File { + /// Metadata associated with the file + metadata: Metadata, + + /// Content of the file as a byte vector + content: Vec, +} + +/// Represents a symbolic link in the memory file system. +/// +/// A symbolic link contains metadata and points to a target path. +#[derive(Debug, Clone, Getters)] +#[getset(get = "pub with_prefix")] +pub struct Symlink { + /// Metadata associated with the symlink + metadata: Metadata, + + /// Target path that the symlink points to + target: PathBuf, +} + +/// Represents an entity in the memory file system. +/// +/// An entity can be either a directory, a file, or a symbolic link. +#[derive(Debug, Clone)] +pub enum Entity { + /// A directory containing other entities + Dir(Dir), + + /// A file containing data + File(File), + + /// A symbolic link pointing to another path + Symlink(Symlink), +} + +/// A reader that provides async read access to a memory file's contents +struct MemoryFileReader { + /// The content to read from + content: Vec, + + /// Current position in the content + position: usize, + + /// Maximum number of bytes to read + remaining: usize, +} + +//-------------------------------------------------------------------------------------------------- +// Methods +//-------------------------------------------------------------------------------------------------- + +impl MemoryFileSystem { + /// Creates a new empty memory file system. + pub fn new() -> Self { + Self { + root_dir: Arc::new(RwLock::new(Dir::new())), + } + } + + /// Splits the given path into its parent and the last path segment. + /// If the path has no explicit parent, an empty path is used as the parent. + #[inline] + fn split_path(path: &Path) -> VfsResult<(&Path, PathSegment)> { + let parent = path.parent().unwrap_or_else(|| Path::new("")); + let name_os = path + .file_name() + .ok_or_else(|| VfsError::InvalidPathComponent("No filename provided".into()))?; + let name = name_os + .to_str() + .ok_or_else(|| VfsError::InvalidPathComponent("Invalid filename".into()))?; + let segment = PathSegment::try_from(name)?; + Ok((parent, segment)) + } + + /// Given a mutable reference to the current root directory, returns a mutable reference + /// to the directory corresponding to the provided parent path. If parent is empty, returns the root. + #[inline] + fn get_parent_dir<'a>(root: &'a mut Dir, parent: &Path) -> VfsResult<&'a mut Dir> { + if parent == Path::new("") { + Ok(root) + } else { + match root.find_mut(parent)? { + Some(entity) => entity.as_mut_dir(), + None => Err(VfsError::ParentDirectoryNotFound(parent.to_path_buf())), + } + } + } +} + +impl File { + /// Creates a new empty file. + pub fn new() -> Self { + Self { + metadata: Metadata::new(ModeType::File), + content: Vec::new(), + } + } + + /// Creates a new file with the given content. + /// + /// ## Arguments + /// + /// * `content` - The initial content of the file as a byte vector + /// + /// ## Returns + /// + /// A new `File` instance with the specified content and default metadata. + pub fn with_content(content: Vec) -> Self { + Self { + metadata: Metadata::new(ModeType::File), + content, + } + } +} + +impl Symlink { + /// Creates a new symbolic link pointing to the given target. + /// + /// ## Arguments + /// + /// * `target` - The path that the symlink should point to + /// + /// ## Returns + /// + /// A new `Symlink` instance with the specified target and default metadata. + pub fn new(target: PathBuf) -> Self { + Self { + metadata: Metadata::new(ModeType::Symlink), + target, + } + } +} + +impl Entity { + /// Attempts to get a reference to this entity as a directory. + /// + /// ## Returns + /// + /// * `Ok(&Dir)` - If this entity is a directory + /// * `Err(VfsError::NotADirectory)` - If this entity is not a directory + pub fn as_dir(&self) -> VfsResult<&Dir> { + match self { + Entity::Dir(dir) => Ok(dir), + _ => Err(VfsError::NotADirectory(PathBuf::new())), + } + } + + /// Attempts to get a mutable reference to this entity as a directory. + /// + /// ## Returns + /// + /// * `Ok(&mut Dir)` - If this entity is a directory + /// * `Err(VfsError::NotADirectory)` - If this entity is not a directory + pub fn as_mut_dir(&mut self) -> VfsResult<&mut Dir> { + match self { + Entity::Dir(dir) => Ok(dir), + _ => Err(VfsError::NotADirectory(PathBuf::new())), + } + } + + /// Attempts to get a reference to this entity as a file. + /// + /// ## Returns + /// + /// * `Ok(&File)` - If this entity is a file + /// * `Err(VfsError::NotAFile)` - If this entity is not a file + pub fn as_file(&self) -> VfsResult<&File> { + match self { + Entity::File(file) => Ok(file), + _ => Err(VfsError::NotAFile(PathBuf::new())), + } + } + + /// Attempts to get a mutable reference to this entity as a file. + /// + /// ## Returns + /// + /// * `Ok(&mut File)` - If this entity is a file + /// * `Err(VfsError::NotAFile)` - If this entity is not a file + pub fn as_mut_file(&mut self) -> VfsResult<&mut File> { + match self { + Entity::File(file) => Ok(file), + _ => Err(VfsError::NotAFile(PathBuf::new())), + } + } + + /// Attempts to get a reference to this entity as a symbolic link. + /// + /// ## Returns + /// + /// * `Ok(&Symlink)` - If this entity is a symbolic link + /// * `Err(VfsError::NotASymlink)` - If this entity is not a symbolic link + pub fn as_symlink(&self) -> VfsResult<&Symlink> { + match self { + Entity::Symlink(symlink) => Ok(symlink), + _ => Err(VfsError::NotASymlink(PathBuf::new())), + } + } + + /// Attempts to get a mutable reference to this entity as a symbolic link. + /// + /// ## Returns + /// + /// * `Ok(&mut Symlink)` - If this entity is a symbolic link + /// * `Err(VfsError::NotASymlink)` - If this entity is not a symbolic link + pub fn as_mut_symlink(&mut self) -> VfsResult<&mut Symlink> { + match self { + Entity::Symlink(symlink) => Ok(symlink), + _ => Err(VfsError::NotASymlink(PathBuf::new())), + } + } +} + +impl Dir { + /// Creates a new empty directory. + /// + /// ## Returns + /// + /// A new `Dir` instance with no entries and default metadata. + pub fn new() -> Self { + Self { + metadata: Metadata::new(ModeType::Directory), + entries: HashMap::new(), + } + } + + /// Retrieves an entity from the directory's entries using the given path segment. + /// + /// ## Arguments + /// + /// * `path` - The path segment to look up in the directory entries + /// + /// ## Returns + /// + /// * `Ok(&Entity)` - A reference to the found entity + /// * `Err(VfsError::NotFound)` - If no entry exists with the given path segment + pub fn get(&self, path: &PathSegment) -> Option<&Entity> { + self.entries.get(path) + } + + /// Retrieves a mutable reference to an entity from the directory's entries using the given path segment. + /// + /// ## Arguments + /// + /// * `path` - The path segment to look up in the directory entries + /// + /// ## Returns + /// + /// * `Ok(&mut Entity)` - A mutable reference to the found entity + /// * `Err(VfsError::NotFound)` - If no entry exists with the given path segment + pub fn get_mut(&mut self, path: PathSegment) -> Option<&mut Entity> { + self.entries.get_mut(&path) + } + + /// Adds a new entity to the directory's entries with the given path segment. + /// + /// ## Arguments + /// + /// * `path` - The path segment under which to store the entity + /// * `entity` - The entity to store + /// + /// ## Returns + /// + /// * `Ok(())` - If the entity was successfully added + /// * `Err(VfsError::AlreadyExists)` - If an entry already exists with the given path segment + pub fn put(&mut self, path: PathSegment, entity: Entity) -> VfsResult<()> { + if self.entries.contains_key(&path) { + return Err(VfsError::AlreadyExists(path.into())); + } + self.entries.insert(path, entity); + Ok(()) + } + + /// Traverses a path starting from this directory to find an entity. + /// + /// ## Arguments + /// + /// * `path` - The path to traverse, can be empty, ".", or a path with normal components + /// + /// ## Returns + /// + /// * `Ok(&Entity)` - A reference to the found entity + /// * `Err(VfsError::NotFound)` - If the path doesn't exist + /// * `Err(VfsError::NotADirectory)` - If a non-final path component isn't a directory + /// * `Err(VfsError::InvalidPathComponent)` - If the path contains invalid components + /// + /// ## Examples + /// + /// ```rust + /// # use std::path::Path; + /// # use virtualfs::{Dir, Entity, File, PathSegment, VfsResult}; + /// # + /// # fn main() -> VfsResult<()> { + /// # let mut dir = Dir::new(); + /// # dir.put( + /// # PathSegment::try_from("foo").unwrap(), + /// # Entity::Dir(Dir::new()) + /// # )?; + /// # + /// // Find an entity at path "foo" + /// let entity = dir.find("foo")?.unwrap(); + /// assert!(matches!(entity, Entity::Dir(_))); + /// # Ok(()) + /// # } + /// ``` + pub fn find(&self, path: impl AsRef + Send + Sync) -> VfsResult> { + let path = path.as_ref(); + + // Ensure the path is not empty + let mut components = path.components().peekable(); + if components.peek().is_none() { + return Err(VfsError::InvalidPathComponent("Empty path provided".into())); + } + + // Traverse the components + let mut current_dir = self; + + while let Some(component) = components.next() { + match component { + // Only allow normal components + Component::Normal(os_str) => { + // Convert the component to a PathSegment + let segment = PathSegment::try_from(os_str.to_str().ok_or_else(|| { + VfsError::InvalidPathComponent(os_str.to_string_lossy().into_owned()) + })?)?; + + let entry = current_dir.get(&segment); + + // If this is the last component, return the entry + if components.peek().is_none() { + return Ok(entry); + } + + // Otherwise, ensure it's a directory and continue traversing + match entry { + Some(entity) => current_dir = entity.as_dir()?, + None => return Ok(None), + } + } + // Reject all non-normal components, including CurDir (.) and ParentDir (..) + _ => { + return Err(VfsError::InvalidPathComponent( + component.as_os_str().to_string_lossy().into_owned(), + )) + } + } + } + + unreachable!() + } + + /// Traverses a path starting from this directory to find an entity, returning a mutable reference. + /// + /// This method is similar to `find`, but returns a mutable reference to the found entity. + /// It follows the same path traversal rules and error handling as `find`. + /// + /// ## Arguments + /// + /// * `path` - The path to traverse, can be empty, ".", or a path with normal components + /// + /// ## Returns + /// + /// * `Ok(Some(&mut Entity))` - A mutable reference to the found entity + /// * `Ok(None)` - If the path doesn't exist + /// * `Err(VfsError::NotADirectory)` - If a non-final path component isn't a directory + /// * `Err(VfsError::InvalidPathComponent)` - If the path contains invalid components + /// + /// ## Examples + /// + /// ```rust + /// # use std::path::Path; + /// # use virtualfs::{Dir, Entity, File, PathSegment, VfsResult}; + /// # + /// # fn main() -> VfsResult<()> { + /// # let mut dir = Dir::new(); + /// # dir.put( + /// # PathSegment::try_from("foo").unwrap(), + /// # Entity::Dir(Dir::new()) + /// # )?; + /// # + /// // Find and modify an entity at path "foo" + /// if let Some(entity) = dir.find_mut("foo")? { + /// assert!(matches!(entity, Entity::Dir(_))); + /// } + /// # Ok(()) + /// # } + /// ``` + pub fn find_mut( + &mut self, + path: impl AsRef + Send + Sync, + ) -> VfsResult> { + let path = path.as_ref(); + + // Ensure the path is not empty + let mut components = path.components().peekable(); + if components.peek().is_none() { + return Err(VfsError::InvalidPathComponent("Empty path provided".into())); + } + + // Traverse the components + let mut current_dir = self; + + while let Some(component) = components.next() { + match component { + // Only allow normal components + Component::Normal(os_str) => { + // Convert the component to a PathSegment + let segment = PathSegment::try_from(os_str.to_str().ok_or_else(|| { + VfsError::InvalidPathComponent(os_str.to_string_lossy().into_owned()) + })?)?; + + let entry = current_dir.get_mut(segment); + + // If this is the last component, return the entry + if components.peek().is_none() { + return Ok(entry); + } + + // Otherwise, ensure it's a directory and continue traversing + match entry { + Some(entity) => current_dir = entity.as_mut_dir()?, + None => return Ok(None), + } + } + // Reject all non-normal components, including CurDir (.) and ParentDir (..) + _ => { + return Err(VfsError::InvalidPathComponent( + component.as_os_str().to_string_lossy().into_owned(), + )) + } + } + } + + unreachable!() + } +} + +impl MemoryFileReader { + fn new(content: Vec, offset: u64, length: u64) -> Self { + let offset = offset.min(content.len() as u64) as usize; + let remaining = length.min(content.len() as u64 - offset as u64) as usize; + let content = content[offset..offset + remaining].to_vec(); + + Self { + content, + position: 0, + remaining, + } + } +} + +//-------------------------------------------------------------------------------------------------- +// Trait Implementations +//-------------------------------------------------------------------------------------------------- + +#[async_trait] +impl VirtualFileSystem for MemoryFileSystem { + async fn exists(&self, path: impl AsRef + Send + Sync) -> VfsResult { + let path = path.as_ref(); + let result = self.root_dir.read().await.find(path)?.is_some(); + + Ok(result) + } + + async fn create_file( + &self, + path: impl AsRef + Send + Sync, + exists_ok: bool, + ) -> VfsResult<()> { + let path = path.as_ref(); + let (parent, filename) = MemoryFileSystem::split_path(path)?; + + let mut root = self.root_dir.write().await; + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, parent)?; + + if let Some(_) = parent_dir.get(&filename) { + if !exists_ok { + return Err(VfsError::AlreadyExists(path.to_path_buf())); + } + return Ok(()); + } + + parent_dir.put(filename, Entity::File(File::new()))?; + + Ok(()) + } + + async fn create_directory(&self, path: impl AsRef + Send + Sync) -> VfsResult<()> { + let path = path.as_ref(); + let (parent, dirname) = MemoryFileSystem::split_path(path)?; + + let mut root = self.root_dir.write().await; + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, parent)?; + + if parent_dir.get(&dirname).is_some() { + return Err(VfsError::AlreadyExists(path.to_path_buf())); + } + + parent_dir.put(dirname, Entity::Dir(Dir::new()))?; + + Ok(()) + } + + async fn create_symlink( + &self, + path: impl AsRef + Send + Sync, + target: impl AsRef + Send + Sync, + ) -> VfsResult<()> { + let path = path.as_ref(); + let target = target.as_ref(); + + if target.as_os_str().is_empty() { + return Err(VfsError::InvalidSymlinkTarget(target.to_path_buf())); + } + + let (parent, linkname) = MemoryFileSystem::split_path(path)?; + + let mut root = self.root_dir.write().await; + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, parent)?; + + if parent_dir.get(&linkname).is_some() { + return Err(VfsError::AlreadyExists(path.to_path_buf())); + } + + parent_dir.put( + linkname, + Entity::Symlink(Symlink::new(target.to_path_buf())), + )?; + + Ok(()) + } + + async fn read_file( + &self, + path: impl AsRef + Send + Sync, + offset: u64, + length: u64, + ) -> VfsResult>> { + let path = path.as_ref(); + + // Find the file + let root = self.root_dir.read().await; + let entity = root + .find(path)? + .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?; + + // Ensure it's a file and get its contents + let file = entity.as_file()?; + let content = file.get_content().clone(); + + // Create and return the reader + Ok(Box::pin(MemoryFileReader::new(content, offset, length))) + } + + async fn read_directory( + &self, + path: impl AsRef + Send + Sync, + ) -> VfsResult + Send + Sync + 'static>> { + let path = path.as_ref(); + + // Find the directory + let root = self.root_dir.read().await; + let entity = if path == Path::new("") { + Ok(&*root) + } else { + root.find(path)? + .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))? + .as_dir() + }?; + + // Clone PathSegments to avoid lifetime issues + let entries = entity.entries.keys().cloned().collect::>(); + + Ok(Box::new(entries.into_iter())) + } + + async fn read_symlink(&self, path: impl AsRef + Send + Sync) -> VfsResult { + let path = path.as_ref(); + + // Find the symlink + let root = self.root_dir.read().await; + let entity = root + .find(path)? + .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?; + + // Ensure it's a symlink and get its target + let symlink = entity.as_symlink()?; + Ok(symlink.get_target().clone()) + } + + async fn get_metadata(&self, path: impl AsRef + Send + Sync) -> VfsResult { + let path = path.as_ref(); + + // Find the entity + let root = self.root_dir.read().await; + let entity = root + .find(path)? + .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?; + + // Return a clone of the metadata based on the entity type + let metadata = match entity { + Entity::Dir(dir) => dir.get_metadata().clone(), + Entity::File(file) => file.get_metadata().clone(), + Entity::Symlink(symlink) => symlink.get_metadata().clone(), + }; + + Ok(metadata) + } + + async fn write_file( + &self, + path: impl AsRef + Send + Sync, + offset: u64, + data: impl AsyncRead + Send + Sync + 'static, + ) -> VfsResult<()> { + let path = path.as_ref(); + + // Find the file and get a mutable reference + let mut root = self.root_dir.write().await; + let entity = root + .find_mut(path)? + .ok_or_else(|| VfsError::NotFound(path.to_path_buf()))?; + + // Ensure it's a file and get mutable access to its content + let file = entity.as_mut_file()?; + let content = &mut file.content; + + // Convert offset to usize, ensuring it's not too large + let offset = usize::try_from(offset).map_err(|_| VfsError::InvalidOffset { + path: path.to_path_buf(), + offset, + })?; + + // If offset is beyond current size, extend the file with zeros + if offset > content.len() { + content.resize(offset, 0); + } + + // Read all data into a buffer + let mut buffer = Vec::new(); + let mut pinned_data = Box::pin(data); + tokio::io::copy(&mut pinned_data, &mut buffer) + .await + .map_err(VfsError::Io)?; + + // Ensure the content vector has enough capacity + let required_len = offset + buffer.len(); + if required_len > content.len() { + content.resize(required_len, 0); + } + + // Write the data at the specified offset + content[offset..offset + buffer.len()].copy_from_slice(&buffer); + + Ok(()) + } + + async fn remove(&self, path: impl AsRef + Send + Sync) -> VfsResult<()> { + let path = path.as_ref(); + let (parent, key) = MemoryFileSystem::split_path(path)?; + + let mut root = self.root_dir.write().await; + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, parent)?; + + match parent_dir.get(&key) { + Some(entity) => match entity { + Entity::Dir(_) => return Err(VfsError::NotAFile(path.to_path_buf())), + _ => { + parent_dir.entries.remove(&key); + } + }, + None => return Err(VfsError::NotFound(path.to_path_buf())), + }; + + Ok(()) + } + + async fn remove_directory(&self, path: impl AsRef + Send + Sync) -> VfsResult<()> { + let path = path.as_ref(); + let (parent, dirname) = MemoryFileSystem::split_path(path)?; + + let mut root = self.root_dir.write().await; + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, parent)?; + + match parent_dir.get(&dirname) { + Some(entity) => match entity { + Entity::Dir(_) => parent_dir.entries.remove(&dirname), + _ => return Err(VfsError::NotADirectory(path.to_path_buf())), + }, + None => return Err(VfsError::NotFound(path.to_path_buf())), + }; + + Ok(()) + } + + async fn rename( + &self, + old_path: impl AsRef + Send + Sync, + new_path: impl AsRef + Send + Sync, + ) -> VfsResult<()> { + let old_path = old_path.as_ref(); + let new_path = new_path.as_ref(); + + let (old_parent, old_segment) = MemoryFileSystem::split_path(old_path)?; + let (new_parent, new_segment) = MemoryFileSystem::split_path(new_path)?; + + { + let root = self.root_dir.read().await; + + if old_parent != Path::new("") { + match root.find(old_parent)? { + Some(entity) => { + if !matches!(entity, Entity::Dir(_)) { + return Err(VfsError::NotADirectory(old_parent.to_path_buf())); + } + } + None => { + return Err(VfsError::ParentDirectoryNotFound(old_parent.to_path_buf())) + } + } + } + + if new_parent != Path::new("") { + match root.find(new_parent)? { + Some(entity) => { + if !matches!(entity, Entity::Dir(_)) { + return Err(VfsError::NotADirectory(new_parent.to_path_buf())); + } + } + None => { + return Err(VfsError::ParentDirectoryNotFound(new_parent.to_path_buf())) + } + } + } + + let source_dir = if old_parent == Path::new("") { + &root + } else { + root.find(old_parent)?.unwrap().as_dir()? + }; + + if !source_dir.entries.contains_key(&old_segment) { + return Err(VfsError::NotFound(old_path.to_path_buf())); + } + + let dest_dir = if new_parent == Path::new("") { + &root + } else { + root.find(new_parent)?.unwrap().as_dir()? + }; + + if dest_dir.entries.contains_key(&new_segment) { + return Err(VfsError::AlreadyExists(new_path.to_path_buf())); + } + } + + let mut root = self.root_dir.write().await; + + if old_parent == new_parent { + let parent_dir = MemoryFileSystem::get_parent_dir(&mut root, old_parent)?; + let entity = parent_dir.entries.remove(&old_segment).unwrap(); + parent_dir.entries.insert(new_segment, entity); + return Ok(()); + } + + let entity = if old_parent == Path::new("") { + root.entries.remove(&old_segment).unwrap() + } else { + MemoryFileSystem::get_parent_dir(&mut root, old_parent)? + .entries + .remove(&old_segment) + .unwrap() + }; + + if new_parent == Path::new("") { + root.entries.insert(new_segment, entity); + } else { + MemoryFileSystem::get_parent_dir(&mut root, new_parent)? + .entries + .insert(new_segment, entity); + } + + Ok(()) + } +} + +impl Default for MemoryFileSystem { + fn default() -> Self { + Self::new() + } +} + +impl Default for Dir { + fn default() -> Self { + Self::new() + } +} + +impl Default for File { + fn default() -> Self { + Self::new() + } +} + +impl AsyncRead for MemoryFileReader { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + let available = self.remaining; + if available == 0 { + return Poll::Ready(Ok(())); + } + + let to_read = buf.remaining().min(available); + if to_read == 0 { + return Poll::Ready(Ok(())); + } + + buf.put_slice(&self.content[self.position..self.position + to_read]); + self.position += to_read; + self.remaining -= to_read; + + Poll::Ready(Ok(())) + } +} + +//-------------------------------------------------------------------------------------------------- +// Tests +//-------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_memoryfs_dir_new() { + let dir = Dir::new(); + assert!(dir.entries.is_empty()); + assert_eq!(dir.metadata.get_size(), 0); + } + + #[test] + fn test_memoryfs_dir_put_and_get() { + let mut dir = Dir::new(); + let file = File::with_content(vec![1, 2, 3]); + let path = PathSegment::try_from("test.txt").unwrap(); + + // Test putting a new entry + assert!(dir.put(path.clone(), Entity::File(file)).is_ok()); + + // Test getting the entry + let entity = dir.get(&path).unwrap(); + match entity { + Entity::File(f) => assert_eq!(&f.content, &vec![1, 2, 3]), + _ => panic!("Expected file entity"), + } + + // Test putting to existing path fails + assert!(matches!( + dir.put(path, Entity::File(File::new())), + Err(VfsError::AlreadyExists(_)) + )); + } + + #[test] + fn test_memoryfs_dir_get_nonexistent() { + let dir = Dir::new(); + let path = PathSegment::try_from("nonexistent.txt").unwrap(); + assert!(dir.get(&path).is_none()); + } + + #[test] + fn test_memoryfs_dir_find() { + let mut root = Dir::new(); + let mut subdir = Dir::new(); + let file = File::with_content(vec![1, 2, 3]); + + // Set up directory structure: + // root/ + // subdir/ + // test.txt + let file_path = PathSegment::try_from("test.txt").unwrap(); + subdir.put(file_path, Entity::File(file)).unwrap(); + + let subdir_path = PathSegment::try_from("subdir").unwrap(); + root.put(subdir_path, Entity::Dir(subdir)).unwrap(); + + // Test finding file through path + let found = root.find("subdir/test.txt").unwrap().unwrap(); + match found { + Entity::File(f) => assert_eq!(&f.content, &vec![1, 2, 3]), + _ => panic!("Expected file entity"), + } + + // Test finding directory + let found = root.find("subdir").unwrap().unwrap(); + assert!(matches!(found, Entity::Dir(_))); + + // Test nonexistent path + assert!(root.find("nonexistent/path").unwrap().is_none()); + + // Test invalid path components + assert!(matches!( + root.find(".."), + Err(VfsError::InvalidPathComponent(_)) + )); + assert!(matches!( + root.find("."), + Err(VfsError::InvalidPathComponent(_)) + )); + assert!(matches!( + root.find(""), + Err(VfsError::InvalidPathComponent(_)) + )); + } + + #[test] + fn test_memoryfs_dir_get_mut() { + let mut dir = Dir::new(); + let file = File::with_content(vec![1, 2, 3]); + let path = PathSegment::try_from("test.txt").unwrap(); + + // Add initial file + dir.put(path.clone(), Entity::File(file)).unwrap(); + + // Get mutable reference and modify + if let Entity::File(file) = dir.get_mut(path).unwrap() { + file.content.push(4); + } + + // Verify modification + if let Entity::File(file) = dir + .get(&PathSegment::try_from("test.txt").unwrap()) + .unwrap() + { + assert_eq!(&file.content, &vec![1, 2, 3, 4]); + } else { + panic!("Expected file entity"); + } + } + + #[test] + fn test_memoryfs_dir_find_mut() { + let mut root = Dir::new(); + let mut subdir = Dir::new(); + let file = File::with_content(vec![1, 2, 3]); + + // Set up directory structure: + // root/ + // subdir/ + // test.txt + let file_path = PathSegment::try_from("test.txt").unwrap(); + subdir.put(file_path, Entity::File(file)).unwrap(); + + let subdir_path = PathSegment::try_from("subdir").unwrap(); + root.put(subdir_path, Entity::Dir(subdir)).unwrap(); + + // Test finding and modifying file through path + let found = root.find_mut("subdir/test.txt").unwrap().unwrap(); + match found { + Entity::File(f) => { + assert_eq!(&f.content, &vec![1, 2, 3]); + f.content.push(4); + } + _ => panic!("Expected file entity"), + } + + // Verify the modification persisted + let found = root.find("subdir/test.txt").unwrap().unwrap(); + match found { + Entity::File(f) => assert_eq!(&f.content, &vec![1, 2, 3, 4]), + _ => panic!("Expected file entity"), + } + + // Test finding and modifying directory metadata + let found = root.find_mut("subdir").unwrap().unwrap(); + match found { + Entity::Dir(d) => { + d.metadata.set_size(100); + } + _ => panic!("Expected directory entity"), + } + + // Verify the directory modification persisted + let found = root.find("subdir").unwrap().unwrap(); + match found { + Entity::Dir(d) => assert_eq!(d.metadata.get_size(), 100), + _ => panic!("Expected directory entity"), + } + + // Test nonexistent path + assert!(root.find_mut("nonexistent/path").unwrap().is_none()); + + // Test invalid path components + assert!(matches!( + root.find_mut(".."), + Err(VfsError::InvalidPathComponent(_)) + )); + assert!(matches!( + root.find_mut("."), + Err(VfsError::InvalidPathComponent(_)) + )); + assert!(matches!( + root.find_mut(""), + Err(VfsError::InvalidPathComponent(_)) + )); + + // Test finding and modifying nested file when parent is not a directory + let file_path = PathSegment::try_from("not_a_dir").unwrap(); + root.put(file_path, Entity::File(File::new())).unwrap(); + assert!(matches!( + root.find_mut("not_a_dir/file.txt"), + Err(VfsError::NotADirectory(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_create_file() { + let fs = MemoryFileSystem::new(); + + // Test creating a file in root directory + assert!(fs.create_file("test.txt", false).await.is_ok()); + + // Verify file exists and is a file + let root = fs.root_dir.read().await; + let file = root + .get(&PathSegment::try_from("test.txt").unwrap()) + .unwrap(); + assert!(matches!(file, Entity::File(_))); + drop(root); + + // Test creating file when parent directory doesn't exist + assert!(matches!( + fs.create_file("nonexistent/test.txt", false).await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + + // Test exists_ok behavior + assert!(matches!( + fs.create_file("test.txt", false).await, + Err(VfsError::AlreadyExists(_)) + )); + assert!(fs.create_file("test.txt", true).await.is_ok()); + + // Test creating file in subdirectory + { + // First create the directory + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("subdir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + drop(root); + + // Then create file in it + assert!(fs.create_file("subdir/nested.txt", false).await.is_ok()); + + // Verify nested file exists + let root = fs.root_dir.read().await; + let subdir = root.find("subdir").unwrap().unwrap().as_dir().unwrap(); + let file = subdir + .get(&PathSegment::try_from("nested.txt").unwrap()) + .unwrap(); + assert!(matches!(file, Entity::File(_))); + } + + // Test invalid path components + for invalid_path in [".", "..", "/"] { + assert!(matches!( + fs.create_file(invalid_path, false).await, + Err(VfsError::InvalidPathComponent(_)) + )); + } + + // Test concurrent file creation + let fs2 = fs.clone(); + let handle1 = tokio::spawn(async move { fs.create_file("concurrent.txt", false).await }); + let handle2 = tokio::spawn(async move { fs2.create_file("concurrent.txt", false).await }); + + let (result1, result2) = tokio::join!(handle1, handle2); + let results = vec![result1.unwrap(), result2.unwrap()]; + assert!(results.iter().filter(|r| r.is_ok()).count() == 1); + assert!( + results + .iter() + .filter(|r| matches!(r, Err(VfsError::AlreadyExists(_)))) + .count() + == 1 + ); + } + + #[tokio::test] + async fn test_memoryfs_create_directory() { + let fs = MemoryFileSystem::new(); + + // Test creating a directory in root + assert!(fs.create_directory("testdir").await.is_ok()); + + // Verify directory exists and is a directory + let root = fs.root_dir.read().await; + let dir = root + .get(&PathSegment::try_from("testdir").unwrap()) + .unwrap(); + assert!(matches!(dir, Entity::Dir(_))); + drop(root); + + // Test creating directory when parent doesn't exist + assert!(matches!( + fs.create_directory("nonexistent/subdir").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + + // Test creating existing directory fails + assert!(matches!( + fs.create_directory("testdir").await, + Err(VfsError::AlreadyExists(_)) + )); + + // Test creating nested directory + { + // Create parent directory first + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("parent").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + drop(root); + + // Create nested directory + assert!(fs.create_directory("parent/child").await.is_ok()); + + // Verify nested directory exists + let root = fs.root_dir.read().await; + let parent = root.find("parent").unwrap().unwrap().as_dir().unwrap(); + let child = parent + .get(&PathSegment::try_from("child").unwrap()) + .unwrap(); + assert!(matches!(child, Entity::Dir(_))); + } + + // Test invalid path components + for invalid_path in [".", "..", "/"] { + assert!(matches!( + fs.create_directory(invalid_path).await, + Err(VfsError::InvalidPathComponent(_)) + )); + } + + // Test concurrent directory creation + let fs2 = fs.clone(); + let handle1 = tokio::spawn(async move { fs.create_directory("concurrent").await }); + let handle2 = tokio::spawn(async move { fs2.create_directory("concurrent").await }); + + let (result1, result2) = tokio::join!(handle1, handle2); + let results = vec![result1.unwrap(), result2.unwrap()]; + assert!(results.iter().filter(|r| r.is_ok()).count() == 1); + assert!( + results + .iter() + .filter(|r| matches!(r, Err(VfsError::AlreadyExists(_)))) + .count() + == 1 + ); + } + + #[tokio::test] + async fn test_memoryfs_create_symlink() { + let fs = MemoryFileSystem::new(); + + // Test creating a symlink in root + assert!(fs.create_symlink("link", "target").await.is_ok()); + + // Verify symlink exists and points to correct target + let root = fs.root_dir.read().await; + let link = root.get(&PathSegment::try_from("link").unwrap()).unwrap(); + match link { + Entity::Symlink(symlink) => assert_eq!(symlink.get_target(), &PathBuf::from("target")), + _ => panic!("Expected symlink entity"), + } + drop(root); + + // Test creating symlink when parent doesn't exist + assert!(matches!( + fs.create_symlink("nonexistent/link", "target").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + + // Test creating existing symlink fails + assert!(matches!( + fs.create_symlink("link", "new_target").await, + Err(VfsError::AlreadyExists(_)) + )); + + // Test creating symlink with empty target + assert!(matches!( + fs.create_symlink("invalid", "").await, + Err(VfsError::InvalidSymlinkTarget(_)) + )); + + // Test creating nested symlink + { + // Create parent directory first + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("parent").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + drop(root); + + // Create nested symlink + assert!(fs.create_symlink("parent/link", "../target").await.is_ok()); + + // Verify nested symlink exists and points to correct target + let root = fs.root_dir.read().await; + let parent = root.find("parent").unwrap().unwrap().as_dir().unwrap(); + let link = parent.get(&PathSegment::try_from("link").unwrap()).unwrap(); + match link { + Entity::Symlink(symlink) => { + assert_eq!(symlink.get_target(), &PathBuf::from("../target")) + } + _ => panic!("Expected symlink entity"), + } + } + + // Test invalid path components + for invalid_path in [".", "..", "/"] { + assert!(matches!( + fs.create_symlink(invalid_path, "target").await, + Err(VfsError::InvalidPathComponent(_)) + )); + } + + // Test concurrent symlink creation + let fs2 = fs.clone(); + let handle1 = tokio::spawn(async move { fs.create_symlink("concurrent", "target1").await }); + let handle2 = + tokio::spawn(async move { fs2.create_symlink("concurrent", "target2").await }); + + let (result1, result2) = tokio::join!(handle1, handle2); + let results = vec![result1.unwrap(), result2.unwrap()]; + assert!(results.iter().filter(|r| r.is_ok()).count() == 1); + assert!( + results + .iter() + .filter(|r| matches!(r, Err(VfsError::AlreadyExists(_)))) + .count() + == 1 + ); + } + + #[tokio::test] + async fn test_memoryfs_read_file() { + let fs = MemoryFileSystem::new(); + let content = vec![1, 2, 3, 4, 5]; + + // Create a test file + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("test.bin").unwrap(), + Entity::File(File::with_content(content.clone())), + ) + .unwrap(); + } + + // Test reading entire file + let mut reader = fs.read_file("test.bin", 0, 5).await.unwrap(); + let mut buf = Vec::new(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + assert_eq!(buf, content); + + // Test reading with offset + let mut reader = fs.read_file("test.bin", 2, 2).await.unwrap(); + let mut buf = Vec::new(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + assert_eq!(buf, vec![3, 4]); + + // Test reading beyond file size + let mut reader = fs.read_file("test.bin", 4, 10).await.unwrap(); + let mut buf = Vec::new(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + assert_eq!(buf, vec![5]); + + // Test reading non-existent file + assert!(matches!( + fs.read_file("nonexistent", 0, 1).await, + Err(VfsError::NotFound(_)) + )); + + // Test reading directory as file + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("dir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + } + assert!(matches!( + fs.read_file("dir", 0, 1).await, + Err(VfsError::NotAFile(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_read_directory() { + let fs = MemoryFileSystem::new(); + + // Create test directory structure + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("file1.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + root.put( + PathSegment::try_from("file2.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + + let mut subdir = Dir::new(); + subdir + .put( + PathSegment::try_from("nested.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + root.put( + PathSegment::try_from("subdir").unwrap(), + Entity::Dir(subdir), + ) + .unwrap(); + } + + // Test reading root directory + let entries: Vec<_> = fs.read_directory("").await.unwrap().collect(); + assert_eq!(entries.len(), 3); + assert!(entries.contains(&PathSegment::try_from("file1.txt").unwrap())); + assert!(entries.contains(&PathSegment::try_from("file2.txt").unwrap())); + assert!(entries.contains(&PathSegment::try_from("subdir").unwrap())); + + // Test reading subdirectory + let entries: Vec<_> = fs.read_directory("subdir").await.unwrap().collect(); + assert_eq!(entries.len(), 1); + assert!(entries.contains(&PathSegment::try_from("nested.txt").unwrap())); + + // Test reading non-existent directory + assert!(matches!( + fs.read_directory("nonexistent").await, + Err(VfsError::NotFound(_)) + )); + + // Test reading file as directory + assert!(matches!( + fs.read_directory("file1.txt").await, + Err(VfsError::NotADirectory(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_read_symlink() { + let fs = MemoryFileSystem::new(); + + // Create test symlink + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("link").unwrap(), + Entity::Symlink(Symlink::new(PathBuf::from("target"))), + ) + .unwrap(); + } + + // Test reading symlink + let target = fs.read_symlink("link").await.unwrap(); + assert_eq!(target, PathBuf::from("target")); + + // Test reading non-existent symlink + assert!(matches!( + fs.read_symlink("nonexistent").await, + Err(VfsError::NotFound(_)) + )); + + // Test reading file as symlink + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("file").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + } + assert!(matches!( + fs.read_symlink("file").await, + Err(VfsError::NotASymlink(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_write_file() { + let fs = MemoryFileSystem::new(); + + // Create an empty file + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("test.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + } + + // Test writing to file + let data = b"Hello, World!".to_vec(); + let reader = std::io::Cursor::new(data.clone()); + assert!(fs.write_file("test.txt", 0, reader).await.is_ok()); + + // Verify content + let mut buf = Vec::new(); + let mut reader = fs + .read_file("test.txt", 0, data.len() as u64) + .await + .unwrap(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + assert_eq!(buf, data.clone()); + + // Test writing with offset + let append_data = b", Rust!".to_vec(); + let reader = std::io::Cursor::new(append_data.clone()); + let data_len = data.len() as u64; + assert!(fs.write_file("test.txt", data_len, reader).await.is_ok()); + + // Verify appended content + let mut buf = Vec::new(); + let total_len = data_len + append_data.len() as u64; + let mut reader = fs.read_file("test.txt", 0, total_len).await.unwrap(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + let mut expected = data.clone(); + expected.extend_from_slice(&append_data); + assert_eq!(buf, expected); + + // Test writing with gap (offset beyond current size) + let gap_data = b"gap".to_vec(); + let reader = std::io::Cursor::new(gap_data.clone()); + let gap_offset = total_len + 5; + assert!(fs.write_file("test.txt", gap_offset, reader).await.is_ok()); + + // Verify content with gap (should be filled with zeros) + let mut buf = Vec::new(); + let final_len = gap_offset + gap_data.len() as u64; + let mut reader = fs.read_file("test.txt", 0, final_len).await.unwrap(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + expected.extend_from_slice(&vec![0; 5]); + expected.extend_from_slice(&gap_data); + assert_eq!(buf, expected); + + // Test writing to non-existent file + let reader = std::io::Cursor::new(vec![1, 2, 3]); + assert!(matches!( + fs.write_file("nonexistent", 0, reader).await, + Err(VfsError::NotFound(_)) + )); + + // Test writing to directory + { + let mut root = fs.root_dir.write().await; + root.put( + PathSegment::try_from("dir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + } + let reader = std::io::Cursor::new(vec![1, 2, 3]); + assert!(matches!( + fs.write_file("dir", 0, reader).await, + Err(VfsError::NotAFile(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_get_metadata() { + let fs = MemoryFileSystem::new(); + + // Create test entities + { + let mut root = fs.root_dir.write().await; + // Create a file + root.put( + PathSegment::try_from("file.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + // Create a directory + root.put( + PathSegment::try_from("dir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + // Create a symlink + root.put( + PathSegment::try_from("link").unwrap(), + Entity::Symlink(Symlink::new(PathBuf::from("target"))), + ) + .unwrap(); + } + + cfg_if::cfg_if! { + if #[cfg(unix)] { + // Test getting file metadata + let metadata = fs.get_metadata("file.txt").await.unwrap(); + assert_eq!(metadata.get_mode().get_type(), Some(ModeType::File)); + + // Test getting directory metadata + let metadata = fs.get_metadata("dir").await.unwrap(); + assert_eq!(metadata.get_mode().get_type(), Some(ModeType::Directory)); + + // Test getting symlink metadata + let metadata = fs.get_metadata("link").await.unwrap(); + assert_eq!(metadata.get_mode().get_type(), Some(ModeType::Symlink)); + } + } + + // Test getting metadata for non-existent path + assert!(matches!( + fs.get_metadata("nonexistent").await, + Err(VfsError::NotFound(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_remove() { + let fs = MemoryFileSystem::new(); + + // Create test files and directories + { + let mut root = fs.root_dir.write().await; + // Create a file + root.put( + PathSegment::try_from("file.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + // Create a directory + root.put( + PathSegment::try_from("dir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + // Create a symlink + root.put( + PathSegment::try_from("link").unwrap(), + Entity::Symlink(Symlink::new(PathBuf::from("target"))), + ) + .unwrap(); + // Create a nested file + let mut subdir = Dir::new(); + subdir + .put( + PathSegment::try_from("nested.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + root.put( + PathSegment::try_from("subdir").unwrap(), + Entity::Dir(subdir), + ) + .unwrap(); + } + + // Test removing a file + assert!(fs.remove("file.txt").await.is_ok()); + assert!(matches!( + fs.read_file("file.txt", 0, 1).await, + Err(VfsError::NotFound(_)) + )); + + // Test removing a symlink + assert!(fs.remove("link").await.is_ok()); + assert!(matches!( + fs.read_symlink("link").await, + Err(VfsError::NotFound(_)) + )); + + // Test removing a directory (should fail) + assert!(matches!(fs.remove("dir").await, Err(VfsError::NotAFile(_)))); + + // Test removing a nested file + assert!(fs.remove("subdir/nested.txt").await.is_ok()); + assert!(matches!( + fs.read_file("subdir/nested.txt", 0, 1).await, + Err(VfsError::NotFound(_)) + )); + + // Test removing non-existent file + assert!(matches!( + fs.remove("nonexistent").await, + Err(VfsError::NotFound(_)) + )); + + // Test removing from non-existent parent directory + assert!(matches!( + fs.remove("nonexistent/file.txt").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_remove_directory() { + let fs = MemoryFileSystem::new(); + + // Create test directory structure + { + let mut root = fs.root_dir.write().await; + // Create an empty directory + root.put( + PathSegment::try_from("empty_dir").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + // Create a file + root.put( + PathSegment::try_from("file.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + // Create a nested directory structure + let mut subdir = Dir::new(); + let mut nested = Dir::new(); + nested + .put( + PathSegment::try_from("deep.txt").unwrap(), + Entity::File(File::new()), + ) + .unwrap(); + subdir + .put( + PathSegment::try_from("nested").unwrap(), + Entity::Dir(nested), + ) + .unwrap(); + root.put( + PathSegment::try_from("subdir").unwrap(), + Entity::Dir(subdir), + ) + .unwrap(); + } + + // Test removing an empty directory + assert!(fs.remove_directory("empty_dir").await.is_ok()); + assert!(matches!( + fs.read_directory("empty_dir").await, + Err(VfsError::NotFound(_)) + )); + + // Test removing a file as directory (should fail) + assert!(matches!( + fs.remove_directory("file.txt").await, + Err(VfsError::NotADirectory(_)) + )); + + // Test removing a nested directory + assert!(fs.remove_directory("subdir/nested").await.is_ok()); + let entries: Vec<_> = fs.read_directory("subdir").await.unwrap().collect(); + assert!(entries.is_empty()); + + // Test removing a directory with parent path + assert!(fs.remove_directory("subdir").await.is_ok()); + assert!(matches!( + fs.read_directory("subdir").await, + Err(VfsError::NotFound(_)) + )); + + // Test removing non-existent directory + assert!(matches!( + fs.remove_directory("nonexistent").await, + Err(VfsError::NotFound(_)) + )); + + // Test removing from non-existent parent directory + assert!(matches!( + fs.remove_directory("nonexistent/dir").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + } + + #[tokio::test] + async fn test_memoryfs_rename() { + let fs = MemoryFileSystem::new(); + + // Create test directory structure + { + let mut root = fs.root_dir.write().await; + + // Create files in root + root.put( + PathSegment::try_from("file.txt").unwrap(), + Entity::File(File::with_content(vec![1, 2, 3])), + ) + .unwrap(); + + // Create directories + root.put( + PathSegment::try_from("dir1").unwrap(), + Entity::Dir(Dir::new()), + ) + .unwrap(); + + let mut dir2 = Dir::new(); + dir2.put( + PathSegment::try_from("nested.txt").unwrap(), + Entity::File(File::with_content(vec![4, 5, 6])), + ) + .unwrap(); + root.put(PathSegment::try_from("dir2").unwrap(), Entity::Dir(dir2)) + .unwrap(); + + // Create symlink + root.put( + PathSegment::try_from("link").unwrap(), + Entity::Symlink(Symlink::new(PathBuf::from("target"))), + ) + .unwrap(); + } + + // Test 1: Rename file in same directory + assert!(fs.rename("file.txt", "renamed.txt").await.is_ok()); + assert!(fs.exists("renamed.txt").await.unwrap()); + assert!(!fs.exists("file.txt").await.unwrap()); + + // Verify content is preserved + let mut buf = Vec::new(); + let mut reader = fs.read_file("renamed.txt", 0, 3).await.unwrap(); + tokio::io::copy(&mut reader, &mut buf).await.unwrap(); + assert_eq!(buf, vec![1, 2, 3]); + + // Test 2: Move file to different directory + assert!(fs.rename("renamed.txt", "dir1/moved.txt").await.is_ok()); + assert!(fs.exists("dir1/moved.txt").await.unwrap()); + assert!(!fs.exists("renamed.txt").await.unwrap()); + + // Test 3: Move nested file to root + assert!(fs.rename("dir2/nested.txt", "extracted.txt").await.is_ok()); + assert!(fs.exists("extracted.txt").await.unwrap()); + assert!(!fs.exists("dir2/nested.txt").await.unwrap()); + + // Test 4: Rename symlink + assert!(fs.rename("link", "newlink").await.is_ok()); + assert_eq!( + fs.read_symlink("newlink").await.unwrap(), + PathBuf::from("target") + ); + assert!(!fs.exists("link").await.unwrap()); + + // Test 5: Move between directories + assert!(fs.rename("dir1/moved.txt", "dir2/final.txt").await.is_ok()); + assert!(fs.exists("dir2/final.txt").await.unwrap()); + assert!(!fs.exists("dir1/moved.txt").await.unwrap()); + + // Error cases + + // Test 6: Source doesn't exist + assert!(matches!( + fs.rename("nonexistent", "dest.txt").await, + Err(VfsError::NotFound(_)) + )); + + // Test 7: Destination already exists + assert!(fs.create_file("existing.txt", false).await.is_ok()); + assert!(matches!( + fs.rename("extracted.txt", "existing.txt").await, + Err(VfsError::AlreadyExists(_)) + )); + + // Test 8: Source parent directory doesn't exist + assert!(matches!( + fs.rename("nonexistent_dir/file.txt", "dest.txt").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + + // Test 9: Destination parent directory doesn't exist + assert!(matches!( + fs.rename("extracted.txt", "nonexistent_dir/file.txt").await, + Err(VfsError::ParentDirectoryNotFound(_)) + )); + + // Test 10: Source parent is not a directory + assert!(fs.create_file("not_a_dir", false).await.is_ok()); + assert!(matches!( + fs.rename("not_a_dir/file.txt", "dest.txt").await, + Err(VfsError::NotADirectory(_)) + )); + + // Test 11: Destination parent is not a directory + assert!(matches!( + fs.rename("extracted.txt", "not_a_dir/file.txt").await, + Err(VfsError::NotADirectory(_)) + )); + + // Test 12: Invalid path components + for invalid_path in [".", "..", "/"] { + assert!(matches!( + fs.rename("extracted.txt", invalid_path).await, + Err(VfsError::InvalidPathComponent(_)) + )); + assert!(matches!( + fs.rename(invalid_path, "valid.txt").await, + Err(VfsError::InvalidPathComponent(_)) + )); + } + + // Test 13: Concurrent rename operations + let fs2 = fs.clone(); + let handle1 = + tokio::spawn(async move { fs.rename("extracted.txt", "concurrent1.txt").await }); + let handle2 = + tokio::spawn(async move { fs2.rename("extracted.txt", "concurrent2.txt").await }); + + let (result1, result2) = tokio::join!(handle1, handle2); + let results = vec![result1.unwrap(), result2.unwrap()]; + assert!(results.iter().filter(|r| r.is_ok()).count() == 1); + assert!( + results + .iter() + .filter(|r| matches!(r, Err(VfsError::NotFound(_)))) + .count() + == 1 + ); + } +} diff --git a/virtualfs/lib/implementations/mod.rs b/virtualfs/lib/implementations/mod.rs new file mode 100644 index 0000000..9c1f170 --- /dev/null +++ b/virtualfs/lib/implementations/mod.rs @@ -0,0 +1,11 @@ +mod memoryfs; +mod nativefs; +mod overlayfs; + +//-------------------------------------------------------------------------------------------------- +// Exports +//-------------------------------------------------------------------------------------------------- + +pub use memoryfs::*; +pub use nativefs::*; +pub use overlayfs::*; diff --git a/virtualfs/lib/implementations/nativefs.rs b/virtualfs/lib/implementations/nativefs.rs new file mode 100644 index 0000000..ce78153 --- /dev/null +++ b/virtualfs/lib/implementations/nativefs.rs @@ -0,0 +1,6 @@ +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// A filesystem implementation that uses the native filesystem. +pub struct NativeFileSystem {} diff --git a/virtualfs/lib/implementations/overlayfs.rs b/virtualfs/lib/implementations/overlayfs.rs new file mode 100644 index 0000000..050afce --- /dev/null +++ b/virtualfs/lib/implementations/overlayfs.rs @@ -0,0 +1,6 @@ +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// A filesystem implementation that combines multiple filesystems into a single logical filesystem. +pub struct OverlayFileSystem {} diff --git a/virtualfs/lib/lib.rs b/virtualfs/lib/lib.rs new file mode 100644 index 0000000..15433c7 --- /dev/null +++ b/virtualfs/lib/lib.rs @@ -0,0 +1,20 @@ +//! `virtualfs` is a library for virtual file systems. + +#![warn(missing_docs)] +#![allow(clippy::module_inception)] + +mod error; +mod filesystem; +mod implementations; +mod metadata; +mod segment; + +//-------------------------------------------------------------------------------------------------- +// Exports +//-------------------------------------------------------------------------------------------------- + +pub use error::*; +pub use filesystem::*; +pub use implementations::*; +pub use metadata::*; +pub use segment::*; diff --git a/virtualfs/lib/metadata.rs b/virtualfs/lib/metadata.rs new file mode 100644 index 0000000..1555dc3 --- /dev/null +++ b/virtualfs/lib/metadata.rs @@ -0,0 +1,962 @@ +use cfg_if::cfg_if; +use chrono::{DateTime, Utc}; +use getset::{CopyGetters, Getters}; + +//-------------------------------------------------------------------------------------------------- +// Constants +//-------------------------------------------------------------------------------------------------- + +cfg_if! { + if #[cfg(unix)] { + // File type bits + const S_IFMT: u32 = 0o170000; // bit mask for the file type bit field + const S_IFREG: u32 = 0o100000; // regular file + const S_IFDIR: u32 = 0o040000; // directory + const S_IFLNK: u32 = 0o120000; // symbolic link + + // Permission bits + const S_IRWXU: u32 = 0o700; // user (file owner) has read, write, and execute permission + const S_IRUSR: u32 = 0o400; // user has read permission + const S_IWUSR: u32 = 0o200; // user has write permission + const S_IXUSR: u32 = 0o100; // user has execute permission + + const S_IRWXG: u32 = 0o070; // group has read, write, and execute permission + const S_IRGRP: u32 = 0o040; // group has read permission + const S_IWGRP: u32 = 0o020; // group has write permission + const S_IXGRP: u32 = 0o010; // group has execute permission + + const S_IRWXO: u32 = 0o007; // others have read, write, and execute permission + const S_IROTH: u32 = 0o004; // others have read permission + const S_IWOTH: u32 = 0o002; // others have write permission + const S_IXOTH: u32 = 0o001; // others have execute permission + + // Permission mask + const S_IPERM: u32 = 0o777; // mask for permission bits + + // Combined permission constants for user, group, and other + const USER_RW: u32 = S_IRUSR | S_IWUSR; + const USER_RX: u32 = S_IRUSR | S_IXUSR; + const USER_WX: u32 = S_IWUSR | S_IXUSR; + + const GROUP_RW: u32 = S_IRGRP | S_IWGRP; + const GROUP_RX: u32 = S_IRGRP | S_IXGRP; + const GROUP_WX: u32 = S_IWGRP | S_IXGRP; + + const OTHER_RW: u32 = S_IROTH | S_IWOTH; + const OTHER_RX: u32 = S_IROTH | S_IXOTH; + const OTHER_WX: u32 = S_IWOTH | S_IXOTH; + } +} + +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// Metadata for a file or directory in the virtual filesystem. +/// +/// This struct holds standard Unix-like metadata including: +/// - File mode (type and permissions) +/// - File size +/// - Creation timestamp +/// - Last modification timestamp +#[derive(Debug, Clone, CopyGetters, Getters, PartialEq, Eq)] +pub struct Metadata { + /// The mode of the file, combining file type and permissions + #[cfg(unix)] + #[getset(get = "pub with_prefix")] + mode: Mode, + + #[cfg(not(unix))] + #[getset(get = "pub with_prefix")] + entity_type: EntityType, + + /// Size of the file in bytes + #[getset(get_copy = "pub with_prefix")] + size: u64, + + /// When the file was created + #[getset(get = "pub with_prefix")] + created_at: DateTime, + + /// When the file was last modified + #[getset(get = "pub with_prefix")] + modified_at: DateTime, +} + +cfg_if! { + if #[cfg(unix)] { + /// A Unix-style file mode that combines file type and permission bits. + /// + /// The mode is represented as a 32-bit integer with the following bit layout: + /// ```text + /// Bits Description + /// ---- ----------- + /// 15-12 File type (S_IFMT) + /// - Regular file (S_IFREG) 0o100000 + /// - Directory (S_IFDIR) 0o040000 + /// - Symbolic link (S_IFLNK) 0o120000 + /// + /// 8-6 User permissions + /// - Read (S_IRUSR) 0o400 + /// - Write (S_IWUSR) 0o200 + /// - Execute (S_IXUSR) 0o100 + /// + /// 5-3 Group permissions + /// - Read (S_IRGRP) 0o040 + /// - Write (S_IWGRP) 0o020 + /// - Execute (S_IXGRP) 0o010 + /// + /// 2-0 Other permissions + /// - Read (S_IROTH) 0o004 + /// - Write (S_IWOTH) 0o002 + /// - Execute (S_IXOTH) 0o001 + /// ``` + /// + /// This struct provides methods to get and set both the file type and permission bits, + /// maintaining compatibility with standard Unix file mode conventions. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub struct Mode(u32); + + /// The type of a file in the filesystem. + #[repr(u32)] + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum ModeType { + /// Regular file + File = 0o100000, + + /// Directory + Directory = 0o040000, + + /// Symbolic link + Symlink = 0o120000, + } + + /// Unix-style user permission flags (bits 8-6) + /// + /// Bit layout (bits 8-6): + /// ```text + /// 8 7 6 + /// r w x + /// ``` + #[repr(u32)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum User { + /// Read permission + R = S_IRUSR, + + /// Write permission + W = S_IWUSR, + + /// Execute permission + X = S_IXUSR, + + /// Read + Write + RW = S_IRUSR | S_IWUSR, + + /// Read + Execute + RX = S_IRUSR | S_IXUSR, + + /// Write + Execute + WX = S_IWUSR | S_IXUSR, + + /// Read + Write + Execute + RWX = S_IRWXU, + + /// No permissions + None = 0, + } + + /// Unix-style group permission flags (bits 5-3) + /// + /// Bit layout (bits 5-3): + /// ```text + /// 5 4 3 + /// r w x + /// ``` + #[repr(u32)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Group { + /// Read permission + R = S_IRGRP, + + /// Write permission + W = S_IWGRP, + + /// Execute permission + X = S_IXGRP, + + /// Read + Write + RW = S_IRGRP | S_IWGRP, + + /// Read + Execute + RX = S_IRGRP | S_IXGRP, + + /// Write + Execute + WX = S_IWGRP | S_IXGRP, + + /// Read + Write + Execute + RWX = S_IRWXG, + + /// No permissions + None = 0, + } + + /// Unix-style other permission flags (bits 2-0) + /// + /// Bit layout (bits 2-0): + /// ```text + /// 2 1 0 + /// r w x + /// ``` + #[repr(u32)] + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum Other { + /// Read permission + R = S_IROTH, + + /// Write permission + W = S_IWOTH, + + /// Execute permission + X = S_IXOTH, + + /// Read + Write + RW = S_IROTH | S_IWOTH, + + /// Read + Execute + RX = S_IROTH | S_IXOTH, + + /// Write + Execute + WX = S_IWOTH | S_IXOTH, + + /// Read + Write + Execute + RWX = S_IRWXO, + + /// No permissions + None = 0, + } + + /// A structured representation of Unix-style permission bits. + /// + /// This struct provides a type-safe way to work with Unix permission bits by breaking them + /// into their user, group, and other components. Each component is represented by its + /// respective enum (`User`, `Group`, `Other`) which ensures valid permission combinations. + /// + /// The permissions can be combined using the bitwise OR operator (`|`). For example: + /// ```rust + /// use virtualfs::{User, Group, Other}; + /// + /// // Create permissions: rw-r--r-- (0o644) + /// let perms = User::RW | Group::R | Other::R; + /// + /// // Create permissions: rwxr-x--- (0o750) + /// let perms = User::RWX | Group::RX | Other::None; + /// ``` + /// + /// When converted to a mode, the permission bits occupy the lower 9 bits of the mode value, + /// maintaining compatibility with the standard Unix permission layout: + /// ```text + /// user group other + /// rwx rwx rwx + /// ``` + #[derive(Debug, Clone, CopyGetters, Getters, PartialEq, Eq)] + pub struct ModePerms { + user: User, + group: Group, + other: Other, + } + } + else { + /// The type of an entity in the filesystem. + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum EntityType { + /// Regular file + File, + + /// Directory + Directory, + + /// Symbolic link + Symlink, + } + } +} + +//-------------------------------------------------------------------------------------------------- +// Methods +//-------------------------------------------------------------------------------------------------- + +impl Metadata { + /// Creates a new Metadata instance with default values. + /// + /// Default values are: + /// - mode: 0 (no permissions) + /// - size: 0 bytes + /// - created_at: current UTC time + /// - modified_at: current UTC time + /// + /// ## Examples + /// ```rust + /// use virtualfs::{Metadata, ModeType}; + /// + /// let metadata = Metadata::new(ModeType::File); + /// assert_eq!(metadata.get_size(), 0); + /// ``` + pub fn new( + #[cfg(unix)] entity_type: ModeType, + #[cfg(not(unix))] entity_type: EntityType, + ) -> Self { + Self { + #[cfg(unix)] + mode: Mode::new(entity_type), + #[cfg(not(unix))] + entity_type, + size: 0, + created_at: Utc::now(), + modified_at: Utc::now(), + } + } + + /// Sets the file type portion of the mode. + /// + /// This method: + /// 1. Clears the existing file type bits + /// 2. Sets the new file type bits based on the provided type + /// + /// The file type is stored in the high bits of the mode (bits 12-15). + #[cfg(unix)] + pub fn set_type(&mut self, entity_type: ModeType) { + self.mode.set_type(entity_type); + } + + /// Gets the file type from the mode. + /// + /// Returns: + /// - `Some(ModeType)` if the file type bits represent a valid type + /// - `None` if the file type bits don't match any known type + #[cfg(unix)] + pub fn get_type(&self) -> Option { + self.mode.get_type() + } + + /// Sets the permission bits of the mode. + /// + /// This method: + /// 1. Clears the existing permission bits (bits 0-8) + /// 2. Sets the new permission bits from the provided permissions + /// + /// ## Examples + /// ```rust + /// use virtualfs::{Metadata, ModeType, User, Group, Other}; + /// + /// let mut metadata = Metadata::new(ModeType::File); + /// metadata.set_permissions(User::RW | Group::R | Other::R); + /// ``` + #[cfg(unix)] + pub fn set_permissions(&mut self, permissions: impl Into) { + self.mode.set_permissions(permissions); + } + + /// Gets the current permissions from the mode + #[cfg(unix)] + pub fn get_permissions(&self) -> ModePerms { + self.mode.get_permissions() + } + + /// Gets the file type of the file. + #[cfg(not(unix))] + pub fn get_entity_type(&self) -> &EntityType { + &self.entity_type + } + + /// Sets the file type of the file. + /// + /// ## Examples + /// ```rust + /// use virtualfs::{Metadata, ModeType}; + /// + /// let mut metadata = Metadata::new(ModeType::File); + /// metadata.set_entity_type(ModeType::Directory); + /// assert_eq!(metadata.get_entity_type(), &ModeType::Directory); + /// ``` + #[cfg(not(unix))] + pub fn set_entity_type(&mut self, entity_type: EntityType) { + self.entity_type = entity_type; + } + + /// Sets the size of the file. + /// + /// ## Examples + /// ```rust + /// use virtualfs::{Metadata, ModeType}; + /// + /// let mut metadata = Metadata::new(ModeType::File); + /// metadata.set_size(100); + /// assert_eq!(metadata.get_size(), 100); + /// ``` + pub fn set_size(&mut self, size: u64) { + self.size = size; + } +} + +cfg_if! { + if #[cfg(unix)] { + impl Mode { + /// Creates a new Mode with no permissions + pub fn new(entity_type: ModeType) -> Self { + let perms: u32 = ModePerms::default().into(); + Self((entity_type as u32) | perms) + } + + /// Gets the file type portion of the mode + pub fn get_type(&self) -> Option { + match self.0 & S_IFMT { + S_IFREG => Some(ModeType::File), + S_IFDIR => Some(ModeType::Directory), + S_IFLNK => Some(ModeType::Symlink), + _ => None, + } + } + + /// Sets the file type portion of the mode + pub fn set_type(&mut self, entity_type: ModeType) { + // Clear the file type bits + self.0 &= !S_IFMT; + // Set the new file type bits + self.0 |= entity_type as u32; + } + + /// Gets the permission portion of the mode + pub fn get_permissions(&self) -> ModePerms { + let mode = self.0 & S_IPERM; + ModePerms { + user: match mode & S_IRWXU { + S_IRWXU => User::RWX, + USER_RW => User::RW, + USER_RX => User::RX, + USER_WX => User::WX, + S_IRUSR => User::R, + S_IWUSR => User::W, + S_IXUSR => User::X, + _ => User::None, + }, + group: match mode & S_IRWXG { + S_IRWXG => Group::RWX, + GROUP_RW => Group::RW, + GROUP_RX => Group::RX, + GROUP_WX => Group::WX, + S_IRGRP => Group::R, + S_IWGRP => Group::W, + S_IXGRP => Group::X, + _ => Group::None, + }, + other: match mode & S_IRWXO { + S_IRWXO => Other::RWX, + OTHER_RW => Other::RW, + OTHER_RX => Other::RX, + OTHER_WX => Other::WX, + S_IROTH => Other::R, + S_IWOTH => Other::W, + S_IXOTH => Other::X, + _ => Other::None, + }, + } + } + + /// Sets the permission portion of the mode + pub fn set_permissions(&mut self, perms: impl Into) { + let perms = perms.into(); + // Clear the permission bits + self.0 &= !S_IPERM; + // Set the new permission bits + self.0 |= u32::from(perms); + } + } + } +} + +//-------------------------------------------------------------------------------------------------- +// Trait Implementations +//-------------------------------------------------------------------------------------------------- + +cfg_if! { + if #[cfg(unix)] { + impl From for ModePerms { + fn from(user: User) -> Self { + ModePerms { + user, + group: Group::None, + other: Other::None, + } + } + } + + impl From for ModePerms { + fn from(group: Group) -> Self { + ModePerms { + user: User::None, + group, + other: Other::None, + } + } + } + + impl From for ModePerms { + fn from(other: Other) -> Self { + ModePerms { + user: User::None, + group: Group::None, + other, + } + } + } + + impl From for u32 { + fn from(permissions: ModePerms) -> Self { + permissions.user as u32 | permissions.group as u32 | permissions.other as u32 + } + } + + impl std::ops::BitOr for User { + type Output = ModePerms; + + fn bitor(self, rhs: Group) -> Self::Output { + ModePerms { + user: self, + group: rhs, + other: Other::None, + } + } + } + + impl std::ops::BitOr for User { + type Output = ModePerms; + + fn bitor(self, rhs: Other) -> Self::Output { + ModePerms { + user: self, + group: Group::None, + other: rhs, + } + } + } + + impl std::ops::BitOr for Group { + type Output = ModePerms; + + fn bitor(self, rhs: Other) -> Self::Output { + ModePerms { + user: User::None, + group: self, + other: rhs, + } + } + } + + impl std::ops::BitOr for User { + type Output = ModePerms; + + fn bitor(self, rhs: ModePerms) -> Self::Output { + ModePerms { + user: self, + group: rhs.group, + other: rhs.other, + } + } + } + + impl std::ops::BitOr for Group { + type Output = ModePerms; + + fn bitor(self, rhs: ModePerms) -> Self::Output { + ModePerms { + user: rhs.user, + group: self, + other: rhs.other, + } + } + } + + impl std::ops::BitOr for Other { + type Output = ModePerms; + + fn bitor(self, rhs: ModePerms) -> Self::Output { + ModePerms { + user: rhs.user, + group: rhs.group, + other: self, + } + } + } + + impl std::ops::BitOr for ModePerms { + type Output = ModePerms; + + fn bitor(self, rhs: Group) -> Self::Output { + ModePerms { + user: self.user, + group: rhs, + other: self.other, + } + } + } + + impl std::ops::BitOr for ModePerms { + type Output = ModePerms; + + fn bitor(self, rhs: Other) -> Self::Output { + ModePerms { + user: self.user, + group: self.group, + other: rhs, + } + } + } + + impl std::ops::BitOr for ModePerms { + type Output = ModePerms; + + fn bitor(self, rhs: User) -> Self::Output { + ModePerms { + user: rhs, + group: self.group, + other: self.other, + } + } + } + + impl Default for ModePerms { + /// Creates a new ModePerms with default Unix permissions (0644). + /// + /// This sets the permissions to: + /// - User: read + write (rw-) + /// - Group: read-only (r--) + /// - Other: read-only (r--) + fn default() -> Self { + Self { + user: User::RW, + group: Group::R, + other: Other::R, + } + } + } + + impl From for Mode { + fn from(entity_type: ModeType) -> Self { + Self(entity_type as u32) + } + } + + impl From for Mode { + fn from(perms: ModePerms) -> Self { + Self(u32::from(perms)) + } + } + + impl std::ops::BitAnd for Mode { + type Output = u32; + + fn bitand(self, rhs: u32) -> Self::Output { + self.0 & rhs + } + } + + impl std::ops::BitAndAssign for Mode { + fn bitand_assign(&mut self, rhs: u32) { + self.0 &= rhs; + } + } + + impl std::ops::BitOrAssign for Mode { + fn bitor_assign(&mut self, rhs: u32) { + self.0 |= rhs; + } + } + + impl From for Mode { + fn from(mode: u32) -> Self { + Self(mode) + } + } + + impl From for u32 { + fn from(mode: Mode) -> Self { + mode.0 + } + } + + impl std::fmt::Display for ModeType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ModeType::File => write!(f, "-"), + ModeType::Directory => write!(f, "d"), + ModeType::Symlink => write!(f, "l"), + } + } + } + + impl std::fmt::Display for ModePerms { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // User permissions + write!( + f, + "{}{}{}", + if (self.user as u32 & S_IRUSR) != 0 { + "r" + } else { + "-" + }, + if (self.user as u32 & S_IWUSR) != 0 { + "w" + } else { + "-" + }, + if (self.user as u32 & S_IXUSR) != 0 { + "x" + } else { + "-" + } + )?; + + // Group permissions + write!( + f, + "{}{}{}", + if (self.group as u32 & S_IRGRP) != 0 { + "r" + } else { + "-" + }, + if (self.group as u32 & S_IWGRP) != 0 { + "w" + } else { + "-" + }, + if (self.group as u32 & S_IXGRP) != 0 { + "x" + } else { + "-" + } + )?; + + // Other permissions + write!( + f, + "{}{}{}", + if (self.other as u32 & S_IROTH) != 0 { + "r" + } else { + "-" + }, + if (self.other as u32 & S_IWOTH) != 0 { + "w" + } else { + "-" + }, + if (self.other as u32 & S_IXOTH) != 0 { + "x" + } else { + "-" + } + ) + } + } + + impl std::fmt::Display for Mode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // File type + match self.get_type() { + Some(entity_type) => write!(f, "{}", entity_type)?, + None => write!(f, "?")?, + } + + // Permissions + write!(f, "{}", self.get_permissions()) + } + } + } +} + +//-------------------------------------------------------------------------------------------------- +// Tests +//-------------------------------------------------------------------------------------------------- + +#[cfg(all(test, unix))] +mod tests { + use super::*; + + #[test] + fn test_metadata_new() { + let metadata = Metadata::new(ModeType::File); + assert_eq!(metadata.get_mode(), &Mode::new(ModeType::File)); + assert_eq!(metadata.get_size(), 0); + } + + #[test] + fn test_entity_type() { + let mut metadata = Metadata::new(ModeType::File); + + // Test setting and getting file type + metadata.set_type(ModeType::File); + assert!(matches!(metadata.get_type(), Some(ModeType::File))); + + metadata.set_type(ModeType::Directory); + assert!(matches!(metadata.get_type(), Some(ModeType::Directory))); + + metadata.set_type(ModeType::Symlink); + assert!(matches!(metadata.get_type(), Some(ModeType::Symlink))); + + // Test that file type bits are preserved when setting permissions + metadata.set_type(ModeType::File); + metadata.set_permissions(User::RWX | Group::RWX | Other::RWX); + assert!(matches!(metadata.get_type(), Some(ModeType::File))); + } + + #[test] + fn test_basic_permissions() { + let mut metadata = Metadata::new(ModeType::File); + + // Test individual permission types + metadata.set_permissions(User::R); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o400); + + metadata.set_permissions(Group::W); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o020); + + metadata.set_permissions(Other::X); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o001); + } + + #[test] + fn test_combined_permissions() { + let mut metadata = Metadata::new(ModeType::File); + + // Test combined permissions within same group + metadata.set_permissions(User::RW); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o600); + + metadata.set_permissions(Group::RX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o050); + + metadata.set_permissions(Other::WX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o003); + + // Test all permissions in a group + metadata.set_permissions(User::RWX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o700); + } + + #[test] + fn test_permission_combinations() { + let mut metadata = Metadata::new(ModeType::File); + + // Test combining different permission groups + metadata.set_permissions(User::RW | Group::R); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o640); + + metadata.set_permissions(User::RWX | Group::RX | Other::R); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o754); + + // Test chaining operations + let perms = User::RW | Group::R | Other::X; + metadata.set_permissions(perms); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o641); + } + + #[test] + fn test_permission_updates() { + // Test updating existing permissions + let base_perms = User::RW | Group::R; + let updated = base_perms | Other::X; + + let mut metadata = Metadata::new(ModeType::File); + metadata.set_permissions(updated); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o641); + + // Test that updating one group doesn't affect others + let perms = User::RW | Group::R; + let with_other = perms | Other::X; + let with_updated_group = with_other | Group::RWX; + + metadata.set_permissions(with_updated_group); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o671); + } + + #[test] + fn test_none_permissions() { + let mut metadata = Metadata::new(ModeType::File); + + // Test that None permissions don't set any bits + metadata.set_permissions(User::None); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0); + + // Test that None permissions clear existing bits for that group + metadata.set_permissions(User::RWX | Group::RWX | Other::RWX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o777); + + metadata.set_permissions(User::None | Group::RWX | Other::RWX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o077); + } + + #[test] + fn test_permission_clearing() { + let mut metadata = Metadata::new(ModeType::File); + + // Set all permissions + metadata.set_permissions(User::RWX | Group::RWX | Other::RWX); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o777); + + // Clear permissions by setting new ones + metadata.set_permissions(User::R | Group::R | Other::R); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o444); + + // Verify that setting new permissions clears old ones + metadata.set_permissions(User::W); + assert_eq!(u32::from(*metadata.get_mode()) & 0o777, 0o200); + } + + #[test] + fn test_mode_type_display() { + assert_eq!(ModeType::File.to_string(), "-"); + assert_eq!(ModeType::Directory.to_string(), "d"); + assert_eq!(ModeType::Symlink.to_string(), "l"); + } + + #[test] + fn test_mode_perms_display() { + // Test common permission patterns + assert_eq!((User::RW | Group::R | Other::R).to_string(), "rw-r--r--"); + assert_eq!( + (User::RWX | Group::RX | Other::None).to_string(), + "rwxr-x---" + ); + assert_eq!( + (User::RWX | Group::RWX | Other::RWX).to_string(), + "rwxrwxrwx" + ); + assert_eq!( + (User::None | Group::None | Other::None).to_string(), + "---------" + ); + } + + #[test] + fn test_mode_display() { + let mut mode = Mode::new(ModeType::File); + + // Regular file with rw-r--r-- permissions + mode.set_type(ModeType::File); + mode.set_permissions(User::RW | Group::R | Other::R); + assert_eq!(mode.to_string(), "-rw-r--r--"); + + // Directory with rwxr-x--- permissions + mode.set_type(ModeType::Directory); + mode.set_permissions(User::RWX | Group::RX | Other::None); + assert_eq!(mode.to_string(), "drwxr-x---"); + + // Symlink with rwxrwxrwx permissions + mode.set_type(ModeType::Symlink); + mode.set_permissions(User::RWX | Group::RWX | Other::RWX); + assert_eq!(mode.to_string(), "lrwxrwxrwx"); + } +} diff --git a/virtualfs/lib/segment.rs b/virtualfs/lib/segment.rs new file mode 100644 index 0000000..e6cdced --- /dev/null +++ b/virtualfs/lib/segment.rs @@ -0,0 +1,263 @@ +use std::{ + ffi::{OsStr, OsString}, + fmt::{self, Display}, + path::{Component, Path, PathBuf}, + str::FromStr, +}; + +use crate::VfsError; + +//-------------------------------------------------------------------------------------------------- +// Types +//-------------------------------------------------------------------------------------------------- + +/// Represents a single segment of a path. +/// +/// This struct provides a way to represent and manipulate individual components +/// of a path, ensuring they are valid path segments. +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PathSegment(OsString); + +//-------------------------------------------------------------------------------------------------- +// Methods +//-------------------------------------------------------------------------------------------------- + +impl PathSegment { + /// Returns the OS string representation of the segment. + pub fn as_os_str(&self) -> &OsStr { + &self.0 + } + + /// Returns the bytes representation of the segment. + pub fn as_bytes(&self) -> &[u8] { + self.as_os_str().as_encoded_bytes() + } + + /// Returns the length of the segment in bytes. + pub fn len(&self) -> usize { + self.as_bytes().len() + } + + /// Returns `true` if the segment is empty. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +//-------------------------------------------------------------------------------------------------- +// Trait Implementations +//-------------------------------------------------------------------------------------------------- + +impl FromStr for PathSegment { + type Err = VfsError; + + fn from_str(s: &str) -> Result { + PathSegment::try_from(s) + } +} + +impl Display for PathSegment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string_lossy()) + } +} + +impl TryFrom<&str> for PathSegment { + type Error = VfsError; + + fn try_from(value: &str) -> Result { + if value.is_empty() { + return Err(VfsError::EmptyPathSegment); + } + + #[cfg(unix)] + { + if value.contains('/') { + return Err(VfsError::InvalidPathComponent(value.to_string())); + } + } + + #[cfg(windows)] + { + if value.contains('/') || value.contains('\\') { + return Err(VfsError::InvalidPathComponent(value.to_string())); + } + } + + // At this point the string does not contain any separator characters + let mut components = Path::new(value).components(); + let component = components + .next() + .ok_or_else(|| VfsError::InvalidPathComponent(value.to_string()))?; + + // Ensure there are no additional components + if components.next().is_some() { + return Err(VfsError::InvalidPathComponent(value.to_string())); + } + + match component { + Component::Normal(comp) => Ok(PathSegment(comp.to_os_string())), + _ => Err(VfsError::InvalidPathComponent(value.to_string())), + } + } +} + +impl<'a> TryFrom> for PathSegment { + type Error = VfsError; + + fn try_from(component: Component<'a>) -> Result { + PathSegment::try_from(&component) + } +} + +impl<'a> TryFrom<&Component<'a>> for PathSegment { + type Error = VfsError; + + fn try_from(component: &Component<'a>) -> Result { + match component { + Component::Normal(component) => Ok(PathSegment(component.to_os_string())), + _ => Err(VfsError::InvalidPathComponent( + component.as_os_str().to_string_lossy().into_owned(), + )), + } + } +} + +impl<'a> From<&'a PathSegment> for Component<'a> { + fn from(segment: &'a PathSegment) -> Self { + Component::Normal(segment.as_os_str()) + } +} + +impl From for PathBuf { + #[inline] + fn from(segment: PathSegment) -> Self { + PathBuf::from(segment.0) + } +} + +impl AsRef<[u8]> for PathSegment { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + +impl AsRef for PathSegment { + #[inline] + fn as_ref(&self) -> &OsStr { + self.as_os_str() + } +} + +impl AsRef for PathSegment { + #[inline] + fn as_ref(&self) -> &Path { + Path::new(self.as_os_str()) + } +} + +//-------------------------------------------------------------------------------------------------- +// Tests +//-------------------------------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_segment_as_os_str() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!(segment.as_os_str(), OsStr::new("example")); + } + + #[test] + fn test_segment_as_bytes() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!(segment.as_bytes(), b"example"); + } + + #[test] + fn test_segment_len() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!(segment.len(), 7); + } + + #[test] + fn test_segment_display() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!(format!("{}", segment), "example"); + } + + #[test] + fn test_segment_try_from_str() { + assert!(PathSegment::try_from("example").is_ok()); + assert!(PathSegment::from_str("example").is_ok()); + assert!("example".parse::().is_ok()); + + // Negative cases + assert!(PathSegment::from_str("").is_err()); + assert!(PathSegment::from_str(".").is_err()); + assert!(PathSegment::from_str("..").is_err()); + assert!(".".parse::().is_err()); + assert!("..".parse::().is_err()); + assert!("".parse::().is_err()); + assert!(PathSegment::try_from(".").is_err()); + assert!(PathSegment::try_from("..").is_err()); + assert!(PathSegment::try_from("/").is_err()); + assert!(PathSegment::try_from("").is_err()); + } + + #[test] + fn test_segment_from_path_segment_to_component() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!( + Component::from(&segment), + Component::Normal(OsStr::new("example")) + ); + } + + #[test] + fn test_segment_from_path_segment_to_path_buf() { + let segment = PathSegment::from_str("example").unwrap(); + assert_eq!(PathBuf::from(segment), PathBuf::from("example")); + } + + #[test] + fn test_segment_normal_with_special_characters() { + assert!(PathSegment::try_from("file.txt").is_ok()); + assert!(PathSegment::try_from("file-name").is_ok()); + assert!(PathSegment::try_from("file_name").is_ok()); + assert!(PathSegment::try_from("file name").is_ok()); + assert!(PathSegment::try_from("file:name").is_ok()); + assert!(PathSegment::try_from("file*name").is_ok()); + assert!(PathSegment::try_from("file?name").is_ok()); + } + + #[test] + #[cfg(unix)] + fn test_segment_with_unix_separator() { + // On Unix systems, forward slash is the main separator + assert!(PathSegment::try_from("file/name").is_err()); + assert!(PathSegment::try_from("/").is_err()); + assert!(PathSegment::try_from("///").is_err()); + assert!(PathSegment::try_from("name/").is_err()); + assert!(PathSegment::try_from("/name").is_err()); + } + + #[test] + #[cfg(windows)] + fn test_segment_with_windows_separators() { + // On Windows, both forward slash and backslash are separators + assert!(PathSegment::try_from("file\\name").is_err()); + assert!(PathSegment::try_from("file/name").is_err()); + assert!(PathSegment::try_from("\\").is_err()); + assert!(PathSegment::try_from("/").is_err()); + assert!(PathSegment::try_from("\\\\\\").is_err()); + assert!(PathSegment::try_from("///").is_err()); + assert!(PathSegment::try_from("name\\").is_err()); + assert!(PathSegment::try_from("name/").is_err()); + assert!(PathSegment::try_from("\\name").is_err()); + assert!(PathSegment::try_from("/name").is_err()); + } +}