kxio 3.0.0

Provides injectable Filesystem and Network resources to make code more testable
Documentation
//
use std::{
    marker::PhantomData,
    path::{Path, PathBuf},
};

use crate::fs::{Error, Result};

use super::{DirHandle, FileHandle, PathHandle};

/// Marker trait for the type of [PathReal].
pub trait PathType {}

/// Path marker for the type of [PathReal].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PathMarker;
impl PathType for PathMarker {}

/// File marker for the type of [PathReal].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileMarker;
impl PathType for FileMarker {}

/// Dir marker for the type of [PathReal].
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct DirMarker;
impl PathType for DirMarker {}

/// Represents a path in the filesystem.
///
/// It can be a simple path, or it can be a file or a directory.
#[derive(Clone, Debug, Default)]
pub struct PathReal<T: PathType> {
    base: PathBuf,
    path: PathBuf,
    _phanton: PhantomData<T>,
    pub(super) error: Option<Error>,
}
impl<T: PathType> PathReal<T> {
    pub(super) fn new(base: impl Into<PathBuf>, path: impl Into<PathBuf>) -> Self {
        let base: PathBuf = base.into();
        let path: PathBuf = path.into();
        let error = PathReal::<T>::validate(&base, &path);
        Self {
            base,
            path,
            _phanton: PhantomData::<T>,
            error,
        }
    }

    /// Returns a [PathBuf] for the path.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let path = fs.path(&path);
    /// let pathbuf = path.as_pathbuf();
    /// #    Ok(())
    /// # }
    /// ```
    pub fn as_pathbuf(&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::<PathMarker>::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 check_error(&self) -> Result<()> {
        if let Some(error) = &self.error {
            Err(error.clone())
        } else {
            Ok(())
        }
    }

    /// Returns true if the path exists.
    ///
    /// N.B. If you have the path used to create the file or directory, you
    /// should use [std::path::Path::exists] instead.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let dir = fs.dir(&path);
    /// #    dir.create()?;
    /// if dir.exists()? { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn exists(&self) -> Result<bool> {
        self.check_error()?;
        Ok(self.as_pathbuf().exists())
    }

    /// Returns true if the path is a directory.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// #    fs.dir(&path).create()?;
    /// if path.is_dir() { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn is_dir(&self) -> Result<bool> {
        self.check_error()?;
        Ok(self.as_pathbuf().is_dir())
    }

    /// Returns true if the path is a file.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// #    fs.dir(&path).create()?;
    /// if path.is_file() { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn is_file(&self) -> Result<bool> {
        self.check_error()?;
        Ok(self.as_pathbuf().is_file())
    }

    /// Returns the path as a directory if it exists and is a directory, otherwise
    /// it will return None.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// #    fs.dir(&path).create()?;
    /// let file = fs.path(&path);
    /// if let Ok(Some(dir)) = file.as_dir() { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn as_dir(&self) -> Result<Option<DirHandle>> {
        self.check_error()?;
        if self.as_pathbuf().is_dir() {
            Ok(Some(PathReal::new(&self.base, &self.path)))
        } else {
            Ok(None)
        }
    }

    /// Returns the path as a file if it exists and is a file, otherwise
    /// it will return None.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// #    fs.dir(&path).create()?;
    /// let file = fs.path(&path);
    /// if let Ok(Some(file)) = file.as_file() { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn as_file(&self) -> Result<Option<FileHandle>> {
        self.check_error()?;
        if self.as_pathbuf().is_file() {
            Ok(Some(PathReal::new(&self.base, &self.path)))
        } else {
            Ok(None)
        }
    }

    /// Renames a path.
    ///
    /// Wrapper for [std::fs::rename]
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let src_path = fs.base().join("foo");
    /// let src = fs.file(&src_path);
    /// # src.write("bar")?;
    /// let dst_path = fs.base().join("bar");
    /// let dst = fs.file(&dst_path);
    /// src.rename(&dst)?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn rename(&self, dest: &PathHandle<T>) -> Result<()> {
        self.check_error()?;
        std::fs::rename(self.as_pathbuf(), dest.as_pathbuf()).map_err(Error::Io)
    }

    /// Returns the metadata for a path.
    ///
    /// Wrapper for [std::fs::metadata]
    ///
    /// ```
    /// # fn try_main() -> kxio::fs::Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let file = fs.file(&path);
    /// let metadata = file.metadata()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn metadata(&self) -> Result<std::fs::Metadata> {
        self.check_error()?;
        std::fs::metadata(self.as_pathbuf()).map_err(Error::Io)
    }

    /// Creates a symbolic link to a path.
    ///
    /// Wrapper for [std::os::unix::fs::symlink]
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let src_path = fs.base().join("foo");
    /// let src = fs.file(&src_path);
    /// # src.write("bar")?;
    /// let link_path = fs.base().join("bar");
    /// let link = fs.path(&link_path);
    /// src.soft_link(&link)?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn soft_link(&self, link: &PathReal<PathMarker>) -> Result<()> {
        self.check_error()?;
        std::os::unix::fs::symlink(self.as_pathbuf(), link.as_pathbuf()).map_err(Error::Io)
    }

    /// Returns true if the path is a symbolic link.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let dir = fs.dir(&path);
    /// # dir.create()?;
    /// if dir.is_link()? { /* ... */ }
    /// #    Ok(())
    /// # }
    /// ```
    pub fn is_link(&self) -> Result<bool> {
        self.check_error()?;
        Ok(self.as_pathbuf().is_symlink())
    }

    /// Returns the canonical, absolute form of the path with all intermediate
    /// components normalized and symbolic links resolved.
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// #    fs.dir(&path).create()?;
    /// let dir = fs.path(&path);
    /// let canonical = dir.canonicalize()?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn canonicalize(&self) -> Result<PathBuf> {
        self.check_error()?;
        self.as_pathbuf().canonicalize().map_err(Error::Io)
    }

    /// Returns the metadata for a path without following symlinks.
    ///
    /// Wrapper for [std::fs::symlink_metadata]
    ///
    /// ```
    /// # fn try_main() -> kxio::fs::Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let file = fs.file(&path);
    /// let metadata = file.symlink_metadata()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn symlink_metadata(&self) -> Result<std::fs::Metadata> {
        self.check_error()?;
        std::fs::symlink_metadata(self.as_pathbuf()).map_err(Error::Io)
    }

    /// Sets the permissions of a file or directory.
    ///
    /// Wrapper for [std::fs::set_permissions]
    ///
    /// ```
    /// # use kxio::fs::Result;
    /// # use std::os::unix::fs::PermissionsExt;
    /// # fn main() -> Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let file = fs.file(&path);
    /// # file.write("bar")?;
    /// file.set_permissions(std::fs::Permissions::from_mode(0o755))?;
    /// #    Ok(())
    /// # }
    /// ```
    pub fn set_permissions(&self, perms: std::fs::Permissions) -> Result<()> {
        self.check_error()?;
        std::fs::set_permissions(self.as_pathbuf(), perms).map_err(Error::Io)
    }

    pub fn read_link(&self) -> Result<PathReal<PathMarker>> {
        self.check_error()?;
        let read_path = std::fs::read_link(self.as_pathbuf()).map_err(Error::Io)?;
        let path = read_path.strip_prefix(&self.base).unwrap().to_path_buf();
        Ok(PathReal::new(&self.base, &path))
    }
}
impl From<PathHandle<PathMarker>> for PathBuf {
    fn from(path: PathReal<PathMarker>) -> Self {
        path.base.join(path.path)
    }
}
impl From<DirHandle> for PathHandle<PathMarker> {
    fn from(dir: DirHandle) -> Self {
        PathReal::new(dir.base, dir.path)
    }
}
impl From<FileHandle> for PathHandle<PathMarker> {
    fn from(file: FileHandle) -> Self {
        PathReal::new(file.base, file.path)
    }
}