diff --git a/Cargo.toml b/Cargo.toml index ff3930c..659ae66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kxio" -version = "1.1.2" +version = "1.2.0" edition = "2021" authors = ["Paul Campbell "] description = "Provides injectable Filesystem and Network resources to make code more testable" @@ -33,8 +33,12 @@ thiserror = "1.0" tempfile = "3.10" path-clean = "1.0" -# error handling -derive_more = { version = "1.0.0-beta.6", features = ["from", "display"] } +# boilerplate +derive_more = { version = "1.0.0-beta.6", features = [ + "from", + "display", + "constructor", +] } [dev-dependencies] # testing diff --git a/src/fs/dir_item.rs b/src/fs/dir_item.rs new file mode 100644 index 0000000..fdd0eac --- /dev/null +++ b/src/fs/dir_item.rs @@ -0,0 +1,37 @@ +use std::{ + fs::{DirEntry, ReadDir}, + path::PathBuf, +}; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum DirItem { + File(PathBuf), + Dir(PathBuf), + SymLink(PathBuf), + Fifo(PathBuf), + Unsupported(PathBuf), +} + +#[derive(Debug, derive_more::Constructor)] +pub struct DirItemIterator(ReadDir); +impl Iterator for DirItemIterator { + type Item = super::Result; + + fn next(&mut self) -> Option { + self.0.next().map(map_dir_item) + } +} + +fn map_dir_item(item: std::io::Result) -> super::Result { + let item = item?; + let file_type = item.file_type()?; + if file_type.is_dir() { + Ok(DirItem::Dir(item.path())) + } else if file_type.is_file() { + Ok(DirItem::File(item.path())) + } else if file_type.is_symlink() { + Ok(DirItem::SymLink(item.path())) + } else { + Ok(DirItem::Unsupported(item.path())) + } +} diff --git a/src/fs/like.rs b/src/fs/like.rs index 9a6c57c..19ae0d9 100644 --- a/src/fs/like.rs +++ b/src/fs/like.rs @@ -1,3 +1,5 @@ +use crate::fs::DirItem; + use super::Result; use std::path::{Path, PathBuf}; @@ -8,6 +10,9 @@ pub trait FileSystemLike { fn dir_create(&self, path: &Path) -> Result<()>; fn dir_create_all(&self, path: &Path) -> Result<()>; + /// Reads the items in a directory and returns them as an iterator. + fn dir_read(&self, path: &Path) -> Result>>>; + fn file_read_to_string(&self, path: &Path) -> Result; fn file_write(&self, path: &Path, contents: &str) -> Result<()>; diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 16692c5..e2b369b 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -8,6 +8,10 @@ mod like; mod real; mod temp; +mod dir_item; +pub use dir_item::DirItem; +pub use dir_item::DirItemIterator; + #[derive(Debug, From, derive_more::Display)] pub enum Error { Io(std::io::Error), @@ -17,6 +21,11 @@ pub enum Error { base: PathBuf, path: PathBuf, }, + + #[display("Path must be a directory: {path:?}")] + NotADirectory { + path: PathBuf, + }, } impl std::error::Error for Error {} diff --git a/src/fs/real.rs b/src/fs/real.rs index cabd979..52c8172 100644 --- a/src/fs/real.rs +++ b/src/fs/real.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use crate::fs::{DirItem, DirItemIterator}; + pub const fn new(base: PathBuf) -> RealFileSystem { RealFileSystem { base } } @@ -24,6 +26,20 @@ impl super::FileSystemLike for RealFileSystem { std::fs::create_dir_all(path).map_err(Into::into) } + fn dir_read( + &self, + path: &Path, + ) -> super::Result>>> { + self.validate(path)?; + if !self.path_is_dir(path)? { + return Err(super::Error::NotADirectory { + path: path.to_path_buf(), + }); + } + let read_dir = std::fs::read_dir(path)?; + Ok(Box::new(DirItemIterator::new(read_dir))) + } + fn file_read_to_string(&self, path: &Path) -> super::Result { self.validate(path)?; std::fs::read_to_string(path).map_err(Into::into) diff --git a/src/tests/fs.rs b/src/tests/fs.rs index b8c5970..dd2175e 100644 --- a/src/tests/fs.rs +++ b/src/tests/fs.rs @@ -107,6 +107,57 @@ mod dir_create_all { } } +mod dir_dir_read { + use crate::fs::DirItem; + + use super::*; + + #[test] + fn should_return_dir_items() -> TestResult { + let fs = fs::temp()?; + let file1 = fs.base().join("file-1"); + let dir = fs.base().join("dir"); + let file2 = dir.join("file-2"); + fs.file_write(&file1, "file-1")?; + fs.dir_create(&dir)?; + fs.file_write(&file2, "file-2")?; + + let items = fs + .dir_read(fs.base())? + .filter_map(|i| i.ok()) + .collect::>(); + + assert_eq!(items.len(), 2); + assert!(items.contains(&DirItem::File(file1))); + assert!(items.contains(&DirItem::Dir(dir))); + + Ok(()) + } + #[test] + fn should_fail_on_not_a_dir() -> TestResult { + let fs = fs::temp()?; + let path = fs.base().join("file"); + fs.file_write(&path, "contents")?; + + let_assert!(Err(fs::Error::NotADirectory { path: _path }) = fs.dir_read(&path)); + + Ok(()) + } + #[test] + fn should_fail_on_path_traversal() -> TestResult { + let fs = fs::temp()?; + let path = fs.base().join("..").join("foo"); + let_assert!( + Err(fs::Error::PathTraversal { + base: _base, + path: _path + }) = fs.dir_read(&path) + ); + + Ok(()) + } +} + mod path_exists { use super::*; #[test]