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(std::env::home_dir)
26            .map(|mut base| {
27                base.push(CONFIG_DIR);
28                base
29            })
30    } else if cfg!(target_os = "macos") {
31        // macOS: Use $HOME/Library/Application Support
32        //  or $HOME/.config if favor-xdg-style is enabled
33        std::env::home_dir().map(|mut home| {
34            if cfg!(feature = "favor-xdg-style") {
35                home.push(CONFIG_DIR);
36                return home;
37            }
38            home.push("Library");
39            home.push("Application Support");
40            home
41        })
42    } else if cfg!(target_os = "windows") {
43        // Windows: Use %APPDATA%
44        env::var_os("APPDATA")
45            .filter(|s| !s.is_empty())
46            .map(PathBuf::from)
47    } else {
48        // Unsupported platform
49        None
50    }
51}
52
53/// Returns the path to the user's data directory.
54///
55/// The returned value depends on the operating system and is either a `Some`, containing a value from the following table, or a `None`.
56///
57/// |Platform | Value                                 | Example                                  |
58/// | ------- | ------------------------------------- | ---------------------------------------- |
59/// | Linux   | `$XDG_DATA_HOME` or `$HOME`/.local/share | /home/alice/.local/share              |
60/// | macOS   | `$HOME`/Library/Application Support   | /Users/Alice/Library/Application Support |
61/// | Windows | `%LOCALAPPDATA%`                      | C:\Users\Alice\AppData\Local             |
62///
63/// NOTE: if the feature `favor-xdg-style` is enabled, `$HOME/.local/share` is favorized on macOS.
64pub fn data_dir() -> Option<PathBuf> {
65    if cfg!(target_os = "linux") {
66        // Linux: Use $XDG_DATA_HOME or $HOME/.local/share
67        env::var_os("XDG_DATA_HOME")
68            .filter(|s| !s.is_empty())
69            .map(PathBuf::from)
70            .or_else(|| {
71                std::env::home_dir().map(|mut home| {
72                    home.push(DATA_DIR);
73                    home
74                })
75            })
76    } else if cfg!(target_os = "macos") {
77        // macOS: Use $HOME/Library/Application Support
78        //  or $HOME/.local/share if favor-xdg-style is enabled
79        std::env::home_dir().map(|mut home| {
80            if cfg!(feature = "favor-xdg-style") {
81                home.push(DATA_DIR);
82                return home;
83            }
84            home.push("Library");
85            home.push("Application Support");
86            home
87        })
88    } else if cfg!(target_os = "windows") {
89        // Windows: Use %LOCALAPPDATA%
90        env::var_os("LOCALAPPDATA")
91            .filter(|s| !s.is_empty())
92            .map(PathBuf::from)
93    } else {
94        // Unsupported platform
95        None
96    }
97}
98
99/// Returns the path to the user's cache directory.
100///
101/// The returned value depends on the operating system and is either a `Some`, containing a value from the following table, or a `None`.
102///
103/// |Platform | Value                                 | Example                                  |
104/// | ------- | ------------------------------------- | ---------------------------------------- |
105/// | Linux   | `$XDG_CACHE_HOME` or `$HOME`/.cache   | /home/alice/.cache                       |
106/// | macOS   | `$HOME`/Library/Caches                | /Users/Alice/Library/Caches              |
107/// | Windows | `%LOCALAPPDATA%`                      | C:\Users\Alice\AppData\Local             |
108///
109/// NOTE: if the feature `favor-xdg-style` is enabled, `$HOME/.cache` is favorized on macOS.
110pub fn cache_dir() -> Option<PathBuf> {
111    if cfg!(target_os = "linux") {
112        // Linux: Use $XDG_CACHE_HOME or $HOME/.cache
113        env::var_os("XDG_CACHE_HOME")
114            .filter(|s| !s.is_empty())
115            .map(PathBuf::from)
116            .or_else(|| {
117                std::env::home_dir().map(|mut home| {
118                    home.push(CACHE_DIR);
119                    home
120                })
121            })
122    } else if cfg!(target_os = "macos") {
123        // macOS: Use $HOME/Library/Caches
124        //  or $HOME/.cache if favor-xdg-style is enabled
125        std::env::home_dir().map(|mut home| {
126            if cfg!(feature = "favor-xdg-style") {
127                home.push(CACHE_DIR);
128                return home;
129            }
130            home.push("Library");
131            home.push("Caches");
132            home
133        })
134    } else if cfg!(target_os = "windows") {
135        // Windows: Use %LOCALAPPDATA%
136        env::var_os("LOCALAPPDATA")
137            .filter(|s| !s.is_empty())
138            .map(PathBuf::from)
139    } else {
140        // Unsupported platform
141        None
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    unsafe fn set_var(key: &str, value: &str) {
150        unsafe { env::set_var(key, value) };
151    }
152
153    unsafe fn remove_var(key: &str) {
154        unsafe { env::remove_var(key) };
155    }
156
157    fn restore_var(key: &str, original: Option<String>) {
158        // SAFETY: Tests run single-threaded with --test-threads=1
159        unsafe {
160            match original {
161                Some(val) => set_var(key, &val),
162                None => remove_var(key),
163            }
164        }
165    }
166
167    #[cfg(any(target_os = "linux", target_os = "macos"))]
168    fn restore_var_os(key: &str, original: Option<std::ffi::OsString>) {
169        // SAFETY: Tests run single-threaded with --test-threads=1
170        unsafe {
171            match original {
172                Some(val) => env::set_var(key, val),
173                None => env::remove_var(key),
174            }
175        }
176    }
177
178    #[test]
179    fn config_dir_returns_some() {
180        let result = config_dir();
181        assert!(
182            result.is_some(),
183            "config_dir should return Some on supported platforms"
184        );
185    }
186
187    #[test]
188    #[cfg(target_os = "linux")]
189    fn linux_uses_xdg_config_home_when_set() {
190        let original = env::var("XDG_CONFIG_HOME").ok();
191        // SAFETY: Tests run single-threaded with --test-threads=1
192        unsafe { set_var("XDG_CONFIG_HOME", "/custom/config") };
193
194        let result = config_dir();
195        assert_eq!(result, Some(PathBuf::from("/custom/config/.config")));
196
197        restore_var("XDG_CONFIG_HOME", original);
198    }
199
200    #[test]
201    #[cfg(target_os = "linux")]
202    fn linux_falls_back_to_home_when_xdg_unset() {
203        let original_xdg = env::var("XDG_CONFIG_HOME").ok();
204        let original_home = env::var("HOME").ok();
205
206        // SAFETY: Tests run single-threaded with --test-threads=1
207        unsafe {
208            remove_var("XDG_CONFIG_HOME");
209            set_var("HOME", "/home/testuser");
210        }
211
212        let result = config_dir();
213        assert_eq!(result, Some(PathBuf::from("/home/testuser/.config")));
214
215        restore_var("XDG_CONFIG_HOME", original_xdg);
216        restore_var("HOME", original_home);
217    }
218
219    #[test]
220    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
221    fn macos_config_dir_uses_library_application_support() {
222        let original = env::var("HOME").ok();
223        // SAFETY: Tests run single-threaded with --test-threads=1
224        unsafe { set_var("HOME", "/Users/testuser") };
225
226        let result = config_dir();
227        assert_eq!(
228            result,
229            Some(PathBuf::from("/Users/testuser/Library/Application Support"))
230        );
231
232        restore_var("HOME", original);
233    }
234
235    #[test]
236    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
237    fn macos_config_dir_uses_xdg_style() {
238        let original = env::var("HOME").ok();
239        // SAFETY: Tests run single-threaded with --test-threads=1
240        unsafe { set_var("HOME", "/Users/testuser") };
241
242        let result = config_dir();
243        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.config")));
244
245        restore_var("HOME", original);
246    }
247
248    #[test]
249    #[cfg(target_os = "windows")]
250    fn windows_uses_appdata() {
251        let original = env::var("APPDATA").ok();
252        // SAFETY: Tests run single-threaded with --test-threads=1
253        unsafe { set_var("APPDATA", "C:\\Users\\testuser\\AppData\\Roaming") };
254
255        let result = config_dir();
256        assert_eq!(
257            result,
258            Some(PathBuf::from("C:\\Users\\testuser\\AppData\\Roaming"))
259        );
260
261        restore_var("APPDATA", original);
262    }
263
264    #[test]
265    fn config_dir_path_is_absolute() {
266        let result = config_dir();
267        if let Some(path) = result {
268            assert!(
269                path.is_absolute(),
270                "config_dir should return an absolute path"
271            );
272        }
273    }
274
275    #[test]
276    fn data_dir_returns_some() {
277        let result = data_dir();
278        assert!(
279            result.is_some(),
280            "data_dir should return Some on supported platforms"
281        );
282    }
283
284    #[test]
285    #[cfg(target_os = "linux")]
286    fn linux_data_dir_uses_xdg_data_home_when_set() {
287        let original = env::var("XDG_DATA_HOME").ok();
288        // SAFETY: Tests run single-threaded with --test-threads=1
289        unsafe { set_var("XDG_DATA_HOME", "/custom/data") };
290
291        let result = data_dir();
292        assert_eq!(result, Some(PathBuf::from("/custom/data")));
293
294        restore_var("XDG_DATA_HOME", original);
295    }
296
297    #[test]
298    #[cfg(target_os = "linux")]
299    fn linux_data_dir_falls_back_to_home_when_xdg_unset() {
300        let original_xdg = env::var("XDG_DATA_HOME").ok();
301        let original_home = env::var("HOME").ok();
302
303        // SAFETY: Tests run single-threaded with --test-threads=1
304        unsafe {
305            remove_var("XDG_DATA_HOME");
306            set_var("HOME", "/home/testuser");
307        }
308
309        let result = data_dir();
310        assert_eq!(result, Some(PathBuf::from("/home/testuser/.local/share")));
311
312        restore_var("XDG_DATA_HOME", original_xdg);
313        restore_var("HOME", original_home);
314    }
315
316    #[test]
317    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
318    fn macos_data_dir_uses_library_application_support() {
319        let original = env::var("HOME").ok();
320        // SAFETY: Tests run single-threaded with --test-threads=1
321        unsafe { set_var("HOME", "/Users/testuser") };
322
323        let result = data_dir();
324        assert_eq!(
325            result,
326            Some(PathBuf::from("/Users/testuser/Library/Application Support"))
327        );
328
329        restore_var("HOME", original);
330    }
331
332    #[test]
333    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
334    fn macos_data_dir_uses_xdg_style() {
335        let original = env::var("HOME").ok();
336        // SAFETY: Tests run single-threaded with --test-threads=1
337        unsafe { set_var("HOME", "/Users/testuser") };
338
339        let result = data_dir();
340        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.local/share")));
341
342        restore_var("HOME", original);
343    }
344
345    #[test]
346    #[cfg(target_os = "windows")]
347    fn windows_data_dir_uses_localappdata() {
348        let original = env::var("LOCALAPPDATA").ok();
349        // SAFETY: Tests run single-threaded with --test-threads=1
350        unsafe { set_var("LOCALAPPDATA", "C:\\Users\\runneradmin\\AppData\\Local") };
351
352        let result = data_dir();
353        assert_eq!(
354            result,
355            Some(PathBuf::from("C:\\Users\\runneradmin\\AppData\\Local"))
356        );
357
358        restore_var("LOCALAPPDATA", original);
359    }
360
361    #[test]
362    fn data_dir_path_is_absolute() {
363        let result = data_dir();
364        if let Some(path) = result {
365            assert!(
366                path.is_absolute(),
367                "data_dir should return an absolute path"
368            );
369        }
370    }
371
372    #[test]
373    fn cache_dir_returns_some() {
374        let result = cache_dir();
375        assert!(
376            result.is_some(),
377            "cache_dir should return Some on supported platforms"
378        );
379    }
380
381    #[test]
382    #[cfg(target_os = "linux")]
383    fn linux_cache_dir_uses_xdg_cache_home_when_set() {
384        let original = env::var("XDG_CACHE_HOME").ok();
385        // SAFETY: Tests run single-threaded with --test-threads=1
386        unsafe { set_var("XDG_CACHE_HOME", "/custom/cache") };
387
388        let result = cache_dir();
389        assert_eq!(result, Some(PathBuf::from("/custom/cache")));
390
391        restore_var("XDG_CACHE_HOME", original);
392    }
393
394    #[test]
395    #[cfg(target_os = "linux")]
396    fn linux_cache_dir_falls_back_to_home_when_xdg_unset() {
397        let original_xdg = env::var("XDG_CACHE_HOME").ok();
398        let original_home = env::var("HOME").ok();
399
400        // SAFETY: Tests run single-threaded with --test-threads=1
401        unsafe {
402            remove_var("XDG_CACHE_HOME");
403            set_var("HOME", "/home/testuser");
404        }
405
406        let result = cache_dir();
407        assert_eq!(result, Some(PathBuf::from("/home/testuser/.cache")));
408
409        restore_var("XDG_CACHE_HOME", original_xdg);
410        restore_var("HOME", original_home);
411    }
412
413    #[test]
414    #[cfg(all(target_os = "macos", not(feature = "favor-xdg-style")))]
415    fn macos_cache_dir_uses_library_caches() {
416        let original = env::var("HOME").ok();
417        // SAFETY: Tests run single-threaded with --test-threads=1
418        unsafe { set_var("HOME", "/Users/testuser") };
419
420        let result = cache_dir();
421        assert_eq!(
422            result,
423            Some(PathBuf::from("/Users/testuser/Library/Caches"))
424        );
425
426        restore_var("HOME", original);
427    }
428
429    #[test]
430    #[cfg(all(target_os = "macos", feature = "favor-xdg-style"))]
431    fn macos_cache_dir_uses_xdg_style() {
432        let original = env::var("HOME").ok();
433        // SAFETY: Tests run single-threaded with --test-threads=1
434        unsafe { set_var("HOME", "/Users/testuser") };
435
436        let result = cache_dir();
437        assert_eq!(result, Some(PathBuf::from("/Users/testuser/.cache")));
438
439        restore_var("HOME", original);
440    }
441
442    #[test]
443    #[cfg(target_os = "windows")]
444    fn windows_cache_dir_uses_localappdata() {
445        let original = env::var("LOCALAPPDATA").ok();
446        // SAFETY: Tests run single-threaded with --test-threads=1
447        unsafe { set_var("LOCALAPPDATA", "C:\\Users\\testuser\\AppData\\Local") };
448
449        let result = cache_dir();
450        assert_eq!(
451            result,
452            Some(PathBuf::from("C:\\Users\\testuser\\AppData\\Local"))
453        );
454
455        restore_var("LOCALAPPDATA", original);
456    }
457
458    #[test]
459    fn cache_dir_path_is_absolute() {
460        let result = cache_dir();
461        if let Some(path) = result {
462            assert!(
463                path.is_absolute(),
464                "cache_dir should return an absolute path"
465            );
466        }
467    }
468
469    #[test]
470    #[cfg(target_os = "linux")]
471    fn linux_config_dir_handles_non_utf8_xdg() {
472        use std::ffi::OsStr;
473        use std::os::unix::ffi::OsStrExt;
474
475        let original = env::var_os("XDG_CONFIG_HOME");
476        let non_utf8 = OsStr::from_bytes(b"/tmp/\xff\xfe");
477        // SAFETY: Tests run single-threaded with --test-threads=1
478        unsafe { env::set_var("XDG_CONFIG_HOME", non_utf8) };
479
480        let result = config_dir();
481        let mut expected = PathBuf::from(non_utf8);
482        expected.push(".config");
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}