Skip to main content

dot/
path.rs

1use std::path::{Component, Path, PathBuf};
2
3use crate::error::{Error, Result};
4
5#[allow(dead_code)]
6pub fn expand_tilde(path: &Path) -> Result<PathBuf> {
7    expand_tilde_with_home(path, dirs::home_dir())
8}
9
10#[allow(dead_code)]
11pub fn collapse_tilde(path: &Path) -> Result<PathBuf> {
12    collapse_tilde_with_home(path, dirs::home_dir())
13}
14
15pub fn expand_tilde_with_home(path: &Path, home: Option<PathBuf>) -> Result<PathBuf> {
16    if path.starts_with("~") {
17        let home = home.ok_or(Error::NoHomeDir)?;
18        let suffix = path.strip_prefix("~").expect("checked above");
19        Ok(home.join(suffix))
20    } else {
21        Ok(path.to_path_buf())
22    }
23}
24
25pub fn collapse_tilde_with_home(path: &Path, home: Option<PathBuf>) -> Result<PathBuf> {
26    let home = home.ok_or(Error::NoHomeDir)?;
27    let abs = to_lexical_absolute(path)?;
28
29    if abs.starts_with(&home) {
30        let suffix = abs.strip_prefix(&home).expect("checked above");
31        Ok(Path::new("~").join(suffix))
32    } else {
33        Ok(abs)
34    }
35}
36
37pub fn to_lexical_absolute(path: &Path) -> Result<PathBuf> {
38    let mut absolute = if path.is_absolute() {
39        PathBuf::new()
40    } else {
41        std::env::current_dir()?
42    };
43
44    for component in path.components() {
45        match component {
46            Component::CurDir => {}
47            Component::ParentDir => {
48                absolute.pop();
49            }
50            c => absolute.push(c.as_os_str()),
51        }
52    }
53    Ok(absolute)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn expand_tilde_with_valid_home() {
62        let home = PathBuf::from("/home/user");
63        let result = expand_tilde_with_home(Path::new("~/config"), Some(home)).unwrap();
64        assert_eq!(result, PathBuf::from("/home/user/config"));
65    }
66
67    #[test]
68    fn expand_tilde_no_prefix() {
69        let result = expand_tilde_with_home(Path::new("/absolute"), Some("/home".into())).unwrap();
70        assert_eq!(result, PathBuf::from("/absolute"));
71    }
72
73    #[test]
74    fn expand_tilde_no_home_errors() {
75        let result = expand_tilde_with_home(Path::new("~/config"), None);
76        assert!(matches!(result, Err(Error::NoHomeDir)));
77    }
78
79    #[test]
80    fn collapse_tilde_inside_home() {
81        let home = PathBuf::from("/home/user");
82        let result = collapse_tilde_with_home(Path::new("/home/user/config"), Some(home)).unwrap();
83        assert_eq!(result, PathBuf::from("~/config"));
84    }
85
86    #[test]
87    fn collapse_tilde_outside_home() {
88        let home = PathBuf::from("/home/user");
89        let result = collapse_tilde_with_home(Path::new("/etc/config"), Some(home)).unwrap();
90        assert_eq!(result, PathBuf::from("/etc/config"));
91    }
92
93    #[test]
94    fn lexical_absolute_resolves_parent() {
95        let result = to_lexical_absolute(Path::new("/foo/bar/../baz")).unwrap();
96        assert_eq!(result, PathBuf::from("/foo/baz"));
97    }
98
99    #[test]
100    fn lexical_absolute_resolves_current() {
101        let result = to_lexical_absolute(Path::new("/foo/./bar")).unwrap();
102        assert_eq!(result, PathBuf::from("/foo/bar"));
103    }
104}