From 9fbc2f79aabd5dd2f7dcd401e4bb03880d54b8f4 Mon Sep 17 00:00:00 2001 From: Paul Campbell Date: Sat, 27 Apr 2024 20:31:23 +0100 Subject: [PATCH] feat(fs): new fs module to replace filesystem --- Cargo.toml | 3 +++ README.md | 39 +++++++++++++++++------------------ src/filesystem.rs | 1 + src/fs/mod.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++ src/fs/real.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++ src/fs/temp.rs | 47 ++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 ++++ src/tests/fs.rs | 20 ++++++++++++++++++ src/tests/mod.rs | 4 ++++ 9 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 src/fs/mod.rs create mode 100644 src/fs/real.rs create mode 100644 src/fs/temp.rs create mode 100644 src/tests/fs.rs diff --git a/Cargo.toml b/Cargo.toml index 4d825fb..3cbec2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,9 @@ thiserror = "1.0" # fs tempfile = "3.10" +# error handling +derive_more = { version = "1.0.0-beta.6", features = ["from", "display"] } + [dev-dependencies] # testing assert2 = "0.3" diff --git a/README.md b/README.md index b3145cf..cf59be9 100644 --- a/README.md +++ b/README.md @@ -13,26 +13,25 @@ There are two FileSystem implementation: [filesystem] and [fs]. #### std::fs alternatives -| To Do | [std::fs] | [kxio::fs] | | -| ----- | ---------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| [ ] | canonicalize | path::canonicalize | Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved. | -| [ ] | copy | file::copy | Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. | -| [ ] | create_dir | dir::create | Creates a new, empty directory at the provided path | -| [ ] | create_dir_all | dir::create_all | Recursively create a directory and all of its parent components if they are missing. | -| [ ] | hard_link | link::create | Creates a new hard link on the filesystem. | -| [ ] | metadata | path::metadata | Given a path, query the file system to get information about a file, directory, etc. | -| [ ] | read | file::read | Read the entire contents of a file into a bytes vector. | -| [ ] | read_dir | dir::read | Returns an iterator over the entries within a directory. | -| [ ] | read_link | link::read | Reads a symbolic link, returning the file that the link points to. | -| [ ] | read_to_string | file::read_to_string | Read the entire contents of a file into a string. | -| [ ] | remove_dir | dir::remove | Removes an empty directory. | -| [ ] | remove_dir_all | dir::remove_all | Removes a directory at this path, after removing all its contents. Use carefully! | -| [ ] | remove_file | file::remove | Removes a file from the filesystem. | -| [ ] | rename | path::rename | Rename a file or directory to a new name, replacing the original file if to already exists. | -| [ ] | set_permissions | path::set_permissions | Changes the permissions found on a file or a directory. | -| [ ] | symlink_metadata | link::metadata | Query the metadata about a file without following symlinks. | -| [ ] | write | file::write | Write a slice as the entire contents of a file. | -| [ ] | try_exists | path::exists | Returns Ok(true) if the path points at an existing entity. | +| To Do | [std::fs] | [kxio::fs::FileSystem] | | +| ----- | ---------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | +| [ ] | canonicalize | path_canonicalize | Returns the canonical, absolute form of a path with all intermediate components normalized and symbolic links resolved. | +| [ ] | copy | file_copy | Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. | +| [ ] | create_dir | dir_create | Creates a new, empty directory at the provided path | +| [ ] | create_dir_all | dir_create_all | Recursively create a directory and all of its parent components if they are missing. | +| [ ] | hard_link | link_create | Creates a new hard link on the filesystem. | +| [ ] | metadata | path_metadata | Given a path, query the file system to get information about a file, directory, etc. | +| [ ] | read | file_read | Read the entire contents of a file into a bytes vector. | +| [ ] | read_dir | dir_read | Returns an iterator over the entries within a directory. | +| [ ] | read_link | link_read | Reads a symbolic link, returning the file that the link points to. | +| [x] | read_to_string | file_read_to_string | Read the entire contents of a file into a string. | +| [ ] | remove_dir | dir_remove | Removes an empty directory. | +| [ ] | remove_dir_all | dir_remove_all | Removes a directory at this path, after removing all its contents. Use carefully! | +| [ ] | remove_file | file_remove | Removes a file from the filesystem. | +| [ ] | rename | path_rename | Rename a file or directory to a new name, replacing the original file if to already exists. | +| [ ] | set_permissions | path_set_permissions | Changes the permissions found on a file or a directory. | +| [ ] | symlink_metadata | link_metadata | Query the metadata about a file without following symlinks. | +| [x] | write | file_write | Write a slice as the entire contents of a file. | ### Network diff --git a/src/filesystem.rs b/src/filesystem.rs index 3237579..3c90a69 100644 --- a/src/filesystem.rs +++ b/src/filesystem.rs @@ -16,6 +16,7 @@ pub fn temp() -> std::io::Result { } #[derive(Clone, Debug)] +#[deprecated(since = "1.1.0", note = "Use [kxio::fs::FileSystem] instead")] pub enum FileSystem { Real(RealFileSystem), Temp(TempFileSystem), diff --git a/src/fs/mod.rs b/src/fs/mod.rs new file mode 100644 index 0000000..dffe440 --- /dev/null +++ b/src/fs/mod.rs @@ -0,0 +1,50 @@ +use std::path::{Path, PathBuf}; + +use derive_more::From; + +mod real; +mod temp; + +#[derive(Debug, From, derive_more::Display)] +pub enum Error { + Io(std::io::Error), + + #[display("Path access attempted outside of base ({base:?}): {path:?}")] + PathTraversal { + base: PathBuf, + path: PathBuf, + }, +} +pub type Result = core::result::Result; + +pub const fn new(base: PathBuf) -> FileSystem { + FileSystem::Real(real::new(base)) +} + +pub fn temp() -> Result { + temp::new().map(FileSystem::Temp) +} + +pub enum FileSystem { + Real(real::RealFileSystem), + Temp(temp::TempFileSystem), +} + +impl std::ops::Deref for FileSystem { + type Target = dyn FileSystemLike; + + fn deref(&self) -> &Self::Target { + match self { + Self::Real(fs) => fs, + Self::Temp(fs) => fs, + } + } +} +pub trait FileSystemLike { + fn base(&self) -> &Path; + fn path_of(&self, path: PathBuf) -> Result; + fn file_write(&self, path: &Path, contents: &str) -> Result<()>; + fn file_read_to_string(&self, path: &Path) -> Result; + fn path_exists(&self, path: &Path) -> Result; + fn path_is_file(&self, path: &Path) -> Result; +} diff --git a/src/fs/real.rs b/src/fs/real.rs new file mode 100644 index 0000000..cece7a9 --- /dev/null +++ b/src/fs/real.rs @@ -0,0 +1,52 @@ +use std::path::{Path, PathBuf}; + +pub const fn new(base: PathBuf) -> RealFileSystem { + RealFileSystem { base } +} + +pub struct RealFileSystem { + base: PathBuf, +} + +impl super::FileSystemLike for RealFileSystem { + fn base(&self) -> &Path { + &self.base + } + fn path_of(&self, path: PathBuf) -> super::Result { + let path_of = self.base.as_path().join(path); + self.validate(&path_of)?; + Ok(path_of) + } + + fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> { + self.validate(path)?; + std::fs::write(path, contents).map_err(Into::into) + } + + fn file_read_to_string(&self, path: &Path) -> super::Result { + self.validate(path)?; + std::fs::read_to_string(path).map_err(Into::into) + } + + fn path_is_file(&self, path: &Path) -> super::Result { + self.validate(path)?; + Ok(path.is_file()) + } + + fn path_exists(&self, path: &Path) -> super::Result { + self.validate(path)?; + Ok(path.exists()) + } +} + +impl RealFileSystem { + fn validate(&self, path: &std::path::Path) -> super::Result<()> { + if !path.starts_with(&self.base) { + return Err(super::Error::PathTraversal { + base: self.base.clone(), + path: path.to_path_buf(), + }); + } + Ok(()) + } +} diff --git a/src/fs/temp.rs b/src/fs/temp.rs new file mode 100644 index 0000000..86f5ac6 --- /dev/null +++ b/src/fs/temp.rs @@ -0,0 +1,47 @@ +use std::{ + path::{Path, PathBuf}, + sync::{Arc, Mutex}, +}; + +use tempfile::TempDir; + +pub(super) fn new() -> super::Result { + let temp_dir = tempfile::tempdir()?; + let base = temp_dir.path().to_path_buf(); + let temp_dir = Arc::new(Mutex::new(temp_dir)); + let real = super::real::new(base); + + Ok(TempFileSystem { + real, + _temp_dir: temp_dir, + }) +} + +pub struct TempFileSystem { + real: super::real::RealFileSystem, + _temp_dir: Arc>, +} + +impl super::FileSystemLike for TempFileSystem { + fn base(&self) -> &Path { + self.real.base() + } + fn path_of(&self, path: PathBuf) -> super::Result { + self.real.path_of(path) + } + fn file_write(&self, path: &Path, contents: &str) -> super::Result<()> { + self.real.file_write(path, contents) + } + + fn file_read_to_string(&self, path: &Path) -> super::Result { + self.real.file_read_to_string(path) + } + + fn path_exists(&self, path: &Path) -> super::Result { + self.real.path_exists(path) + } + + fn path_is_file(&self, path: &Path) -> super::Result { + self.real.path_is_file(path) + } +} diff --git a/src/lib.rs b/src/lib.rs index b37925b..83c0cba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,9 @@ #[cfg(feature = "fs")] pub mod filesystem; + +#[cfg(feature = "fs")] +pub mod fs; + #[cfg(feature = "network")] pub mod network; diff --git a/src/tests/fs.rs b/src/tests/fs.rs new file mode 100644 index 0000000..498f11a --- /dev/null +++ b/src/tests/fs.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +use crate::fs; + +type TestResult = Result<(), crate::fs::Error>; + +#[test] +fn write_read_file_exists() -> TestResult { + let temp_fs = fs::temp()?; + let name: PathBuf = temp_fs.path_of("foo".into())?; + temp_fs.file_write(&name, "content")?; + let c = temp_fs.file_read_to_string(&name)?; + assert_eq!(c, "content"); + let exists = temp_fs.path_exists(&name)?; + assert!(exists); + let is_file = temp_fs.path_is_file(&name)?; + assert!(is_file); + + Ok(()) +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b8d834d..a4e08a4 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,4 +1,8 @@ #[cfg(feature = "fs")] pub mod filesystem; + +#[cfg(feature = "fs")] +pub mod fs; + #[cfg(feature = "network")] pub mod network;