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