1use std::{env, path::PathBuf};
2
3use crate::{
4 error::{PathError, PathResult},
5 system::get_username,
6};
7
8pub fn resolve_path(path: &str) -> PathResult<PathBuf> {
42 let path = path.trim();
43
44 if path.is_empty() {
45 return Err(PathError::Empty);
46 }
47
48 let resolved = expand_variables(path)?;
49 let path_buf = PathBuf::from(resolved);
50
51 if path_buf.is_absolute() {
52 Ok(path_buf)
53 } else {
54 env::current_dir()
55 .map(|cwd| cwd.join(path_buf))
56 .map_err(|err| {
57 PathError::FailedToGetCurrentDir {
58 source: err,
59 }
60 })
61 }
62}
63
64pub fn home_dir() -> PathBuf {
78 env::var("HOME")
79 .map(PathBuf::from)
80 .unwrap_or_else(|_| PathBuf::from(format!("/home/{}", get_username())))
81}
82
83pub fn xdg_config_home() -> PathBuf {
97 env::var("XDG_CONFIG_HOME")
98 .map(PathBuf::from)
99 .unwrap_or_else(|_| home_dir().join(".config"))
100}
101
102pub fn xdg_data_home() -> PathBuf {
116 env::var("XDG_DATA_HOME")
117 .map(PathBuf::from)
118 .unwrap_or_else(|_| home_dir().join(".local/share"))
119}
120
121pub fn xdg_cache_home() -> PathBuf {
135 env::var("XDG_CACHE_HOME")
136 .map(PathBuf::from)
137 .unwrap_or_else(|_| home_dir().join(".cache"))
138}
139
140pub fn desktop_dir() -> PathBuf {
142 xdg_data_home().join("applications")
143}
144
145pub fn icons_dir() -> PathBuf {
147 xdg_data_home().join("icons/hicolor")
148}
149
150fn expand_variables(path: &str) -> PathResult<String> {
151 let mut result = String::with_capacity(path.len());
152 let mut chars = path.chars().peekable();
153
154 while let Some(c) = chars.next() {
155 match c {
156 '$' => {
157 if chars.peek() == Some(&'{') {
158 chars.next();
159 let var_name = consume_until(&mut chars, '}')?;
160 expand_env_var(&var_name, &mut result, path)?;
161 } else {
162 let var_name = consume_var_name(&mut chars);
163 if var_name.is_empty() {
164 result.push('$');
165 } else {
166 expand_env_var(&var_name, &mut result, path)?;
167 }
168 }
169 }
170 '~' if result.is_empty() => result.push_str(&home_dir().to_string_lossy()),
171 _ => result.push(c),
172 }
173 }
174
175 Ok(result)
176}
177
178fn consume_until(
179 chars: &mut std::iter::Peekable<std::str::Chars>,
180 delimiter: char,
181) -> PathResult<String> {
182 let mut var_name = String::new();
183
184 for c in chars.by_ref() {
185 if c == delimiter {
186 return Ok(var_name);
187 }
188 var_name.push(c);
189 }
190
191 Err(PathError::UnclosedVariable {
192 input: format!("${{{var_name}"),
193 })
194}
195
196fn consume_var_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
197 let mut var_name = String::new();
198
199 while let Some(&c) = chars.peek() {
200 if c.is_alphanumeric() || c == '_' {
201 var_name.push(chars.next().unwrap());
202 } else {
203 break;
204 }
205 }
206
207 var_name
208}
209
210fn expand_env_var(var_name: &str, result: &mut String, original: &str) -> PathResult<()> {
211 match var_name {
212 "HOME" => result.push_str(&home_dir().to_string_lossy()),
213 "XDG_CONFIG_HOME" => result.push_str(&xdg_config_home().to_string_lossy()),
214 "XDG_DATA_HOME" => result.push_str(&xdg_data_home().to_string_lossy()),
215 "XDG_CACHE_HOME" => result.push_str(&xdg_cache_home().to_string_lossy()),
216 _ => {
217 let value = env::var(var_name).map_err(|_| {
218 PathError::MissingEnvVar {
219 input: original.into(),
220 var: var_name.into(),
221 }
222 })?;
223 result.push_str(&value);
224 }
225 }
226 Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231 use std::env;
232
233 use serial_test::serial;
234
235 use super::*;
236
237 #[test]
238 fn test_expand_variables_simple() {
239 env::set_var("TEST_VAR", "test_value");
240
241 let result = expand_variables("$TEST_VAR/path").unwrap();
242 assert_eq!(result, "test_value/path");
243
244 env::remove_var("TEST_VAR");
245 }
246
247 #[test]
248 fn test_expand_variables_braces() {
249 env::set_var("TEST_VAR_BRACES", "test_value");
250
251 let result = expand_variables("${TEST_VAR_BRACES}/path").unwrap();
252 assert_eq!(result, "test_value/path");
253
254 env::remove_var("TEST_VAR_BRACES");
255 }
256
257 #[test]
258 fn test_expand_variables_missing_braces() {
259 env::set_var("TEST_VAR_MISSING_BRACES", "test_value");
260
261 let result = expand_variables("${TEST_VAR_MISSING_BRACES");
262 assert!(result.is_err());
263
264 env::remove_var("TEST_VAR_MISSING_BRACES");
265 }
266
267 #[test]
268 fn test_expand_variables_missing_var() {
269 let result = expand_variables("$THIS_VAR_DOESNT_EXIST");
270 assert!(result.is_err());
271 }
272
273 #[test]
274 fn test_consume_var_name() {
275 let mut chars = "VAR_NAME_123/extra".chars().peekable();
276 let var_name = consume_var_name(&mut chars);
277 assert_eq!(var_name, "VAR_NAME_123");
278 }
279
280 #[test]
281 #[serial]
282 fn test_xdg_directories() {
283 env::set_var("HOME", "/tmp/home");
285 let home = home_dir();
286 assert_eq!(home, PathBuf::from("/tmp/home"));
287
288 env::remove_var("XDG_CONFIG_HOME");
290 env::remove_var("XDG_DATA_HOME");
291 env::remove_var("XDG_CACHE_HOME");
292
293 let config = xdg_config_home();
294 let data = xdg_data_home();
295 let cache = xdg_cache_home();
296
297 assert_eq!(config, home.join(".config"));
298 assert_eq!(data, home.join(".local/share"));
299 assert_eq!(cache, home.join(".cache"));
300 assert!(config.is_absolute());
301 assert!(data.is_absolute());
302 assert!(cache.is_absolute());
303
304 env::set_var("XDG_CONFIG_HOME", "/tmp/config");
306 env::set_var("XDG_DATA_HOME", "/tmp/data");
307 env::set_var("XDG_CACHE_HOME", "/tmp/cache");
308
309 assert_eq!(xdg_config_home(), PathBuf::from("/tmp/config"));
310 assert_eq!(xdg_data_home(), PathBuf::from("/tmp/data"));
311 assert_eq!(xdg_cache_home(), PathBuf::from("/tmp/cache"));
312
313 env::remove_var("XDG_CONFIG_HOME");
314 env::remove_var("XDG_DATA_HOME");
315 env::remove_var("XDG_CACHE_HOME");
316 env::remove_var("HOME");
317 }
318
319 #[test]
320 #[serial]
321 fn test_resolve_path() {
322 env::set_var("HOME", "/tmp/home");
323
324 assert!(resolve_path("").is_err());
325
326 assert_eq!(
328 resolve_path("/absolute/path").unwrap(),
329 PathBuf::from("/absolute/path")
330 );
331
332 let expected_relative = env::current_dir().unwrap().join("relative/path");
334 assert_eq!(resolve_path("relative/path").unwrap(), expected_relative);
335
336 let home = home_dir();
338 assert_eq!(resolve_path("~/path").unwrap(), home.join("path"));
339 assert_eq!(resolve_path("~").unwrap(), home);
340
341 let expected_tilde_middle = env::current_dir().unwrap().join("not/at/~/start");
343 assert_eq!(
344 resolve_path("not/at/~/start").unwrap(),
345 expected_tilde_middle
346 );
347 env::remove_var("HOME");
348
349 let result = resolve_path("${VAR");
351 assert!(result.is_err());
352
353 let result = resolve_path("${VAR}");
355 assert!(result.is_err());
356 }
357
358 #[test]
359 #[serial]
360 fn test_home_dir() {
361 env::set_var("HOME", "/tmp/home");
363 assert_eq!(home_dir(), PathBuf::from("/tmp/home"));
364
365 env::remove_var("HOME");
367 let expected = PathBuf::from(format!("/home/{}", get_username()));
368 assert_eq!(home_dir(), expected);
369 }
370
371 #[test]
372 #[serial]
373 fn test_expand_variables_edge_cases() {
374 env::set_var("HOME", "/tmp/home");
375
376 assert_eq!(expand_variables("path/$").unwrap(), "path/$");
378
379 assert_eq!(
381 expand_variables("path/$!invalid").unwrap(),
382 "path/$!invalid"
383 );
384
385 env::set_var("VAR1", "val1");
387 env::set_var("VAR2", "val2");
388 assert_eq!(expand_variables("$VAR1/${VAR2}").unwrap(), "val1/val2");
389 env::remove_var("VAR1");
390 env::remove_var("VAR2");
391
392 let home_str = home_dir().to_string_lossy().to_string();
394 assert_eq!(
395 expand_variables("~/path").unwrap(),
396 format!("{}/path", home_str)
397 );
398 assert_eq!(expand_variables("~").unwrap(), home_str);
399 assert_eq!(expand_variables("a/~/b").unwrap(), "a/~/b");
400 env::remove_var("HOME");
401 }
402
403 #[test]
404 #[serial]
405 fn test_resolve_path_invalid_cwd() {
406 let temp_dir = tempfile::tempdir().unwrap();
407 let invalid_path = temp_dir.path().join("invalid");
408 std::fs::create_dir(&invalid_path).unwrap();
409
410 let original_cwd = env::current_dir().unwrap();
411 env::set_current_dir(&invalid_path).unwrap();
412 std::fs::remove_dir(&invalid_path).unwrap();
413
414 let result = resolve_path("relative/path");
415 assert!(result.is_err());
416
417 env::set_current_dir(original_cwd).unwrap();
419 }
420
421 #[test]
422 #[serial]
423 fn test_expand_env_var_special_vars() {
424 env::set_var("HOME", "/tmp/home");
425 env::remove_var("XDG_CONFIG_HOME");
426 env::remove_var("XDG_DATA_HOME");
427 env::remove_var("XDG_CACHE_HOME");
428
429 let mut result = String::new();
430 expand_env_var("HOME", &mut result, "$HOME").unwrap();
431 assert_eq!(result, "/tmp/home");
432
433 result.clear();
434 expand_env_var("XDG_CONFIG_HOME", &mut result, "$XDG_CONFIG_HOME").unwrap();
435 assert_eq!(result, "/tmp/home/.config");
436
437 result.clear();
438 expand_env_var("XDG_DATA_HOME", &mut result, "$XDG_DATA_HOME").unwrap();
439 assert_eq!(result, "/tmp/home/.local/share");
440
441 result.clear();
442 expand_env_var("XDG_CACHE_HOME", &mut result, "$XDG_CACHE_HOME").unwrap();
443 assert_eq!(result, "/tmp/home/.cache");
444
445 env::remove_var("HOME");
446 }
447
448 #[test]
449 #[serial]
450 fn test_desktop_dir() {
451 env::set_var("XDG_DATA_HOME", "/tmp/data");
452 let desktop = desktop_dir();
453 assert_eq!(desktop, PathBuf::from("/tmp/data/applications"));
454 }
455
456 #[test]
457 #[serial]
458 fn test_icons_dir() {
459 env::set_var("XDG_DATA_HOME", "/tmp/data");
460 let icons = icons_dir();
461 assert_eq!(icons, PathBuf::from("/tmp/data/icons/hicolor"));
462 }
463}