Skip to main content

dirs_lite/
lib.rs

1use std::env;
2use std::path::PathBuf;
3
4const CONFIG_DIR: &str = ".config";
5const DATA_DIR: &str = ".local/share";
6const CACHE_DIR: &str = ".cache";
7
8/// Returns the path to the user's config directory.
9///
10/// The returned value depends on the operating system and is either a `Some`, containing a value from the following table, or a `None`.
11///
12/// |Platform | Value                                 | Example                                  |
13/// | ------- | ------------------------------------- | ---------------------------------------- |
14/// | Linux   | `$XDG_CONFIG_HOME` or `$HOME`/.config | /home/alice/.config                      |
15/// | macOS   | `$HOME`/Library/Application Support   | /Users/Alice/Library/Application Support |
16/// | Windows | `%APPDATA%`\Roaming                 | C:\Users\Alice\AppData\Roaming           |
17///
18/// NOTE: if the feature `favor-xdg-style` is enabled, `$HOME/.config` is favorized.
19pub fn config_dir() -> Option<PathBuf> {
20    if cfg!(target_os = "linux") {
21        // Linux: Use $HOME/.config
22        env::var_os("XDG_CONFIG_HOME")
23            .filter(|s| !s.is_empty())
24            .map(PathBuf::from)
25            .or_else(|| {
26                std::env::home_dir().map(|mut base| {
27                    base.push(CONFIG_DIR);
28                    base
29                })
30            })
31    } else if cfg!(target_os = "macos") {
32        // macOS: Use $HOME/Library/Application Support
33        //  or $HOME/.config if favor-xdg-style is enabled
34        std::env::home_dir().map(|mut home| {
35            if cfg!(feature = "favor-xdg-style") {
36                home.push(CONFIG_DIR);
37                return home;
38            }
39            home.push("Library");
40            home.push("Application Support");
41            home
42        })
43    } else if cfg!(target_os = "windows") {
44        // Windows: Use %APPDATA%
45        env::var_os("APPDATA")
46            .filter(|s| !s.is_empty())
47            .map(PathBuf::from)
48    } else {
49        // Unsupported platform
50        None
51    }
52}
53
54/// Returns the path to the user's data directory.
55///
56/// The returned value depends on the operating system and is either a `Some`, containing a value from the following table, or a `None`.
57///
58/// |Platform | Value                                 | Example                                  |
59/// | ------- | ------------------------------------- | ---------------------------------------- |
60/// | Linux   | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/alice/.local/share              |
61/// | macOS   | `$HOME`/Library/Application Support   | /Users/Alice/Library/Application Support |
62/// | Windows | `%LOCALAPPDATA%`                      | C:\Users\Alice\AppData\Local             |
63///
64/// NOTE: if the feature `favor-xdg-style` is enabled, `$HOME/.local/share` is favorized on macOS.
65pub fn data_dir() -> Option<PathBuf> {
66    if cfg!(target_os = "linux") {
67        // Linux: Use $XDG_DATA_HOME or $HOME/.local/share
68        env::var_os("XDG_DATA_HOME")
69            .filter(|s| !s.is_empty())
70            .map(PathBuf::from)
71            .or_else(|| {
72                std::env::home_dir().map(|mut home| {
73                    home.push(DATA_DIR);
74                    home
75                })
76            })
77    } else if cfg!(target_os = "macos") {
78        // macOS: Use $HOME/Library/Application Support
79        //  or $HOME/.local/share if favor-xdg-style is enabled
80        std::env::home_dir().map(|mut home| {
81            if cfg!(feature = "favor-xdg-style") {
82                home.push(DATA_DIR);
83                return home;
84            }
85            home.push("Library");
86            home.push("Application Support");
87            home
88        })
89    } else if cfg!(target_os = "windows") {
90        // Windows: Use %LOCALAPPDATA%
91        env::var_os("LOCALAPPDATA")
92            .filter(|s| !s.is_empty())
93            .map(PathBuf::from)
94    } else {
95        // Unsupported platform
96        None
97    }
98}
99
100/// Returns the path to the user's cache directory.
101///
102/// The returned value depends on the operating system and is either a `Some`, containing a value from the following table, or a `None`.
103///
104/// |Platform | Value                                 | Example                                  |
105/// | ------- | ------------------------------------- | ---------------------------------------- |
106/// | Linux   | `$XDG_CACHE_HOME` or `$HOME`/.cache   | /home/alice/.cache                       |
107/// | macOS   | `$HOME`/Library/Caches                | /Users/Alice/Library/Caches              |
108/// | Windows | `%LOCALAPPDATA%`                      | C:\Users\Alice\AppData\Local             |
109///
110/// NOTE: if the feature `favor-xdg-style` is enabled, `$HOME/.cache` is favorized on macOS.
111pub fn cache_dir() -> Option<PathBuf> {
112    if cfg!(target_os = "linux") {
113        // Linux: Use $XDG_CACHE_HOME or $HOME/.cache
114        env::var_os("XDG_CACHE_HOME")
115            .filter(|s| !s.is_empty())
116            .map(PathBuf::from)
117            .or_else(|| {
118                std::env::home_dir().map(|mut home| {
119                    home.push(CACHE_DIR);
120                    home
121                })
122            })
123    } else if cfg!(target_os = "macos") {
124        // macOS: Use $HOME/Library/Caches
125        //  or $HOME/.cache if favor-xdg-style is enabled
126        std::env::home_dir().map(|mut home| {
127            if cfg!(feature = "favor-xdg-style") {
128                home.push(CACHE_DIR);
129                return home;
130            }
131            home.push("Library");
132            home.push("Caches");
133            home
134        })
135    } else if cfg!(target_os = "windows") {
136        // Windows: Use %LOCALAPPDATA%
137        env::var_os("LOCALAPPDATA")
138            .filter(|s| !s.is_empty())
139            .map(PathBuf::from)
140    } else {
141        // Unsupported platform
142        None
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    unsafe fn set_var(key: &str, value: &str) {
151        unsafe { env::set_var(key, value) };
152    }
153
154    unsafe fn remove_var(key: &str) {
155        unsafe { env::remove_var(key) };
156    }
157
158    fn restore_var(key: &str, original: Option<String>) {
159        // SAFETY: Tests run single-threaded with --test-threads=1
160        unsafe {
161            match original {
162                Some(val) => set_var(key, &val),
163                None => remove_var(key),
164            }
165        }
166    }
167
168    #[cfg(any(target_os = "linux", target_os = "macos"))]
169    fn restore_var_os(key: &str, original: Option<std::ffi::OsString>) {
170        // SAFETY: Tests run single-threaded with --test-threads=1
171        unsafe {
172            match original {
173                Some(val) => env::set_var(key, val),
174                None => env::remove_var(key),
175            }
176        }
177    }
178
179    #[test]
180    fn config_dir_returns_some() {
181        let result = config_dir();
182        assert!(
183            result.is_some(),
184            "config_dir should return Some on supported platforms"
185        );
186    }
187
188    #[test]
189    #[cfg(target_os = "linux")]
190    fn linux_uses_xdg_config_home_when_set() {
191        let original = env::var("XDG_CONFIG_HOME").ok();
192        // SAFETY: Tests run single-threaded with --test-threads=1
193        unsafe { set_var("XDG_CONFIG_HOME", "/custom/.config") };
194
195        let result = config_dir();
196        assert_eq!(result, Some(PathBuf::from("/custom/.config")));
197
198        restore_var("XDG_CONFIG_HOME", original);
199    }
200
201    #[test]
202    #[cfg(target_os = "linux")]
203    fn linux_falls_back_to_home_when_xdg_unset() {
204        let original_xdg = env::var("XDG_CONFIG_HOME").ok();
205        let original_home = env::var("HOME").ok();
206
207        // SAFETY: Tests run single-threaded with --test-threads=1
208        unsafe {
209            remove_var("XDG_CONFIG_HOME");
210            set_var("HOME", "/home/testuser");
211        }
212
213        let result = config_dir();
214        assert_eq!(result, Some(PathBuf::from("/home/testuser/.config")));
215
216        restore_var("XDG_CONFIG_HOME", original_xdg);
217        restore_var("HOME", original_home);
218    }
219
220    #[test]
221    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
222    fn macos_config_dir_uses_library_application_support() {
223        let original = env::var("HOME").ok();
224        // SAFETY: Tests run single-threaded with --test-threads=1
225        unsafe { set_var("HOME", "/Users/testuser") };
226
227        let result = config_dir();
228        assert_eq!(
229            result,
230            Some(PathBuf::from("/Users/testuser/Library/Application Support"))
231        );
232
233        restore_var("HOME", original);
234    }
235
236    #[test]
237    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
238    fn macos_config_dir_uses_xdg_style() {
239        let original = env::var("HOME").ok();
240        // SAFETY: Tests run single-threaded with --test-threads=1
241        unsafe { set_var("HOME", "/Users/testuser") };
242
243        let result = config_dir();
244        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.config")));
245
246        restore_var("HOME", original);
247    }
248
249    #[test]
250    #[cfg(target_os = "windows")]
251    fn windows_uses_appdata() {
252        let original = env::var("APPDATA").ok();
253        // SAFETY: Tests run single-threaded with --test-threads=1
254        unsafe { set_var("APPDATA", "C:\\Users\\testuser\\AppData\\Roaming") };
255
256        let result = config_dir();
257        assert_eq!(
258            result,
259            Some(PathBuf::from("C:\\Users\\testuser\\AppData\\Roaming"))
260        );
261
262        restore_var("APPDATA", original);
263    }
264
265    #[test]
266    fn config_dir_path_is_absolute() {
267        let result = config_dir();
268        if let Some(path) = result {
269            assert!(
270                path.is_absolute(),
271                "config_dir should return an absolute path"
272            );
273        }
274    }
275
276    #[test]
277    fn data_dir_returns_some() {
278        let result = data_dir();
279        assert!(
280            result.is_some(),
281            "data_dir should return Some on supported platforms"
282        );
283    }
284
285    #[test]
286    #[cfg(target_os = "linux")]
287    fn linux_data_dir_uses_xdg_data_home_when_set() {
288        let original = env::var("XDG_DATA_HOME").ok();
289        // SAFETY: Tests run single-threaded with --test-threads=1
290        unsafe { set_var("XDG_DATA_HOME", "/custom/data") };
291
292        let result = data_dir();
293        assert_eq!(result, Some(PathBuf::from("/custom/data")));
294
295        restore_var("XDG_DATA_HOME", original);
296    }
297
298    #[test]
299    #[cfg(target_os = "linux")]
300    fn linux_data_dir_falls_back_to_home_when_xdg_unset() {
301        let original_xdg = env::var("XDG_DATA_HOME").ok();
302        let original_home = env::var("HOME").ok();
303
304        // SAFETY: Tests run single-threaded with --test-threads=1
305        unsafe {
306            remove_var("XDG_DATA_HOME");
307            set_var("HOME", "/home/testuser");
308        }
309
310        let result = data_dir();
311        assert_eq!(result, Some(PathBuf::from("/home/testuser/.local/share")));
312
313        restore_var("XDG_DATA_HOME", original_xdg);
314        restore_var("HOME", original_home);
315    }
316
317    #[test]
318    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
319    fn macos_data_dir_uses_library_application_support() {
320        let original = env::var("HOME").ok();
321        // SAFETY: Tests run single-threaded with --test-threads=1
322        unsafe { set_var("HOME", "/Users/testuser") };
323
324        let result = data_dir();
325        assert_eq!(
326            result,
327            Some(PathBuf::from("/Users/testuser/Library/Application Support"))
328        );
329
330        restore_var("HOME", original);
331    }
332
333    #[test]
334    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
335    fn macos_data_dir_uses_xdg_style() {
336        let original = env::var("HOME").ok();
337        // SAFETY: Tests run single-threaded with --test-threads=1
338        unsafe { set_var("HOME", "/Users/testuser") };
339
340        let result = data_dir();
341        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.local/share")));
342
343        restore_var("HOME", original);
344    }
345
346    #[test]
347    #[cfg(target_os = "windows")]
348    fn windows_data_dir_uses_localappdata() {
349        let original = env::var("LOCALAPPDATA").ok();
350        // SAFETY: Tests run single-threaded with --test-threads=1
351        unsafe { set_var("LOCALAPPDATA", "C:\\Users\\runneradmin\\AppData\\Local") };
352
353        let result = data_dir();
354        assert_eq!(
355            result,
356            Some(PathBuf::from("C:\\Users\\runneradmin\\AppData\\Local"))
357        );
358
359        restore_var("LOCALAPPDATA", original);
360    }
361
362    #[test]
363    fn data_dir_path_is_absolute() {
364        let result = data_dir();
365        if let Some(path) = result {
366            assert!(
367                path.is_absolute(),
368                "data_dir should return an absolute path"
369            );
370        }
371    }
372
373    #[test]
374    fn cache_dir_returns_some() {
375        let result = cache_dir();
376        assert!(
377            result.is_some(),
378            "cache_dir should return Some on supported platforms"
379        );
380    }
381
382    #[test]
383    #[cfg(target_os = "linux")]
384    fn linux_cache_dir_uses_xdg_cache_home_when_set() {
385        let original = env::var("XDG_CACHE_HOME").ok();
386        // SAFETY: Tests run single-threaded with --test-threads=1
387        unsafe { set_var("XDG_CACHE_HOME", "/custom/cache") };
388
389        let result = cache_dir();
390        assert_eq!(result, Some(PathBuf::from("/custom/cache")));
391
392        restore_var("XDG_CACHE_HOME", original);
393    }
394
395    #[test]
396    #[cfg(target_os = "linux")]
397    fn linux_cache_dir_falls_back_to_home_when_xdg_unset() {
398        let original_xdg = env::var("XDG_CACHE_HOME").ok();
399        let original_home = env::var("HOME").ok();
400
401        // SAFETY: Tests run single-threaded with --test-threads=1
402        unsafe {
403            remove_var("XDG_CACHE_HOME");
404            set_var("HOME", "/home/testuser");
405        }
406
407        let result = cache_dir();
408        assert_eq!(result, Some(PathBuf::from("/home/testuser/.cache")));
409
410        restore_var("XDG_CACHE_HOME", original_xdg);
411        restore_var("HOME", original_home);
412    }
413
414    #[test]
415    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
416    fn macos_cache_dir_uses_library_caches() {
417        let original = env::var("HOME").ok();
418        // SAFETY: Tests run single-threaded with --test-threads=1
419        unsafe { set_var("HOME", "/Users/testuser") };
420
421        let result = cache_dir();
422        assert_eq!(
423            result,
424            Some(PathBuf::from("/Users/testuser/Library/Caches"))
425        );
426
427        restore_var("HOME", original);
428    }
429
430    #[test]
431    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
432    fn macos_cache_dir_uses_xdg_style() {
433        let original = env::var("HOME").ok();
434        // SAFETY: Tests run single-threaded with --test-threads=1
435        unsafe { set_var("HOME", "/Users/testuser") };
436
437        let result = cache_dir();
438        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.cache")));
439
440        restore_var("HOME", original);
441    }
442
443    #[test]
444    #[cfg(target_os = "windows")]
445    fn windows_cache_dir_uses_localappdata() {
446        let original = env::var("LOCALAPPDATA").ok();
447        // SAFETY: Tests run single-threaded with --test-threads=1
448        unsafe { set_var("LOCALAPPDATA", "C:\\Users\\testuser\\AppData\\Local") };
449
450        let result = cache_dir();
451        assert_eq!(
452            result,
453            Some(PathBuf::from("C:\\Users\\testuser\\AppData\\Local"))
454        );
455
456        restore_var("LOCALAPPDATA", original);
457    }
458
459    #[test]
460    fn cache_dir_path_is_absolute() {
461        let result = cache_dir();
462        if let Some(path) = result {
463            assert!(
464                path.is_absolute(),
465                "cache_dir should return an absolute path"
466            );
467        }
468    }
469
470    #[test]
471    #[cfg(target_os = "linux")]
472    fn linux_config_dir_handles_non_utf8_xdg() {
473        use std::ffi::OsStr;
474        use std::os::unix::ffi::OsStrExt;
475
476        let original = env::var_os("XDG_CONFIG_HOME");
477        let non_utf8 = OsStr::from_bytes(b"/tmp/\xff\xfe");
478        // SAFETY: Tests run single-threaded with --test-threads=1
479        unsafe { env::set_var("XDG_CONFIG_HOME", non_utf8) };
480
481        let result = config_dir();
482        let expected = PathBuf::from(non_utf8);
483        assert_eq!(result, Some(expected));
484
485        restore_var_os("XDG_CONFIG_HOME", original);
486    }
487
488    #[test]
489    #[cfg(target_os = "linux")]
490    fn linux_data_dir_handles_non_utf8_xdg() {
491        use std::ffi::OsStr;
492        use std::os::unix::ffi::OsStrExt;
493
494        let original = env::var_os("XDG_DATA_HOME");
495        let non_utf8 = OsStr::from_bytes(b"/tmp/\xff\xfe/data");
496        // SAFETY: Tests run single-threaded with --test-threads=1
497        unsafe { env::set_var("XDG_DATA_HOME", non_utf8) };
498
499        let result = data_dir();
500        assert_eq!(result, Some(PathBuf::from(non_utf8)));
501
502        restore_var_os("XDG_DATA_HOME", original);
503    }
504
505    #[test]
506    #[cfg(target_os = "linux")]
507    fn linux_cache_dir_handles_non_utf8_xdg() {
508        use std::ffi::OsStr;
509        use std::os::unix::ffi::OsStrExt;
510
511        let original = env::var_os("XDG_CACHE_HOME");
512        let non_utf8 = OsStr::from_bytes(b"/tmp/\xff\xfe/cache");
513        // SAFETY: Tests run single-threaded with --test-threads=1
514        unsafe { env::set_var("XDG_CACHE_HOME", non_utf8) };
515
516        let result = cache_dir();
517        assert_eq!(result, Some(PathBuf::from(non_utf8)));
518
519        restore_var_os("XDG_CACHE_HOME", original);
520    }
521
522    #[test]
523    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
524    fn macos_config_dir_handles_non_utf8_home() {
525        use std::ffi::OsStr;
526        use std::os::unix::ffi::OsStrExt;
527
528        let original = env::var_os("HOME");
529        let non_utf8_home = OsStr::from_bytes(b"/Users/\xff\xfe");
530        // SAFETY: Tests run single-threaded with --test-threads=1
531        unsafe { env::set_var("HOME", non_utf8_home) };
532
533        let result = config_dir();
534        let mut expected = PathBuf::from(non_utf8_home);
535        expected.push("Library");
536        expected.push("Application Support");
537        assert_eq!(result, Some(expected));
538
539        restore_var_os("HOME", original);
540    }
541
542    #[test]
543    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
544    fn macos_data_dir_handles_non_utf8_home() {
545        use std::ffi::OsStr;
546        use std::os::unix::ffi::OsStrExt;
547
548        let original = env::var_os("HOME");
549        let non_utf8_home = OsStr::from_bytes(b"/Users/\xff\xfe");
550        // SAFETY: Tests run single-threaded with --test-threads=1
551        unsafe { env::set_var("HOME", non_utf8_home) };
552
553        let result = data_dir();
554        let mut expected = PathBuf::from(non_utf8_home);
555        expected.push("Library");
556        expected.push("Application Support");
557        assert_eq!(result, Some(expected));
558
559        restore_var_os("HOME", original);
560    }
561
562    #[test]
563    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
564    fn macos_cache_dir_handles_non_utf8_home() {
565        use std::ffi::OsStr;
566        use std::os::unix::ffi::OsStrExt;
567
568        let original = env::var_os("HOME");
569        let non_utf8_home = OsStr::from_bytes(b"/Users/\xff\xfe");
570        // SAFETY: Tests run single-threaded with --test-threads=1
571        unsafe { env::set_var("HOME", non_utf8_home) };
572
573        let result = cache_dir();
574        let mut expected = PathBuf::from(non_utf8_home);
575        expected.push("Library");
576        expected.push("Caches");
577        assert_eq!(result, Some(expected));
578
579        restore_var_os("HOME", original);
580    }
581
582    #[test]
583    #[cfg(target_os = "linux")]
584    fn linux_config_dir_ignores_empty_xdg() {
585        let original_xdg = env::var("XDG_CONFIG_HOME").ok();
586        let original_home = env::var("HOME").ok();
587        // SAFETY: Tests run single-threaded with --test-threads=1
588        unsafe {
589            set_var("XDG_CONFIG_HOME", "");
590            set_var("HOME", "/home/testuser");
591        }
592
593        let result = config_dir();
594        assert_eq!(result, Some(PathBuf::from("/home/testuser/.config")));
595
596        restore_var("XDG_CONFIG_HOME", original_xdg);
597        restore_var("HOME", original_home);
598    }
599
600    #[test]
601    #[cfg(target_os = "linux")]
602    fn linux_data_dir_ignores_empty_xdg() {
603        let original_xdg = env::var("XDG_DATA_HOME").ok();
604        let original_home = env::var("HOME").ok();
605        // SAFETY: Tests run single-threaded with --test-threads=1
606        unsafe {
607            set_var("XDG_DATA_HOME", "");
608            set_var("HOME", "/home/testuser");
609        }
610
611        let result = data_dir();
612        assert_eq!(result, Some(PathBuf::from("/home/testuser/.local/share")));
613
614        restore_var("XDG_DATA_HOME", original_xdg);
615        restore_var("HOME", original_home);
616    }
617
618    #[test]
619    #[cfg(target_os = "linux")]
620    fn linux_cache_dir_ignores_empty_xdg() {
621        let original_xdg = env::var("XDG_CACHE_HOME").ok();
622        let original_home = env::var("HOME").ok();
623        // SAFETY: Tests run single-threaded with --test-threads=1
624        unsafe {
625            set_var("XDG_CACHE_HOME", "");
626            set_var("HOME", "/home/testuser");
627        }
628
629        let result = cache_dir();
630        assert_eq!(result, Some(PathBuf::from("/home/testuser/.cache")));
631
632        restore_var("XDG_CACHE_HOME", original_xdg);
633        restore_var("HOME", original_home);
634    }
635}