1#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
2use pwd::Passwd;
3use std::path::{Path, PathBuf};
4
5#[cfg(target_os = "macos")]
6const FALLBACK_USER_HOME_BASE_DIR: &str = "/Users";
7
8#[cfg(target_os = "windows")]
9const FALLBACK_USER_HOME_BASE_DIR: &str = "C:\\Users\\";
10
11#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
12const FALLBACK_USER_HOME_BASE_DIR: &str = "/home";
13
14#[cfg(all(unix, target_os = "android"))]
15const FALLBACK_USER_HOME_BASE_DIR: &str = "/data";
16
17#[cfg(target_os = "android")]
18const TERMUX_HOME: &str = "/data/data/com.termux/files/home";
19
20fn expand_tilde_with_home(path: impl AsRef<Path>, home: Option<PathBuf>) -> PathBuf {
21 let path = path.as_ref();
22
23 if !path.starts_with("~") {
24 let string = path.to_string_lossy();
25 let mut path_as_string = string.as_ref().bytes();
26 return match path_as_string.next() {
27 Some(b'~') => expand_tilde_with_another_user_home(path),
28 _ => path.into(),
29 };
30 }
31
32 let path_last_char = path.as_os_str().to_string_lossy().chars().next_back();
33 let need_trailing_slash = path_last_char == Some('/') || path_last_char == Some('\\');
34
35 match home {
36 None => path.into(),
37 Some(mut h) => {
38 if h == Path::new("/") {
39 path.strip_prefix("~").unwrap_or(path).into()
42 } else {
43 if let Ok(p) = path.strip_prefix("~/") {
44 if p != Path::new("") {
50 h.push(p)
51 }
52
53 if need_trailing_slash {
54 h.push("");
55 }
56 }
57 h
58 }
59 }
60 }
61}
62
63#[cfg(not(target_arch = "wasm32"))]
64fn fallback_home_dir(username: &str) -> PathBuf {
65 PathBuf::from_iter([FALLBACK_USER_HOME_BASE_DIR, username])
66}
67
68#[cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))]
69fn user_home_dir(username: &str) -> PathBuf {
70 let passwd = Passwd::from_name(username);
71 match &passwd.ok() {
72 Some(Some(dir)) => PathBuf::from(&dir.dir),
73 _ => fallback_home_dir(username),
74 }
75}
76
77#[cfg(any(target_os = "android", target_os = "windows", target_os = "macos"))]
78fn user_home_dir(username: &str) -> PathBuf {
79 use std::path::Component;
80
81 match dirs::home_dir() {
82 None => {
83 #[cfg(target_os = "android")]
85 if is_termux() {
86 return PathBuf::from(TERMUX_HOME);
87 }
88
89 fallback_home_dir(username)
90 }
91 Some(user) => {
92 let mut expected_path = user;
93
94 if !cfg!(target_os = "android")
95 && expected_path
96 .components()
97 .next_back()
98 .map(|last| last != Component::Normal(username.as_ref()))
99 .unwrap_or(false)
100 {
101 expected_path.pop();
102 expected_path.push(Path::new(username));
103 }
104
105 if expected_path.is_dir() {
106 expected_path
107 } else {
108 fallback_home_dir(username)
109 }
110 }
111 }
112}
113
114#[cfg(target_arch = "wasm32")]
115fn user_home_dir(_: &str) -> PathBuf {
116 let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
118 PathBuf::from(home)
119}
120
121#[cfg(target_os = "android")]
124fn is_termux() -> bool {
125 std::env::var("TERMUX_VERSION").is_ok()
126}
127
128fn expand_tilde_with_another_user_home(path: &Path) -> PathBuf {
129 match path.to_str() {
130 Some(file_path) => {
131 let mut file = file_path.to_string();
132 match file_path.find(['/', '\\']) {
133 None => {
134 file.remove(0);
135 user_home_dir(&file)
136 }
137 Some(i) => {
138 let (pre_name, rest_of_path) = file.split_at(i);
139 let mut name = pre_name.to_string();
140 let mut rest_path = rest_of_path.to_string();
141 rest_path.remove(0);
142 name.remove(0);
143 let mut path = user_home_dir(&name);
144 path.push(Path::new(&rest_path));
145 path
146 }
147 }
148 }
149 None => path.to_path_buf(),
150 }
151}
152
153pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
155 expand_tilde_with_home(path, dirs::home_dir())
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use crate::assert_path_eq;
162 use std::path::MAIN_SEPARATOR;
163
164 fn check_expanded(s: &str) {
165 let home = Path::new("/home");
166 let buf = Some(PathBuf::from(home));
167 assert!(expand_tilde_with_home(Path::new(s), buf).starts_with(home));
168
169 let home = Path::new("/");
171 let buf = Some(PathBuf::from(home));
172 assert!(!expand_tilde_with_home(Path::new(s), buf).starts_with("//"));
173 }
174
175 fn check_not_expanded(s: &str) {
176 let home = PathBuf::from("/home");
177 let expanded = expand_tilde_with_home(Path::new(s), Some(home));
178 assert_eq!(expanded, Path::new(s));
179 }
180
181 #[test]
182 fn string_with_tilde() {
183 check_expanded("~");
184 }
185
186 #[test]
187 fn string_with_tilde_forward_slash() {
188 check_expanded("~/test/");
189 }
190
191 #[test]
192 fn string_with_tilde_double_forward_slash() {
193 check_expanded("~//test/");
194 }
195
196 #[test]
197 fn string_with_tilde_other_user() {
198 let s = "~someone/test/";
199 let expected = format!("{FALLBACK_USER_HOME_BASE_DIR}/someone/test/");
200
201 assert_eq!(expand_tilde(Path::new(s)), PathBuf::from(expected));
202 }
203
204 #[test]
205 fn string_with_multi_byte_chars() {
206 let s = "~あ/";
207 let expected = format!("{FALLBACK_USER_HOME_BASE_DIR}/あ/");
208
209 assert_eq!(expand_tilde(Path::new(s)), PathBuf::from(expected));
210 }
211
212 #[test]
213 fn does_not_expand_tilde_if_tilde_is_not_first_character() {
214 check_not_expanded("1~1");
215 }
216
217 #[test]
218 fn path_does_not_include_trailing_separator() {
219 let home = Path::new("/home");
220 let buf = Some(PathBuf::from(home));
221 let expanded = expand_tilde_with_home(Path::new("~"), buf);
222 let expanded_str = expanded.to_str().unwrap();
223 assert!(!expanded_str.ends_with(MAIN_SEPARATOR));
224 }
225
226 #[cfg(windows)]
227 #[test]
228 fn string_with_tilde_backslash() {
229 check_expanded("~\\test/test2/test3");
230 }
231
232 #[cfg(windows)]
233 #[test]
234 fn string_with_double_tilde_backslash() {
235 check_expanded("~\\\\test\\test2/test3");
236 }
237
238 #[test]
240 fn user_home_dir_fallback() {
241 let user = "nonexistent";
242 let expected_home = PathBuf::from_iter([FALLBACK_USER_HOME_BASE_DIR, user]);
243
244 #[cfg(target_os = "android")]
245 let expected_home = if is_termux() {
246 PathBuf::from(TERMUX_HOME)
247 } else {
248 expected_home
249 };
250
251 let actual_home = super::user_home_dir(user);
252
253 assert_eq!(expected_home, actual_home, "wrong home");
254 }
255
256 #[test]
257 #[cfg(not(windows))]
258 fn expand_tilde_preserve_trailing_slash() {
259 let path = PathBuf::from("~/foo/");
260 let home = PathBuf::from("/home");
261
262 let actual = expand_tilde_with_home(path, Some(home));
263 assert_path_eq!(actual, "/home/foo/");
264 }
265 #[test]
266 #[cfg(windows)]
267 fn expand_tilde_preserve_trailing_slash() {
268 let path = PathBuf::from("~\\foo\\");
269 let home = PathBuf::from("C:\\home");
270
271 let actual = expand_tilde_with_home(path, Some(home));
272 assert_path_eq!(actual, "C:\\home\\foo\\");
273 }
274}