1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
//! A enable expansion of tildes in paths
//!
//! built for unix, windows patch welcome
//!
//! - ~ expands using the HOME environmental variable.
//! - if HOME does not exist, lookup current user in the user database
//!
//! - ~`user` will expand to the user's home directory from the user database
//!

use std::env;
use std::path::{Component, Path, PathBuf};

use nix::unistd::{Uid, User};

#[cfg(test)]
mod tests;

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
/// Error while expanding path
pub enum Error {
    /// The user being looked up is not in the user database
    #[error("the user does not have entry")]
    MissingEntry,
}

/// The expansion trait extension
pub trait HomeDirExt {
    /// Expands a users home directory signified by a tilde.
    ///
    /// ```
    /// # use home_dir::HomeDirExt;
    /// # use std::env::var;
    /// # use std::path::PathBuf;
    /// let mut path = PathBuf::from(var("HOME").unwrap());
    /// path.push(".vimrc");
    ///
    /// assert_eq!("~/.vimrc".expand_home().unwrap(), path, "current user path expansion");
    ///
    /// # #[cfg(target_os = "macos")]
    /// # const ROOT_VIMRC: &'static str = "/var/root/.vimrc";
    /// # #[cfg(target_os = "linux")]
    /// # const ROOT_VIMRC: &'static str = "/root/.vimrc";
    /// assert_eq!("~root/.vimrc".expand_home().unwrap(), PathBuf::from(ROOT_VIMRC));
    /// ```
    fn expand_home(&self) -> Result<PathBuf, Error>;
}

impl HomeDirExt for Path {
    fn expand_home(&self) -> Result<PathBuf, Error> {
        let mut path = PathBuf::new();
        let mut comps = self.components();

        match comps.next() {
            Some(Component::Normal(os)) => {
                if let Some(s) = os.to_str() {
                    match s {
                        "~" => {
                            let p = getenv()
                                .ok_or(Error::MissingEntry)
                                .or_else(|_| getent_current())?;
                            path.push(p);
                        }
                        s if s.starts_with('~') => {
                            path.push(getent(&s[1..])?);
                        }
                        s => path.push(s),
                    }
                } else {
                    path.push(os)
                }
            }
            Some(comp) => path.push(comp),
            None => return Ok(path),
        };

        for comp in comps {
            path.push(comp);
        }

        Ok(path)
    }
}

impl<T> HomeDirExt for T
where
    T: AsRef<Path>,
{
    fn expand_home(&self) -> Result<PathBuf, Error> {
        self.as_ref().expand_home()
    }
}

pub(crate) fn getent(name: &str) -> Result<PathBuf, Error> {
    let usr = User::from_name(name).or(Err(Error::MissingEntry))?;
    let usr = usr.ok_or_else(|| Error::MissingEntry)?;

    Ok(usr.dir)
}

pub(crate) fn getenv() -> Option<PathBuf> {
    env::var("HOME").ok().map(Into::into)
}

pub(crate) fn getent_current() -> Result<PathBuf, Error> {
    let usr = User::from_uid(Uid::current()).or(Err(Error::MissingEntry))?;
    let usr = usr.ok_or_else(|| Error::MissingEntry)?;

    Ok(usr.dir)
}