kxio 3.0.0

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

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

use super::{
    path::{DirMarker, FileMarker, PathReal},
    PathMarker,
};

/// Represents to base of a section of a file system.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FileSystem {
    base: PathBuf,
}

/// Represents a directory path in the filesystem.
pub type DirHandle = PathReal<DirMarker>;

/// Represents a file path in the filesystem.
pub type FileHandle = PathReal<FileMarker>;

/// Represents a path in the filesystem.
pub type PathHandle<T> = PathReal<T>;

impl FileSystem {
    pub const fn new(base: PathBuf) -> Self {
        Self { base }
    }

    /// Returns the base of the [FileSystem].
    pub fn base(&self) -> &Path {
        &self.base
    }

    /// Returns a [PathBuf] for the path.
    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)
    }

    /// Access the path as a directory.
    ///
    /// The path must exist and be a directory.
    ///
    /// If the path does not exist, or is not a directory, an error is returned.
    ///
    /// ```
    /// # fn try_main() -> kxio::fs::Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let dir = fs.dir(&path);
    /// dir.create()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn dir(&self, path: &Path) -> DirHandle {
        let mut dir = PathReal::new(&self.base, path);

        if dir.error.is_none() {
            if let Ok(exists) = dir.exists() {
                if exists {
                    if let Ok(is_dir) = dir.is_dir() {
                        if !is_dir {
                            dir.put(Error::NotADirectory {
                                path: dir.as_pathbuf(),
                            })
                        }
                    }
                }
            }
        }

        dir
    }

    /// Access the path as a file.
    ///
    /// The path must exist and be a file.
    ///
    /// If the path does not exist, or is not a file, an error is returned.
    ///
    /// ```
    /// # fn try_main() -> kxio::fs::Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let file = fs.file(&path);
    /// file.write("new file contents")?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn file(&self, path: &Path) -> FileHandle {
        let mut file = PathReal::new(&self.base, path);

        if file.error.is_none() {
            if let Ok(exists) = file.exists() {
                if exists {
                    if let Ok(is_file) = file.is_file() {
                        if !is_file {
                            file.put(Error::NotAFile {
                                path: file.as_pathbuf(),
                            })
                        }
                    }
                }
            }
        }

        file
    }

    /// Access the path as a path.
    ///
    /// The path must exist.
    ///
    /// If the path does not exist, an error is returned.
    ///
    /// ```
    /// # fn try_main() -> kxio::fs::Result<()> {
    /// let fs = kxio::fs::temp()?;
    /// let path = fs.base().join("foo");
    /// let path_handle = fs.path(&path);
    /// # Ok(())
    /// # }
    /// ```
    pub fn path(&self, path: &Path) -> PathHandle<PathMarker> {
        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().map_err(Error::Io)?.join(path)
        }
        .clean();
        Ok(abs_path)
    }
}