komichi 2.2.0

Application tools for working with file-system paths
Documentation
//! Utilities.
//!
//! <div class="warning">
//! The functions, in this module, are <a href="../index.html#functions">wrapped</a>
//! to provide better context in errors.
//! </div>
//!
use crate::error::{CwdError, HomeError, PathConvertError};
use camino::{Utf8Path, Utf8PathBuf};

use std::ffi::OsStr;
use std::path::PathBuf;

/// Convert the given path into a [`Utf8PathBuf`](module@camino)
///
/// # Notes
/// * It is better to use
///   [`komichi::utf8_path_buf`](function@crate::utf8_path_buf) because it
///   uses [`KomichiError`](enum@crate::error::KomichiError)
///
/// * This is a convenience function that wraps
///   [`Utf8PathBuf::try_from`](function@camino::Utf8PathBuf::try_from)
///
/// # Arguments
/// * `path` - the string-like path to be converted
///
/// # Errors
///
/// * [`PathConvertError`] - will be returned if unable to convert the given
///   path. The given path must safely convert to UTF-8.
///
pub fn utf8_path_buf<T>(
    path: &T
) -> Result<Utf8PathBuf, PathConvertError>
where
    T: AsRef<OsStr> + ?Sized,
{
    let path = path.as_ref();
    let path_buf = PathBuf::from(path);
    Utf8PathBuf::try_from(path_buf).map_err(PathConvertError::new)
}

/// Retrieve and return the home directory for the current user.
///
/// # Notes
/// * It is better to use [`komichi::get_home`](function@crate::get_home)
///   because it uses [`KomichiError`](enum@crate::error::KomichiError) and
///   no `func` argument is needed.
///
/// # Arguments
/// * `func` - the function to use to retrieve the home directory.
///
/// # Errors
///
/// * [`HomeError`] - will be returned if:
///   * unable to locate the user's home directory; or
///   * the retrieved home directory contains non UTF-8 encoded characters; or
///   * the retrieved home directory is not an absolute path.
///
pub fn get_home<F>(func: F) -> Result<Utf8PathBuf, HomeError>
where
    F: Fn() -> Option<PathBuf>,
{
    match func() {
        Some(path) => Ok(utf8_path_buf(&path).map_err(|e| {
            HomeError::retrieve_convert_error(&path, e)
        })?),
        None => Err(HomeError::RetrieveLocateError),
    }
}

/// Return the given path or use the user's home directory if it's absolute.
///
/// # Notes
/// * It is better to use [`komichi::use_home`](fn@crate::use_home)
///   because it uses [`KomichiError`](enum@crate::error::KomichiError) and
///   no `func` argument is needed.
///
/// # Arguments
/// * `path` - the path to use
/// * `func` - the function to use to retrieve the home directory.
///
/// # Errors
///
/// * [`HomeError`] - will be returned if:
///   * unable to locate the user's home directory; or
///   * the retrieved home directory contains non UTF-8 encoded characters; or
///   * the retrieved, or given, home directory is not an absolute path.
///
pub fn use_home<T, F>(
    path: Option<&T>,
    func: F,
) -> Result<Utf8PathBuf, HomeError>
where
    T: AsRef<Utf8Path> + ?Sized,
    F: Fn() -> Option<PathBuf>,
{
    let out = match path {
        Some(p) => p.as_ref().to_owned(),
        None => get_home(func)?,
    };

    if !out.is_absolute() {
        return Err(HomeError::NotAbsolute(out.to_string()));
    }
    Ok(out)
}

/// Retrieve and return the current working directory ("CWD") for the current
/// user.
///
/// # Notes
/// * It is better to use [`komichi::get_cwd`](fn@crate::get_cwd)
///   because it uses [`KomichiError`](enum@crate::error::KomichiError) and
///   no `func` argument is needed.
///
/// # Arguments
/// * `func` - the function to use to retrieve the CWD.
///
/// # Errors
///
/// * [`CwdError`] - will be returned if:
///   * unable to locate the current user's CWD; or
///   * the retrieved CWD has a file-error e.g. Does not exist, permissions,
///     etc., or
///   * the retrieved CWD contains non UTF-8 encoded characters
///
pub fn get_cwd<F>(func: F) -> Result<Utf8PathBuf, CwdError>
where
    F: Fn() -> Result<PathBuf, std::io::Error>,
{
    match func() {
        Ok(path) => Ok(utf8_path_buf(&path).map_err(|e| {
            CwdError::retrieve_convert_error(&path, e)
        })?),
        Err(e) => Err(CwdError::RetrieveLocateError(e)),
    }
}

/// Return the given path or use the current user's current working directory
/// ("CWD") if it's an absolute path.
///
/// # Notes
/// * It is better to use [`komichi::use_cwd`](fn@crate::use_cwd) because it
///   uses [`KomichiError`](enum@crate::error::KomichiError) and no `func`
///   argument is needed.
///
/// # Arguments
/// * `path` - the path to use
/// * `func` - the function to use to retrieve the CWD.
///
/// # Errors
///
/// * [`CwdError`] - will be returned if:
///   * unable to locate the current user's CWD; or
///   * the retrieved CWD has a file-error e.g. Does not exist, permissions,
///     etc., or
///   * the retrieved CWD contains non UTF-8 encoded characters; or
///   * the retrieved, or given, CWD is not an absolute path.
///
pub fn use_cwd<T, F>(
    path: Option<&T>,
    func: F,
) -> Result<Utf8PathBuf, CwdError>
where
    T: AsRef<Utf8Path> + ?Sized,
    F: Fn() -> Result<PathBuf, std::io::Error>,
{
    let out = match path {
        Some(p) => p.as_ref().to_owned(),
        None => get_cwd(func)?,
    };

    if !out.is_absolute() {
        return Err(CwdError::NotAbsolute(out.to_string()));
    }
    Ok(out)
}

/// Replace the user's home directory, in the given path, with a tilde.
///
/// The general use case is to clean up paths in error messages so that
/// the user's path information will not make it to the screen.
///
/// # Arguments
/// * `path` - A string-like reference to a [`str`] containing the path
///   to be scrubbed.
/// * `home` - An [`Option`] containing a string-like reference to a [`str`]
///   containing the home directory. The home directory will be queried
///   from the system if the value is [`None`].
///
/// # Example
/// ```
/// use komichi::scrub_path;
///
/// let path = "/a/b/c/d/e/f/g";
/// let home = "/a/b";
/// let expect = "~/c/d/e/f/g".to_string();
/// let result = scrub_path(&path, Some(&home));
/// assert_eq!(expect, result);
/// ```
pub fn scrub_path<T>(
    path: &T,
    home: Option<&T>,
) -> String
where
    T: AsRef<OsStr> + ?Sized,
{
    let path = path.as_ref().to_string_lossy().to_string();
    let home = match home {
        Some(h) => h.as_ref().to_string_lossy().to_string(),
        None => match dirs_sys::home_dir() {
            Some(h) => h.to_string_lossy().to_string(),
            None => return path,
        },
    };
    if path.starts_with(&home) {
        return path.replacen(&home, "~", 1);
    }
    path
}

/// Used to replace a user's home directory, in the given path, with a tilde.
///
/// The general use case is to clean up paths in error messages so that
/// the user's path information will not make it to the screen.
///
/// # Example
///
/// This will default to using the current-user's home directory. If the
/// user's home directory doesn't match the full path will be returned.
///
/// ```
/// use komichi::Scrub;
/// let out = Scrub::new().path("/home/i-mziB!6ct/.local/bin");
/// assert_eq!(out, "/home/i-mziB!6ct/.local/bin".to_string());
/// ```
/// However this can be set to use the home directory of a user other
/// that the current-user:
///
/// ```
/// use komichi::Scrub;
/// let mut scrub = Scrub::new();
/// scrub.set_home("/home/i-mziB!6ct");
/// let out = scrub.path("/home/i-mziB!6ct/.local/bin");
/// assert_eq!(out, "~/.local/bin".to_string());
///
/// ```
#[derive(Debug, Clone)]
pub struct Scrub {
    home: Option<String>,
}

impl Default for Scrub {
    fn default() -> Self {
        Self::new()
    }
}

impl Scrub {
    pub fn new() -> Self {
        let home = match dirs_sys::home_dir() {
            Some(h) => match Utf8PathBuf::from_path_buf(h) {
                Ok(h) => Some(h.to_string()),
                Err(_) => None,
            },
            None => None,
        };
        Self { home }
    }

    pub fn path<T>(
        &self,
        input: &T,
    ) -> String
    where
        T: AsRef<OsStr> + ?Sized,
    {
        let input = input.as_ref().to_string_lossy().to_string();
        if let Some(home) = self.home.as_ref() {
            if input.starts_with(home) {
                return input.replacen(home, "~", 1);
            }
        }
        input
    }

    pub fn set_home<T>(
        &mut self,
        input: &T,
    ) where
        T: AsRef<OsStr> + ?Sized,
    {
        let input = input.as_ref().to_string_lossy().to_string();
        if input.is_empty() {
            self.home = None;
        } else {
            self.home = Some(input)
        }
    }
}