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}