1use std::{
2 env,
3 path::{Component, Path, PathBuf},
4};
5
6use crate::{
7 error::{PathError, PathResult},
8 system::get_username,
9};
10
11pub fn is_safe_component(name: &str) -> bool {
28 let mut components = Path::new(name).components();
29 matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()
30}
31
32pub fn resolve_path(path: &str) -> PathResult<PathBuf> {
66 let path = path.trim();
67
68 if path.is_empty() {
69 return Err(PathError::Empty);
70 }
71
72 let resolved = expand_variables(path)?;
73 let path_buf = PathBuf::from(resolved);
74
75 if path_buf.is_absolute() {
76 Ok(path_buf)
77 } else {
78 env::current_dir()
79 .map(|cwd| cwd.join(path_buf))
80 .map_err(|err| {
81 PathError::FailedToGetCurrentDir {
82 source: err,
83 }
84 })
85 }
86}
87
88pub fn home_dir() -> PathBuf {
102 env::var("HOME")
103 .map(PathBuf::from)
104 .unwrap_or_else(|_| PathBuf::from(format!("/home/{}", get_username())))
105}
106
107pub fn xdg_config_home() -> PathBuf {
121 env::var("XDG_CONFIG_HOME")
122 .map(PathBuf::from)
123 .unwrap_or_else(|_| home_dir().join(".config"))
124}
125
126pub fn xdg_data_home() -> PathBuf {
140 env::var("XDG_DATA_HOME")
141 .map(PathBuf::from)
142 .unwrap_or_else(|_| home_dir().join(".local/share"))
143}
144
145pub fn xdg_cache_home() -> PathBuf {
159 env::var("XDG_CACHE_HOME")
160 .map(PathBuf::from)
161 .unwrap_or_else(|_| home_dir().join(".cache"))
162}
163
164pub fn desktop_dir(system: bool) -> PathBuf {
166 if system {
167 PathBuf::from("/usr/local/share/applications")
168 } else {
169 xdg_data_home().join("applications")
170 }
171}
172
173pub fn icons_dir(system: bool) -> PathBuf {
175 if system {
176 PathBuf::from("/usr/local/share/icons/hicolor")
177 } else {
178 xdg_data_home().join("icons/hicolor")
179 }
180}
181
182fn expand_variables(path: &str) -> PathResult<String> {
183 let mut result = String::with_capacity(path.len());
184 let mut chars = path.chars().peekable();
185
186 while let Some(c) = chars.next() {
187 match c {
188 '$' => {
189 if chars.peek() == Some(&'{') {
190 chars.next();
191 let var_name = consume_until(&mut chars, '}')?;
192 expand_env_var(&var_name, &mut result, path)?;
193 } else {
194 let var_name = consume_var_name(&mut chars);
195 if var_name.is_empty() {
196 result.push('$');
197 } else {
198 expand_env_var(&var_name, &mut result, path)?;
199 }
200 }
201 }
202 '~' if result.is_empty() => result.push_str(&home_dir().to_string_lossy()),
203 _ => result.push(c),
204 }
205 }
206
207 Ok(result)
208}
209
210fn consume_until(
211 chars: &mut std::iter::Peekable<std::str::Chars>,
212 delimiter: char,
213) -> PathResult<String> {
214 let mut var_name = String::new();
215
216 for c in chars.by_ref() {
217 if c == delimiter {
218 return Ok(var_name);
219 }
220 var_name.push(c);
221 }
222
223 Err(PathError::UnclosedVariable {
224 input: format!("${{{var_name}"),
225 })
226}
227
228fn consume_var_name(chars: &mut std::iter::Peekable<std::str::Chars>) -> String {
229 let mut var_name = String::new();
230
231 while let Some(&c) = chars.peek() {
232 if c.is_alphanumeric() || c == '_' {
233 var_name.push(chars.next().unwrap());
234 } else {
235 break;
236 }
237 }
238
239 var_name
240}
241
242fn expand_env_var(var_name: &str, result: &mut String, original: &str) -> PathResult<()> {
243 match var_name {
244 "HOME" => result.push_str(&home_dir().to_string_lossy()),
245 "XDG_CONFIG_HOME" => result.push_str(&xdg_config_home().to_string_lossy()),
246 "XDG_DATA_HOME" => result.push_str(&xdg_data_home().to_string_lossy()),
247 "XDG_CACHE_HOME" => result.push_str(&xdg_cache_home().to_string_lossy()),
248 _ => {
249 let value = env::var(var_name).map_err(|_| {
250 PathError::MissingEnvVar {
251 input: original.into(),
252 var: var_name.into(),
253 }
254 })?;
255 result.push_str(&value);
256 }
257 }
258 Ok(())
259}
260
261#[cfg(test)]
262mod tests {
263 #[test]
264 fn test_is_safe_component() {
265 assert!(super::is_safe_component("clipcat"));
266 assert!(super::is_safe_component("clip-cat.1"));
267 assert!(super::is_safe_component("soarpkgs"));
268
269 assert!(!super::is_safe_component(""));
270 assert!(!super::is_safe_component("."));
271 assert!(!super::is_safe_component(".."));
272 assert!(!super::is_safe_component("a/b"));
273 assert!(!super::is_safe_component("../etc"));
274 assert!(!super::is_safe_component("../../home/user/.bashrc"));
275 assert!(!super::is_safe_component("/etc/passwd"));
276 }
277
278 use std::env;
279
280 use serial_test::serial;
281
282 use super::*;
283
284 #[test]
285 fn test_expand_variables_simple() {
286 env::set_var("TEST_VAR", "test_value");
287
288 let result = expand_variables("$TEST_VAR/path").unwrap();
289 assert_eq!(result, "test_value/path");
290
291 env::remove_var("TEST_VAR");
292 }
293
294 #[test]
295 fn test_expand_variables_braces() {
296 env::set_var("TEST_VAR_BRACES", "test_value");
297
298 let result = expand_variables("${TEST_VAR_BRACES}/path").unwrap();
299 assert_eq!(result, "test_value/path");
300
301 env::remove_var("TEST_VAR_BRACES");
302 }
303
304 #[test]
305 fn test_expand_variables_missing_braces() {
306 env::set_var("TEST_VAR_MISSING_BRACES", "test_value");
307
308 let result = expand_variables("${TEST_VAR_MISSING_BRACES");
309 assert!(result.is_err());
310
311 env::remove_var("TEST_VAR_MISSING_BRACES");
312 }
313
314 #[test]
315 fn test_expand_variables_missing_var() {
316 let result = expand_variables("$THIS_VAR_DOESNT_EXIST");
317 assert!(result.is_err());
318 }
319
320 #[test]
321 fn test_consume_var_name() {
322 let mut chars = "VAR_NAME_123/extra".chars().peekable();
323 let var_name = consume_var_name(&mut chars);
324 assert_eq!(var_name, "VAR_NAME_123");
325 }
326
327 #[test]
328 #[serial]
329 fn test_xdg_directories() {
330 env::set_var("HOME", "/tmp/home");
332 let home = home_dir();
333 assert_eq!(home, PathBuf::from("/tmp/home"));
334
335 env::remove_var("XDG_CONFIG_HOME");
337 env::remove_var("XDG_DATA_HOME");
338 env::remove_var("XDG_CACHE_HOME");
339
340 let config = xdg_config_home();
341 let data = xdg_data_home();
342 let cache = xdg_cache_home();
343
344 assert_eq!(config, home.join(".config"));
345 assert_eq!(data, home.join(".local/share"));
346 assert_eq!(cache, home.join(".cache"));
347 assert!(config.is_absolute());
348 assert!(data.is_absolute());
349 assert!(cache.is_absolute());
350
351 env::set_var("XDG_CONFIG_HOME", "/tmp/config");
353 env::set_var("XDG_DATA_HOME", "/tmp/data");
354 env::set_var("XDG_CACHE_HOME", "/tmp/cache");
355
356 assert_eq!(xdg_config_home(), PathBuf::from("/tmp/config"));
357 assert_eq!(xdg_data_home(), PathBuf::from("/tmp/data"));
358 assert_eq!(xdg_cache_home(), PathBuf::from("/tmp/cache"));
359
360 env::remove_var("XDG_CONFIG_HOME");
361 env::remove_var("XDG_DATA_HOME");
362 env::remove_var("XDG_CACHE_HOME");
363 env::remove_var("HOME");
364 }
365
366 #[test]
367 #[serial]
368 fn test_resolve_path() {
369 env::set_var("HOME", "/tmp/home");
370
371 assert!(resolve_path("").is_err());
372
373 assert_eq!(
375 resolve_path("/absolute/path").unwrap(),
376 PathBuf::from("/absolute/path")
377 );
378
379 let expected_relative = env::current_dir().unwrap().join("relative/path");
381 assert_eq!(resolve_path("relative/path").unwrap(), expected_relative);
382
383 let home = home_dir();
385 assert_eq!(resolve_path("~/path").unwrap(), home.join("path"));
386 assert_eq!(resolve_path("~").unwrap(), home);
387
388 let expected_tilde_middle = env::current_dir().unwrap().join("not/at/~/start");
390 assert_eq!(
391 resolve_path("not/at/~/start").unwrap(),
392 expected_tilde_middle
393 );
394 env::remove_var("HOME");
395
396 let result = resolve_path("${VAR");
398 assert!(result.is_err());
399
400 let result = resolve_path("${VAR}");
402 assert!(result.is_err());
403 }
404
405 #[test]
406 #[serial]
407 fn test_home_dir() {
408 env::set_var("HOME", "/tmp/home");
410 assert_eq!(home_dir(), PathBuf::from("/tmp/home"));
411
412 env::remove_var("HOME");
414 let expected = PathBuf::from(format!("/home/{}", get_username()));
415 assert_eq!(home_dir(), expected);
416 }
417
418 #[test]
419 #[serial]
420 fn test_expand_variables_edge_cases() {
421 env::set_var("HOME", "/tmp/home");
422
423 assert_eq!(expand_variables("path/$").unwrap(), "path/$");
425
426 assert_eq!(
428 expand_variables("path/$!invalid").unwrap(),
429 "path/$!invalid"
430 );
431
432 env::set_var("VAR1", "val1");
434 env::set_var("VAR2", "val2");
435 assert_eq!(expand_variables("$VAR1/${VAR2}").unwrap(), "val1/val2");
436 env::remove_var("VAR1");
437 env::remove_var("VAR2");
438
439 let home_str = home_dir().to_string_lossy().to_string();
441 assert_eq!(
442 expand_variables("~/path").unwrap(),
443 format!("{}/path", home_str)
444 );
445 assert_eq!(expand_variables("~").unwrap(), home_str);
446 assert_eq!(expand_variables("a/~/b").unwrap(), "a/~/b");
447 env::remove_var("HOME");
448 }
449
450 #[test]
451 #[serial]
452 fn test_resolve_path_invalid_cwd() {
453 let temp_dir = tempfile::tempdir().unwrap();
454 let invalid_path = temp_dir.path().join("invalid");
455 std::fs::create_dir(&invalid_path).unwrap();
456
457 let original_cwd = env::current_dir().unwrap();
458 env::set_current_dir(&invalid_path).unwrap();
459 std::fs::remove_dir(&invalid_path).unwrap();
460
461 let result = resolve_path("relative/path");
462 assert!(result.is_err());
463
464 env::set_current_dir(original_cwd).unwrap();
466 }
467
468 #[test]
469 #[serial]
470 fn test_expand_env_var_special_vars() {
471 env::set_var("HOME", "/tmp/home");
472 env::remove_var("XDG_CONFIG_HOME");
473 env::remove_var("XDG_DATA_HOME");
474 env::remove_var("XDG_CACHE_HOME");
475
476 let mut result = String::new();
477 expand_env_var("HOME", &mut result, "$HOME").unwrap();
478 assert_eq!(result, "/tmp/home");
479
480 result.clear();
481 expand_env_var("XDG_CONFIG_HOME", &mut result, "$XDG_CONFIG_HOME").unwrap();
482 assert_eq!(result, "/tmp/home/.config");
483
484 result.clear();
485 expand_env_var("XDG_DATA_HOME", &mut result, "$XDG_DATA_HOME").unwrap();
486 assert_eq!(result, "/tmp/home/.local/share");
487
488 result.clear();
489 expand_env_var("XDG_CACHE_HOME", &mut result, "$XDG_CACHE_HOME").unwrap();
490 assert_eq!(result, "/tmp/home/.cache");
491
492 env::remove_var("HOME");
493 }
494
495 #[test]
496 #[serial]
497 fn test_desktop_dir() {
498 env::set_var("XDG_DATA_HOME", "/tmp/data");
500 let desktop = desktop_dir(false);
501 assert_eq!(desktop, PathBuf::from("/tmp/data/applications"));
502
503 let desktop = desktop_dir(true);
505 assert_eq!(desktop, PathBuf::from("/usr/local/share/applications"));
506 }
507
508 #[test]
509 #[serial]
510 fn test_icons_dir() {
511 env::set_var("XDG_DATA_HOME", "/tmp/data");
513 let icons = icons_dir(false);
514 assert_eq!(icons, PathBuf::from("/tmp/data/icons/hicolor"));
515
516 let icons = icons_dir(true);
518 assert_eq!(icons, PathBuf::from("/usr/local/share/icons/hicolor"));
519 }
520}