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(system: bool) -> PathBuf {
142 if system {
143 PathBuf::from("/usr/local/share/applications")
144 } else {
145 xdg_data_home().join("applications")
146 }
147}
148
149pub fn icons_dir(system: bool) -> PathBuf {
151 if system {
152 PathBuf::from("/usr/local/share/icons/hicolor")
153 } else {
154 xdg_data_home().join("icons/hicolor")
155 }
156}
157
158fn expand_variables(path: &str) -> PathResult<String> {
159 let mut result = String::with_capacity(path.len());
160 let mut chars = path.chars().peekable();
161
162 while let Some(c) = chars.next() {
163 match c {
164 '$' => {
165 if chars.peek() == Some(&'{') {
166 chars.next();
167 let var_name = consume_until(&mut chars, '}')?;
168 expand_env_var(&var_name, &mut result, path)?;
169 } else {
170 let var_name = consume_var_name(&mut chars);
171 if var_name.is_empty() {
172 result.push('$');
173 } else {
174 expand_env_var(&var_name, &mut result, path)?;
175 }
176 }
177 }
178 '~' if result.is_empty() => result.push_str(&home_dir().to_string_lossy()),
179 _ => result.push(c),
180 }
181 }
182
183 Ok(result)
184}
185
186fn consume_until(
187 chars: &mut std::iter::Peekable<std::str::Chars>,
188 delimiter: char,
189) -> PathResult<String> {
190 let mut var_name = String::new();
191
192 for c in chars.by_ref() {
193 if c == delimiter {
194 return Ok(var_name);
195 }
196 var_name.push(c);
197 }
198
199 Err(PathError::UnclosedVariable {
200 input: format!("${{{var_name}"),
201 })
202}
203
204fn consume_var_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
205 let mut var_name = String::new();
206
207 while let Some(&c) = chars.peek() {
208 if c.is_alphanumeric() || c == '_' {
209 var_name.push(chars.next().unwrap());
210 } else {
211 break;
212 }
213 }
214
215 var_name
216}
217
218fn expand_env_var(var_name: &str, result: &mut String, original: &str) -> PathResult<()> {
219 match var_name {
220 "HOME" => result.push_str(&home_dir().to_string_lossy()),
221 "XDG_CONFIG_HOME" => result.push_str(&xdg_config_home().to_string_lossy()),
222 "XDG_DATA_HOME" => result.push_str(&xdg_data_home().to_string_lossy()),
223 "XDG_CACHE_HOME" => result.push_str(&xdg_cache_home().to_string_lossy()),
224 _ => {
225 let value = env::var(var_name).map_err(|_| {
226 PathError::MissingEnvVar {
227 input: original.into(),
228 var: var_name.into(),
229 }
230 })?;
231 result.push_str(&value);
232 }
233 }
234 Ok(())
235}
236
237#[cfg(test)]
238mod tests {
239 use std::env;
240
241 use serial_test::serial;
242
243 use super::*;
244
245 #[test]
246 fn test_expand_variables_simple() {
247 env::set_var("TEST_VAR", "test_value");
248
249 let result = expand_variables("$TEST_VAR/path").unwrap();
250 assert_eq!(result, "test_value/path");
251
252 env::remove_var("TEST_VAR");
253 }
254
255 #[test]
256 fn test_expand_variables_braces() {
257 env::set_var("TEST_VAR_BRACES", "test_value");
258
259 let result = expand_variables("${TEST_VAR_BRACES}/path").unwrap();
260 assert_eq!(result, "test_value/path");
261
262 env::remove_var("TEST_VAR_BRACES");
263 }
264
265 #[test]
266 fn test_expand_variables_missing_braces() {
267 env::set_var("TEST_VAR_MISSING_BRACES", "test_value");
268
269 let result = expand_variables("${TEST_VAR_MISSING_BRACES");
270 assert!(result.is_err());
271
272 env::remove_var("TEST_VAR_MISSING_BRACES");
273 }
274
275 #[test]
276 fn test_expand_variables_missing_var() {
277 let result = expand_variables("$THIS_VAR_DOESNT_EXIST");
278 assert!(result.is_err());
279 }
280
281 #[test]
282 fn test_consume_var_name() {
283 let mut chars = "VAR_NAME_123/extra".chars().peekable();
284 let var_name = consume_var_name(&mut chars);
285 assert_eq!(var_name, "VAR_NAME_123");
286 }
287
288 #[test]
289 #[serial]
290 fn test_xdg_directories() {
291 env::set_var("HOME", "/tmp/home");
293 let home = home_dir();
294 assert_eq!(home, PathBuf::from("/tmp/home"));
295
296 env::remove_var("XDG_CONFIG_HOME");
298 env::remove_var("XDG_DATA_HOME");
299 env::remove_var("XDG_CACHE_HOME");
300
301 let config = xdg_config_home();
302 let data = xdg_data_home();
303 let cache = xdg_cache_home();
304
305 assert_eq!(config, home.join(".config"));
306 assert_eq!(data, home.join(".local/share"));
307 assert_eq!(cache, home.join(".cache"));
308 assert!(config.is_absolute());
309 assert!(data.is_absolute());
310 assert!(cache.is_absolute());
311
312 env::set_var("XDG_CONFIG_HOME", "/tmp/config");
314 env::set_var("XDG_DATA_HOME", "/tmp/data");
315 env::set_var("XDG_CACHE_HOME", "/tmp/cache");
316
317 assert_eq!(xdg_config_home(), PathBuf::from("/tmp/config"));
318 assert_eq!(xdg_data_home(), PathBuf::from("/tmp/data"));
319 assert_eq!(xdg_cache_home(), PathBuf::from("/tmp/cache"));
320
321 env::remove_var("XDG_CONFIG_HOME");
322 env::remove_var("XDG_DATA_HOME");
323 env::remove_var("XDG_CACHE_HOME");
324 env::remove_var("HOME");
325 }
326
327 #[test]
328 #[serial]
329 fn test_resolve_path() {
330 env::set_var("HOME", "/tmp/home");
331
332 assert!(resolve_path("").is_err());
333
334 assert_eq!(
336 resolve_path("/absolute/path").unwrap(),
337 PathBuf::from("/absolute/path")
338 );
339
340 let expected_relative = env::current_dir().unwrap().join("relative/path");
342 assert_eq!(resolve_path("relative/path").unwrap(), expected_relative);
343
344 let home = home_dir();
346 assert_eq!(resolve_path("~/path").unwrap(), home.join("path"));
347 assert_eq!(resolve_path("~").unwrap(), home);
348
349 let expected_tilde_middle = env::current_dir().unwrap().join("not/at/~/start");
351 assert_eq!(
352 resolve_path("not/at/~/start").unwrap(),
353 expected_tilde_middle
354 );
355 env::remove_var("HOME");
356
357 let result = resolve_path("${VAR");
359 assert!(result.is_err());
360
361 let result = resolve_path("${VAR}");
363 assert!(result.is_err());
364 }
365
366 #[test]
367 #[serial]
368 fn test_home_dir() {
369 env::set_var("HOME", "/tmp/home");
371 assert_eq!(home_dir(), PathBuf::from("/tmp/home"));
372
373 env::remove_var("HOME");
375 let expected = PathBuf::from(format!("/home/{}", get_username()));
376 assert_eq!(home_dir(), expected);
377 }
378
379 #[test]
380 #[serial]
381 fn test_expand_variables_edge_cases() {
382 env::set_var("HOME", "/tmp/home");
383
384 assert_eq!(expand_variables("path/$").unwrap(), "path/$");
386
387 assert_eq!(
389 expand_variables("path/$!invalid").unwrap(),
390 "path/$!invalid"
391 );
392
393 env::set_var("VAR1", "val1");
395 env::set_var("VAR2", "val2");
396 assert_eq!(expand_variables("$VAR1/${VAR2}").unwrap(), "val1/val2");
397 env::remove_var("VAR1");
398 env::remove_var("VAR2");
399
400 let home_str = home_dir().to_string_lossy().to_string();
402 assert_eq!(
403 expand_variables("~/path").unwrap(),
404 format!("{}/path", home_str)
405 );
406 assert_eq!(expand_variables("~").unwrap(), home_str);
407 assert_eq!(expand_variables("a/~/b").unwrap(), "a/~/b");
408 env::remove_var("HOME");
409 }
410
411 #[test]
412 #[serial]
413 fn test_resolve_path_invalid_cwd() {
414 let temp_dir = tempfile::tempdir().unwrap();
415 let invalid_path = temp_dir.path().join("invalid");
416 std::fs::create_dir(&invalid_path).unwrap();
417
418 let original_cwd = env::current_dir().unwrap();
419 env::set_current_dir(&invalid_path).unwrap();
420 std::fs::remove_dir(&invalid_path).unwrap();
421
422 let result = resolve_path("relative/path");
423 assert!(result.is_err());
424
425 env::set_current_dir(original_cwd).unwrap();
427 }
428
429 #[test]
430 #[serial]
431 fn test_expand_env_var_special_vars() {
432 env::set_var("HOME", "/tmp/home");
433 env::remove_var("XDG_CONFIG_HOME");
434 env::remove_var("XDG_DATA_HOME");
435 env::remove_var("XDG_CACHE_HOME");
436
437 let mut result = String::new();
438 expand_env_var("HOME", &mut result, "$HOME").unwrap();
439 assert_eq!(result, "/tmp/home");
440
441 result.clear();
442 expand_env_var("XDG_CONFIG_HOME", &mut result, "$XDG_CONFIG_HOME").unwrap();
443 assert_eq!(result, "/tmp/home/.config");
444
445 result.clear();
446 expand_env_var("XDG_DATA_HOME", &mut result, "$XDG_DATA_HOME").unwrap();
447 assert_eq!(result, "/tmp/home/.local/share");
448
449 result.clear();
450 expand_env_var("XDG_CACHE_HOME", &mut result, "$XDG_CACHE_HOME").unwrap();
451 assert_eq!(result, "/tmp/home/.cache");
452
453 env::remove_var("HOME");
454 }
455
456 #[test]
457 #[serial]
458 fn test_desktop_dir() {
459 env::set_var("XDG_DATA_HOME", "/tmp/data");
461 let desktop = desktop_dir(false);
462 assert_eq!(desktop, PathBuf::from("/tmp/data/applications"));
463
464 let desktop = desktop_dir(true);
466 assert_eq!(desktop, PathBuf::from("/usr/local/share/applications"));
467 }
468
469 #[test]
470 #[serial]
471 fn test_icons_dir() {
472 env::set_var("XDG_DATA_HOME", "/tmp/data");
474 let icons = icons_dir(false);
475 assert_eq!(icons, PathBuf::from("/tmp/data/icons/hicolor"));
476
477 let icons = icons_dir(true);
479 assert_eq!(icons, PathBuf::from("/usr/local/share/icons/hicolor"));
480 }
481}