Compare commits
8 commits
b3118173ad
...
c45a2c2273
Author | SHA1 | Date | |
---|---|---|---|
c45a2c2273 | |||
17221a06d8 | |||
a202de0c85 | |||
be4b924bf7 | |||
cca1aad696 | |||
420e1071a4 | |||
b2646ccab3 | |||
892899bfe2 |
16 changed files with 437 additions and 481 deletions
5
justfile
5
justfile
|
@ -1,3 +1,8 @@
|
||||||
|
build:
|
||||||
|
cargo build
|
||||||
|
cargo test
|
||||||
|
cargo clippy
|
||||||
|
|
||||||
install-hooks:
|
install-hooks:
|
||||||
@echo "Installing git hooks"
|
@echo "Installing git hooks"
|
||||||
git config core.hooksPath .git-hooks
|
git config core.hooksPath .git-hooks
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
#![allow(deprecated)]
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
ops::Deref,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
use tempfile::{tempdir, TempDir};
|
|
||||||
use tracing::{debug, info};
|
|
||||||
|
|
||||||
pub fn real(cwd: Option<PathBuf>) -> FileSystem {
|
|
||||||
let cwd = cwd.unwrap_or_default();
|
|
||||||
FileSystem::Real(RealFileSystem::new(cwd))
|
|
||||||
}
|
|
||||||
pub fn temp() -> std::io::Result<FileSystem> {
|
|
||||||
TempFileSystem::new().map(FileSystem::Temp)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
#[deprecated(since = "1.1.0", note = "Use [kxio::fs::FileSystem] instead")]
|
|
||||||
pub enum FileSystem {
|
|
||||||
Real(RealFileSystem),
|
|
||||||
Temp(TempFileSystem),
|
|
||||||
}
|
|
||||||
impl FileSystem {
|
|
||||||
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::real()] instead")]
|
|
||||||
pub fn new_real(cwd: Option<PathBuf>) -> Self {
|
|
||||||
real(cwd)
|
|
||||||
}
|
|
||||||
#[deprecated(since = "1.1.0", note = "Use [kxio::filesystem::temp()] instead")]
|
|
||||||
pub fn new_temp() -> std::io::Result<Self> {
|
|
||||||
temp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Deref for FileSystem {
|
|
||||||
type Target = dyn FileSystemLike;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
match self {
|
|
||||||
Self::Real(env) => env,
|
|
||||||
Self::Temp(env) => env,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait FileSystemLike: Sync + Send + std::fmt::Debug {
|
|
||||||
fn cwd(&self) -> &PathBuf;
|
|
||||||
|
|
||||||
fn in_cwd(&self, name: &str) -> PathBuf {
|
|
||||||
self.cwd().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_file(&self, file_name: &str, content: &str) -> std::io::Result<PathBuf> {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{LineWriter, Write};
|
|
||||||
|
|
||||||
let path = self.in_cwd(file_name);
|
|
||||||
debug!("writing to {:?}", path);
|
|
||||||
let file = File::create(path.clone())?;
|
|
||||||
let mut file = LineWriter::new(file);
|
|
||||||
file.write_all(content.as_bytes())?;
|
|
||||||
Ok(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn file_exists(&self, name: &PathBuf) -> bool {
|
|
||||||
use std::fs::File;
|
|
||||||
File::open(name).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file(&self, file_name: &str) -> std::io::Result<String> {
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::Read;
|
|
||||||
|
|
||||||
let path = self.in_cwd(file_name);
|
|
||||||
debug!("reading from {:?}", path);
|
|
||||||
let mut file = File::open(path)?;
|
|
||||||
let mut content = String::new();
|
|
||||||
file.read_to_string(&mut content)?;
|
|
||||||
Ok(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
pub struct RealFileSystem {
|
|
||||||
cwd: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TempFileSystem {
|
|
||||||
cwd: PathBuf,
|
|
||||||
|
|
||||||
// Handle to the temporary directory
|
|
||||||
// When this handle is dropped the directory is deleted
|
|
||||||
_temp_dir: Arc<Mutex<TempDir>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileSystemLike for TempFileSystem {
|
|
||||||
fn cwd(&self) -> &PathBuf {
|
|
||||||
&self.cwd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileSystemLike for RealFileSystem {
|
|
||||||
fn cwd(&self) -> &PathBuf {
|
|
||||||
&self.cwd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RealFileSystem {
|
|
||||||
const fn new(cwd: PathBuf) -> Self {
|
|
||||||
Self { cwd }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TempFileSystem {
|
|
||||||
fn new() -> std::io::Result<Self> {
|
|
||||||
let temp_dir = tempdir()?;
|
|
||||||
info!("temp dir: {:?}", temp_dir.path());
|
|
||||||
let cwd = temp_dir.path().to_path_buf();
|
|
||||||
let temp_dir = Arc::new(Mutex::new(temp_dir));
|
|
||||||
Ok(Self {
|
|
||||||
cwd,
|
|
||||||
_temp_dir: temp_dir,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_cwd() {
|
|
||||||
let cwd = PathBuf::from("/tmp");
|
|
||||||
let env = RealFileSystem::new(cwd.clone());
|
|
||||||
assert_eq!(env.cwd(), &cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_create_on_temp_fs() -> std::io::Result<()> {
|
|
||||||
let env = TempFileSystem::new()?;
|
|
||||||
assert!(env.cwd().exists());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_create_on_real_fs() {
|
|
||||||
let cwd = PathBuf::from("/tmp");
|
|
||||||
let env = RealFileSystem::new(cwd.clone());
|
|
||||||
assert_eq!(env.cwd(), &cwd);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test_log::test]
|
|
||||||
fn test_write_and_read_file() -> std::io::Result<()> {
|
|
||||||
let env = TempFileSystem::new()?;
|
|
||||||
let file_name = "test.txt";
|
|
||||||
let content = "Hello, World!";
|
|
||||||
let path = env.write_file(file_name, content)?;
|
|
||||||
assert_eq!(env.read_file(file_name)?, content);
|
|
||||||
assert!(path.exists());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +1,18 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use derive_more::From;
|
mod dir_item;
|
||||||
|
|
||||||
mod real;
|
mod real;
|
||||||
|
mod result;
|
||||||
mod temp;
|
mod temp;
|
||||||
|
|
||||||
mod dir_item;
|
|
||||||
pub use dir_item::DirItem;
|
pub use dir_item::DirItem;
|
||||||
pub use dir_item::DirItemIterator;
|
pub use dir_item::DirItemIterator;
|
||||||
use real::FileSystem;
|
pub use result::{Error, Result};
|
||||||
use temp::TempFileSystem;
|
|
||||||
|
|
||||||
#[derive(Debug, From, derive_more::Display)]
|
pub const fn new(base: PathBuf) -> real::FileSystem {
|
||||||
pub enum Error {
|
real::FileSystem::new(base)
|
||||||
Io(std::io::Error),
|
|
||||||
|
|
||||||
#[display("Path access attempted outside of base ({base:?}): {path:?}")]
|
|
||||||
PathTraversal {
|
|
||||||
base: PathBuf,
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[display("Path must be a directory: {path:?}")]
|
|
||||||
NotADirectory {
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
impl std::error::Error for Error {}
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
|
||||||
|
|
||||||
pub const fn new(base: PathBuf) -> FileSystem {
|
|
||||||
real::new(base)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn temp() -> Result<TempFileSystem> {
|
pub fn temp() -> Result<temp::TempFileSystem> {
|
||||||
temp::new()
|
temp::TempFileSystem::new()
|
||||||
}
|
}
|
||||||
|
|
247
src/fs/real.rs
247
src/fs/real.rs
|
@ -1,247 +0,0 @@
|
||||||
use std::{
|
|
||||||
fmt::Display,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::fs::DirItem;
|
|
||||||
|
|
||||||
use super::{DirItemIterator, Result};
|
|
||||||
|
|
||||||
pub const fn new(base: PathBuf) -> FileSystem {
|
|
||||||
FileSystem { base }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FileSystem {
|
|
||||||
base: PathBuf,
|
|
||||||
}
|
|
||||||
impl FileSystem {
|
|
||||||
pub fn base(&self) -> &Path {
|
|
||||||
&self.base
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path_of(&self, path: PathBuf) -> Result<PathBuf> {
|
|
||||||
let path_of = self.base.as_path().join(path);
|
|
||||||
self.validate(&path_of)?;
|
|
||||||
Ok(path_of)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dir<'base, 'path>(&'base self, path: &'path Path) -> DirReal<'base, 'path> {
|
|
||||||
DirReal::new(self.base(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file<'base, 'path>(&'base self, path: &'path Path) -> FileReal<'base, 'path> {
|
|
||||||
FileReal::new(self.base(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path<'base, 'path>(&'base self, path: &'path Path) -> PathReal<'base, 'path> {
|
|
||||||
PathReal::new(self.base(), path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate(&self, path: &Path) -> Result<()> {
|
|
||||||
let path = self.clean_path(path)?;
|
|
||||||
if !path.starts_with(&self.base) {
|
|
||||||
return Err(super::Error::PathTraversal {
|
|
||||||
base: self.base.clone(),
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean_path(&self, path: &Path) -> Result<PathBuf> {
|
|
||||||
// let path = path.as_ref();
|
|
||||||
use path_clean::PathClean;
|
|
||||||
let abs_path = if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::current_dir()?.join(path)
|
|
||||||
}
|
|
||||||
.clean();
|
|
||||||
Ok(abs_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DirReal<'base, 'path> {
|
|
||||||
path: PathReal<'base, 'path>,
|
|
||||||
}
|
|
||||||
impl<'base, 'path> DirReal<'base, 'path> {
|
|
||||||
fn new(base: &'base Path, path: &'path Path) -> Self {
|
|
||||||
let mut path = PathReal::new(base, path);
|
|
||||||
if path.error.is_none() {
|
|
||||||
if let Ok(exists) = path.exists() {
|
|
||||||
if exists {
|
|
||||||
if let Ok(is_dir) = path.is_dir() {
|
|
||||||
if !is_dir {
|
|
||||||
path.put(super::Error::NotADirectory {
|
|
||||||
path: path.full_path(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Self { path }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create(&mut self) -> Result<()> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
std::fs::create_dir(self.path.full_path()).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_all(&mut self) -> Result<()> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
std::fs::create_dir_all(self.path.full_path()).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read(&mut self) -> Result<Box<dyn Iterator<Item = Result<DirItem>>>> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
let read_dir = std::fs::read_dir(self.path.full_path())?;
|
|
||||||
Ok(Box::new(DirItemIterator::new(read_dir)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
self.path.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_dir(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_file(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct FileReal<'base, 'path> {
|
|
||||||
path: PathReal<'base, 'path>,
|
|
||||||
}
|
|
||||||
impl<'base, 'path> FileReal<'base, 'path> {
|
|
||||||
fn new(base: &'base Path, path: &'path Path) -> Self {
|
|
||||||
Self {
|
|
||||||
path: PathReal::new(base, path),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reader(&mut self) -> Result<ReaderReal> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
ReaderReal::new(&self.path.full_path())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write(&mut self, contents: &str) -> Result<()> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
std::fs::write(self.path.full_path(), contents).map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
self.path.exists()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_dir(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_file(&mut self) -> Result<bool> {
|
|
||||||
self.path.check_error()?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct PathReal<'base, 'path> {
|
|
||||||
base: &'base Path,
|
|
||||||
path: &'path Path,
|
|
||||||
error: Option<super::Error>,
|
|
||||||
}
|
|
||||||
impl<'base, 'path> PathReal<'base, 'path> {
|
|
||||||
fn full_path(&self) -> PathBuf {
|
|
||||||
self.base.join(self.path)
|
|
||||||
}
|
|
||||||
fn put(&mut self, error: super::Error) {
|
|
||||||
if self.error.is_none() {
|
|
||||||
self.error.replace(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn validate(base: &Path, path: &Path) -> Option<super::Error> {
|
|
||||||
match PathReal::clean_path(path) {
|
|
||||||
Err(error) => Some(error),
|
|
||||||
Ok(path) => {
|
|
||||||
if !path.starts_with(base) {
|
|
||||||
return Some(super::Error::PathTraversal {
|
|
||||||
base: base.to_path_buf(),
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean_path(path: &Path) -> Result<PathBuf> {
|
|
||||||
// let path = path.as_ref();
|
|
||||||
use path_clean::PathClean;
|
|
||||||
let abs_path = if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
std::env::current_dir().expect("current_dir").join(path)
|
|
||||||
}
|
|
||||||
.clean();
|
|
||||||
Ok(abs_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new(base: &'base Path, path: &'path Path) -> Self {
|
|
||||||
Self {
|
|
||||||
base,
|
|
||||||
path,
|
|
||||||
error: PathReal::validate(base, path),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists(&mut self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.full_path().exists())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_error(&mut self) -> Result<()> {
|
|
||||||
if let Some(error) = self.error.take() {
|
|
||||||
return Err(error);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_dir(&mut self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.full_path().is_dir())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_file(&mut self) -> Result<bool> {
|
|
||||||
self.check_error()?;
|
|
||||||
Ok(self.full_path().is_file())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<PathReal<'_, '_>> for PathBuf {
|
|
||||||
fn from(path: PathReal) -> Self {
|
|
||||||
path.base.join(path.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub struct ReaderReal {
|
|
||||||
contents: String,
|
|
||||||
}
|
|
||||||
impl ReaderReal {
|
|
||||||
fn new(path: &Path) -> Result<Self> {
|
|
||||||
let contents = std::fs::read_to_string(path)?;
|
|
||||||
Ok(Self { contents })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_str(&self) -> &str {
|
|
||||||
&self.contents
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Display for ReaderReal {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.contents)
|
|
||||||
}
|
|
||||||
}
|
|
64
src/fs/real/dir.rs
Normal file
64
src/fs/real/dir.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
//
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::fs::{DirItem, DirItemIterator, Error, Result};
|
||||||
|
|
||||||
|
use super::path::PathReal;
|
||||||
|
|
||||||
|
pub struct DirReal<'base, 'path> {
|
||||||
|
path: PathReal<'base, 'path>,
|
||||||
|
}
|
||||||
|
impl<'base, 'path> DirReal<'base, 'path> {
|
||||||
|
pub(super) fn new(base: &'base Path, path: &'path Path) -> Self {
|
||||||
|
let mut path = PathReal::new(base, path);
|
||||||
|
if path.error.is_none() {
|
||||||
|
if let Ok(exists) = path.exists() {
|
||||||
|
if exists {
|
||||||
|
if let Ok(is_dir) = path.is_dir() {
|
||||||
|
if !is_dir {
|
||||||
|
path.put(Error::NotADirectory {
|
||||||
|
path: path.full_path(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self { path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> PathBuf {
|
||||||
|
self.path.full_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(&mut self) -> Result<()> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
std::fs::create_dir(self.path.full_path()).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_all(&mut self) -> Result<()> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
std::fs::create_dir_all(self.path.full_path()).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&mut self) -> Result<Box<dyn Iterator<Item = Result<DirItem>>>> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
let read_dir = std::fs::read_dir(self.path.full_path())?;
|
||||||
|
Ok(Box::new(DirItemIterator::new(read_dir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
self.path.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dir(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_file(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
46
src/fs/real/file.rs
Normal file
46
src/fs/real/file.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
//
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::fs::Result;
|
||||||
|
|
||||||
|
use super::{path::PathReal, reader::ReaderReal};
|
||||||
|
|
||||||
|
pub struct FileReal<'base, 'path> {
|
||||||
|
path: PathReal<'base, 'path>,
|
||||||
|
}
|
||||||
|
impl<'base, 'path> FileReal<'base, 'path> {
|
||||||
|
pub(super) fn new(base: &'base Path, path: &'path Path) -> Self {
|
||||||
|
Self {
|
||||||
|
path: PathReal::new(base, path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> PathBuf {
|
||||||
|
self.path.full_path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reader(&mut self) -> Result<ReaderReal> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
ReaderReal::new(&self.path.full_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(&mut self, contents: &str) -> Result<()> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
std::fs::write(self.path.full_path(), contents).map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
self.path.exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dir(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_file(&mut self) -> Result<bool> {
|
||||||
|
self.path.check_error()?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
8
src/fs/real/mod.rs
Normal file
8
src/fs/real/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
//
|
||||||
|
mod system;
|
||||||
|
mod path;
|
||||||
|
mod file;
|
||||||
|
mod dir;
|
||||||
|
mod reader;
|
||||||
|
|
||||||
|
pub use system::FileSystem;
|
102
src/fs/real/path.rs
Normal file
102
src/fs/real/path.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::fs::{Error, Result};
|
||||||
|
|
||||||
|
use super::{dir::DirReal, file::FileReal};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PathReal<'base, 'path> {
|
||||||
|
base: &'base Path,
|
||||||
|
path: &'path Path,
|
||||||
|
pub(super) error: Option<Error>,
|
||||||
|
}
|
||||||
|
impl<'base, 'path> PathReal<'base, 'path> {
|
||||||
|
pub(super) fn full_path(&self) -> PathBuf {
|
||||||
|
self.base.join(self.path)
|
||||||
|
}
|
||||||
|
pub(super) fn put(&mut self, error: Error) {
|
||||||
|
if self.error.is_none() {
|
||||||
|
self.error.replace(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn validate(base: &Path, path: &Path) -> Option<Error> {
|
||||||
|
match PathReal::clean_path(path) {
|
||||||
|
Err(error) => Some(error),
|
||||||
|
Ok(path) => {
|
||||||
|
if !path.starts_with(base) {
|
||||||
|
return Some(Error::PathTraversal {
|
||||||
|
base: base.to_path_buf(),
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_path(path: &Path) -> Result<PathBuf> {
|
||||||
|
// let path = path.as_ref();
|
||||||
|
use path_clean::PathClean;
|
||||||
|
let abs_path = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir().expect("current_dir").join(path)
|
||||||
|
}
|
||||||
|
.clean();
|
||||||
|
Ok(abs_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn new(base: &'base Path, path: &'path Path) -> Self {
|
||||||
|
Self {
|
||||||
|
base,
|
||||||
|
path,
|
||||||
|
error: PathReal::validate(base, path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exists(&mut self) -> Result<bool> {
|
||||||
|
self.check_error()?;
|
||||||
|
Ok(self.full_path().exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn check_error(&mut self) -> Result<()> {
|
||||||
|
if let Some(error) = self.error.take() {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dir(&mut self) -> Result<bool> {
|
||||||
|
self.check_error()?;
|
||||||
|
Ok(self.full_path().is_dir())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_file(&mut self) -> Result<bool> {
|
||||||
|
self.check_error()?;
|
||||||
|
Ok(self.full_path().is_file())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_dir(&mut self) -> Result<Option<DirReal<'base, 'path>>> {
|
||||||
|
self.check_error()?;
|
||||||
|
if self.full_path().is_dir() {
|
||||||
|
Ok(Some(DirReal::new(self.base, self.path)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_file(&mut self) -> Result<Option<FileReal<'base, 'path>>> {
|
||||||
|
self.check_error()?;
|
||||||
|
if self.full_path().is_file() {
|
||||||
|
Ok(Some(FileReal::new(self.base, self.path)))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<PathReal<'_, '_>> for PathBuf {
|
||||||
|
fn from(path: PathReal) -> Self {
|
||||||
|
path.base.join(path.path)
|
||||||
|
}
|
||||||
|
}
|
31
src/fs/real/reader.rs
Normal file
31
src/fs/real/reader.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
//
|
||||||
|
use std::{
|
||||||
|
fmt::Display,
|
||||||
|
path::Path, str::Lines,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::fs::Result;
|
||||||
|
|
||||||
|
|
||||||
|
pub struct ReaderReal {
|
||||||
|
contents: String,
|
||||||
|
}
|
||||||
|
impl ReaderReal {
|
||||||
|
pub(super) fn new(path: &Path) -> Result<Self> {
|
||||||
|
let contents = std::fs::read_to_string(path)?;
|
||||||
|
Ok(Self { contents })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.contents
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lines(&self) -> Lines<'_> {
|
||||||
|
self.contents.lines()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for ReaderReal {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.contents)
|
||||||
|
}
|
||||||
|
}
|
62
src/fs/real/system.rs
Normal file
62
src/fs/real/system.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
//
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::fs::{Error, Result};
|
||||||
|
|
||||||
|
use super::{dir::DirReal, file::FileReal, path::PathReal};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FileSystem {
|
||||||
|
base: PathBuf,
|
||||||
|
}
|
||||||
|
impl FileSystem {
|
||||||
|
pub const fn new(base: PathBuf) -> Self {
|
||||||
|
Self { base }
|
||||||
|
}
|
||||||
|
pub fn base(&self) -> &Path {
|
||||||
|
&self.base
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path_of(&self, path: PathBuf) -> Result<PathBuf> {
|
||||||
|
let path_of = self.base.as_path().join(path);
|
||||||
|
self.validate(&path_of)?;
|
||||||
|
Ok(path_of)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dir<'base, 'path>(&'base self, path: &'path Path) -> DirReal<'base, 'path> {
|
||||||
|
DirReal::new(self.base(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file<'base, 'path>(&'base self, path: &'path Path) -> FileReal<'base, 'path> {
|
||||||
|
FileReal::new(self.base(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path<'base, 'path>(&'base self, path: &'path Path) -> PathReal<'base, 'path> {
|
||||||
|
PathReal::new(self.base(), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate(&self, path: &Path) -> Result<()> {
|
||||||
|
let path = self.clean_path(path)?;
|
||||||
|
if !path.starts_with(&self.base) {
|
||||||
|
return Err(Error::PathTraversal {
|
||||||
|
base: self.base.clone(),
|
||||||
|
path,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clean_path(&self, path: &Path) -> Result<PathBuf> {
|
||||||
|
// let path = path.as_ref();
|
||||||
|
use path_clean::PathClean;
|
||||||
|
let abs_path = if path.is_absolute() {
|
||||||
|
path.to_path_buf()
|
||||||
|
} else {
|
||||||
|
std::env::current_dir()?.join(path)
|
||||||
|
}
|
||||||
|
.clean();
|
||||||
|
Ok(abs_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
23
src/fs/result.rs
Normal file
23
src/fs/result.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use derive_more::From;
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[display("Path must be a directory: {path:?}")]
|
||||||
|
NotADirectory {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
pub type Result<T> = core::result::Result<T, Error>;
|
|
@ -4,24 +4,24 @@ use tempfile::TempDir;
|
||||||
|
|
||||||
use super::real::FileSystem;
|
use super::real::FileSystem;
|
||||||
|
|
||||||
pub(super) fn new() -> super::Result<TempFileSystem> {
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TempFileSystem {
|
pub struct TempFileSystem {
|
||||||
real: FileSystem,
|
real: FileSystem,
|
||||||
_temp_dir: Arc<Mutex<TempDir>>,
|
_temp_dir: Arc<Mutex<TempDir>>,
|
||||||
}
|
}
|
||||||
|
impl TempFileSystem {
|
||||||
|
pub fn new() -> super::Result<Self> {
|
||||||
|
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::new(base);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
real,
|
||||||
|
_temp_dir: temp_dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
impl std::ops::Deref for TempFileSystem {
|
impl std::ops::Deref for TempFileSystem {
|
||||||
type Target = FileSystem;
|
type Target = FileSystem;
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
#[cfg(feature = "fs")]
|
//
|
||||||
pub mod filesystem;
|
|
||||||
|
|
||||||
#[cfg(feature = "fs")]
|
#[cfg(feature = "fs")]
|
||||||
pub mod fs;
|
pub mod fs;
|
||||||
|
|
||||||
#[cfg(feature = "network")]
|
#[cfg(feature = "network")]
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
type TestResult = Result<(), Box<dyn std::error::Error>>;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn write_read_file_exists() -> TestResult {
|
|
||||||
let fs = crate::filesystem::temp()?;
|
|
||||||
let pathbuf = fs.write_file("foo", "content")?;
|
|
||||||
let c = fs.read_file("foo")?;
|
|
||||||
assert_eq!(c, "content");
|
|
||||||
assert!(fs.file_exists(&pathbuf));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
#[cfg(feature = "fs")]
|
|
||||||
pub mod filesystem;
|
|
||||||
|
|
||||||
#[cfg(feature = "fs")]
|
|
||||||
pub mod fs;
|
|
||||||
|
|
||||||
// #[cfg(feature = "network")]
|
|
||||||
// pub mod network;
|
|
|
@ -1,6 +1,6 @@
|
||||||
use assert2::let_assert;
|
use assert2::let_assert;
|
||||||
|
|
||||||
use crate::fs;
|
use kxio::fs;
|
||||||
|
|
||||||
type TestResult = Result<(), fs::Error>;
|
type TestResult = Result<(), fs::Error>;
|
||||||
|
|
||||||
|
@ -32,6 +32,65 @@ mod path_of {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod path {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_is_dir_as_dir_some() -> TestResult {
|
||||||
|
let fs = fs::temp().expect("temp fs");
|
||||||
|
|
||||||
|
let path = fs.base().join("foo");
|
||||||
|
let mut dir = fs.dir(&path);
|
||||||
|
dir.create().expect("create");
|
||||||
|
|
||||||
|
let_assert!(Ok(Some(as_dir)) = fs.path(&path).as_dir());
|
||||||
|
|
||||||
|
assert_eq!(dir.path(), as_dir.path());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn path_is_file_as_dir_none() -> TestResult {
|
||||||
|
let fs = fs::temp().expect("temp fs");
|
||||||
|
|
||||||
|
let path = fs.base().join("foo");
|
||||||
|
let mut file = fs.file(&path);
|
||||||
|
file.write("contents").expect("create");
|
||||||
|
|
||||||
|
let_assert!(Ok(Some(mut as_file)) = fs.path(&path).as_file());
|
||||||
|
|
||||||
|
assert_eq!(file.path(), as_file.path());
|
||||||
|
assert_eq!(as_file.reader().expect("reader").to_string(), "contents");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn path_is_dir_as_file_none() -> TestResult {
|
||||||
|
let fs = fs::temp().expect("temp fs");
|
||||||
|
|
||||||
|
let path = fs.base().join("foo");
|
||||||
|
let mut dir = fs.dir(&path);
|
||||||
|
dir.create().expect("create");
|
||||||
|
|
||||||
|
let_assert!(Ok(None) = fs.path(&path).as_file());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn path_is_file_as_file_some() -> TestResult {
|
||||||
|
let fs = fs::temp().expect("temp fs");
|
||||||
|
|
||||||
|
let path = fs.base().join("foo");
|
||||||
|
let mut file = fs.file(&path);
|
||||||
|
file.write("contents").expect("create");
|
||||||
|
|
||||||
|
let_assert!(Ok(None) = fs.path(&path).as_dir());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mod file {
|
mod file {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -55,6 +114,22 @@ mod file {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_file_lines() -> TestResult {
|
||||||
|
let fs = fs::temp().expect("temp fs");
|
||||||
|
|
||||||
|
let path = fs.base().join("foo");
|
||||||
|
let mut file = fs.file(&path);
|
||||||
|
file.write("line 1\nline 2").expect("write");
|
||||||
|
|
||||||
|
let reader = file.reader().expect("reader");
|
||||||
|
let lines = reader.lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(lines, vec!["line 1", "line 2"]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod dir_create {
|
mod dir_create {
|
Loading…
Reference in a new issue