cross_xdg/
base_dirs.rs

1use std::path::{Path, PathBuf};
2
3/// The `BaseDirs` struct provides access to the XDG base directories.
4pub struct BaseDirs {
5    home: PathBuf,
6    config_home: PathBuf,
7    cache_home: PathBuf,
8    data_home: PathBuf,
9    state_home: PathBuf,
10    runtime_dir: Option<PathBuf>,
11}
12
13impl BaseDirs {
14    /// Creates a new `BaseDirs` struct without a prefix.
15    /// # Errors
16    /// Returns an error if the home directory could not be found.
17    pub fn new() -> Result<Self, std::io::Error> {
18        Self::internal_new(None)
19    }
20
21    /// Creates a new `BaseDirs` struct with a prefix. The prefix is appended to the XDG
22    /// directories. For example, if the prefix is `myapp`, the config directory will be
23    /// `~/.config/myapp`.
24    /// # Errors
25    /// Returns an error if the home directory could not be found.
26    pub fn with_prefix(prefix: impl Into<String>) -> Result<Self, std::io::Error> {
27        Self::internal_new(Some(prefix.into()))
28    }
29
30    /// Returns the home directory determined by
31    /// [std::env::home_dir()](https://doc.rust-lang.org/std/env/fn.home_dir.html).
32    pub fn home(&self) -> &Path {
33        &self.home
34    }
35
36    /// Returns the config directory specified by the `XDG_CONFIG_HOME` environment variable.
37    /// If the environment variable is not set, the default is [home()](#method.home) + `/.config`.
38    ///
39    /// # Without a prefix
40    /// For example: \
41    /// On Linux: `/home/<user>/.config` \
42    /// On Windows: `C:\Users\<user>\.config` \
43    /// On macOS: `/Users/<user>/.config`
44    ///
45    /// # With a prefix
46    /// For example: \
47    /// On Linux: `/home/<user>/.config/<prefix>` \
48    /// On Windows: `C:\Users\<user>\.config\<prefix>` \
49    /// On macOS: `/Users/<user>/.config/<prefix>`
50    ///
51    /// This function does not create the directory, nor does it check if the directory exists.
52    /// Use [crate::BaseDirsEx::create()] to create the directory.
53    pub fn config_home(&self) -> &Path {
54        &self.config_home
55    }
56
57    /// Returns the cache directory specified by the `XDG_CACHE_HOME` environment variable.
58    /// See [config_home()](#method.config_home) for more information.
59    pub fn cache_home(&self) -> &Path {
60        &self.cache_home
61    }
62
63    /// Returns the data directory specified by the `XDG_DATA_HOME` environment variable.
64    /// See [config_home()](#method.config_home) for more information.
65    pub fn data_home(&self) -> &Path {
66        &self.data_home
67    }
68
69    /// Returns the state directory specified by the `XDG_STATE_HOME` environment variable.
70    /// See [config_home()](#method.config_home) for more information.
71    pub fn state_home(&self) -> &Path {
72        &self.state_home
73    }
74
75    /// Returns the runtime directory specified by the `XDG_RUNTIME_DIR` environment variable.
76    /// If the environment variable is not set, this function returns `None`.
77    pub fn runtime_dir(&self) -> Option<&Path> {
78        self.runtime_dir.as_deref()
79    }
80
81    fn internal_new(prefix: Option<String>) -> Result<Self, std::io::Error> {
82        // Ignore deprecation warning, as home_dir was fixed in Rust 1.85 and the warning will be
83        // removed in 1.86
84        #[allow(deprecated)]
85        let tmp_home = std::env::home_dir().ok_or(std::io::Error::new(
86            std::io::ErrorKind::NotFound,
87            "Could not find home directory",
88        ))?;
89
90        Ok(Self {
91            config_home: Self::get_config_home(&tmp_home, &prefix),
92            cache_home: Self::get_cache_home(&tmp_home, &prefix),
93            data_home: Self::get_data_home(&tmp_home, &prefix),
94            state_home: Self::get_state_home(&tmp_home, &prefix),
95            runtime_dir: Self::get_runtime_dir(&prefix),
96            home: tmp_home,
97        })
98    }
99
100    fn get_cache_home(home: &Path, prefix: &Option<String>) -> PathBuf {
101        Self::get_dir(home, "XDG_CACHE_HOME", ".cache", prefix)
102    }
103
104    fn get_data_home(home: &Path, prefix: &Option<String>) -> PathBuf {
105        Self::get_dir(home, "XDG_DATA_HOME", ".local/share", prefix)
106    }
107
108    fn get_state_home(home: &Path, prefix: &Option<String>) -> PathBuf {
109        Self::get_dir(home, "XDG_STATE_HOME", ".local/state", prefix)
110    }
111
112    fn get_runtime_dir(prefix: &Option<String>) -> Option<PathBuf> {
113        let dir = std::env::var("XDG_RUNTIME_DIR").map(PathBuf::from).ok();
114        match (&dir, prefix) {
115            (Some(dir), Some(prefix)) => Some(dir.join(prefix)),
116            _ => dir, // Either both are None or prefix is None
117        }
118    }
119
120    fn get_config_home(home: &Path, prefix: &Option<String>) -> PathBuf {
121        Self::get_dir(home, "XDG_CONFIG_HOME", ".config", prefix)
122    }
123
124    fn get_dir(home: &Path, xdg_var: &str, default: &str, prefix: &Option<String>) -> PathBuf {
125        let xdg_dir = std::env::var(xdg_var).ok();
126
127        let dir = if let Some(dir) = xdg_dir {
128            PathBuf::from(dir)
129        } else {
130            home.join(default)
131        };
132
133        match prefix {
134            Some(prefix) => dir.join(prefix),
135            None => dir,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::test_helper::{get_home, set_var, unset_var};
144    use serial_test::serial;
145
146    mod without_prefix {
147
148        use super::*;
149
150        #[test]
151        #[serial]
152        fn config_home_no_xdg_set_returns_correct_path() {
153            unset_var("XDG_CONFIG_HOME");
154            let base_dir = BaseDirs::new().unwrap();
155            assert_eq!(base_dir.config_home(), &get_home().join(".config"));
156        }
157
158        #[test]
159        #[serial]
160        fn config_home_xdg_set_returns_correct_path() {
161            set_var("XDG_CONFIG_HOME", "/tmp/config");
162            let base_dir = BaseDirs::new().unwrap();
163            assert_eq!(base_dir.config_home(), Path::new("/tmp/config"));
164        }
165
166        #[test]
167        #[serial]
168        fn cache_home_no_xdg_set_returns_correct_path() {
169            unset_var("XDG_CACHE_HOME");
170            let base_dir = BaseDirs::new().unwrap();
171            assert_eq!(base_dir.cache_home(), &get_home().join(".cache"));
172        }
173
174        #[test]
175        #[serial]
176        fn cache_home_xdg_set_returns_correct_path() {
177            set_var("XDG_CACHE_HOME", "/tmp/cache");
178            let base_dir = BaseDirs::new().unwrap();
179            assert_eq!(base_dir.cache_home(), Path::new("/tmp/cache"));
180        }
181
182        #[test]
183        #[serial]
184        fn data_home_no_xdg_set_returns_correct_path() {
185            unset_var("XDG_DATA_HOME");
186            let base_dir = BaseDirs::new().unwrap();
187            assert_eq!(base_dir.data_home(), &get_home().join(".local/share"));
188        }
189
190        #[test]
191        #[serial]
192        fn data_home_xdg_set_returns_correct_path() {
193            set_var("XDG_DATA_HOME", "/tmp/data");
194            let base_dir = BaseDirs::new().unwrap();
195            assert_eq!(base_dir.data_home(), Path::new("/tmp/data"));
196        }
197
198        #[test]
199        #[serial]
200        fn state_home_no_xdg_set_returns_correct_path() {
201            unset_var("XDG_STATE_HOME");
202            let base_dir = BaseDirs::new().unwrap();
203            assert_eq!(base_dir.state_home(), &get_home().join(".local/state"));
204        }
205
206        #[test]
207        #[serial]
208        fn state_home_xdg_set_returns_correct_path() {
209            set_var("XDG_STATE_HOME", "/tmp/state");
210            let base_dir = BaseDirs::new().unwrap();
211            assert_eq!(base_dir.state_home(), Path::new("/tmp/state"));
212        }
213
214        #[test]
215        #[serial]
216        fn runtime_dir_no_xdg_set_returns_none() {
217            unset_var("XDG_RUNTIME_DIR");
218            let base_dir = BaseDirs::new().unwrap();
219            assert_eq!(base_dir.runtime_dir(), None);
220        }
221
222        #[test]
223        #[serial]
224        fn runtime_dir_xdg_set_returns_correct_path() {
225            set_var("XDG_RUNTIME_DIR", "/tmp/runtime");
226            let base_dir = BaseDirs::new().unwrap();
227            assert_eq!(base_dir.runtime_dir(), Some(Path::new("/tmp/runtime")));
228        }
229    }
230
231    mod with_prefix {
232        use super::*;
233
234        #[test]
235        #[serial]
236        fn config_home_no_xdg_set_returns_correct_path() {
237            unset_var("XDG_CONFIG_HOME");
238            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
239            assert_eq!(base_dir.config_home(), &get_home().join(".config/prefix"));
240        }
241
242        #[test]
243        #[serial]
244        fn config_home_xdg_set_returns_correct_path() {
245            set_var("XDG_CONFIG_HOME", "/tmp/config");
246            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
247            assert_eq!(base_dir.config_home(), Path::new("/tmp/config/prefix"));
248        }
249
250        #[test]
251        #[serial]
252        fn cache_home_no_xdg_set_returns_correct_path() {
253            unset_var("XDG_CACHE_HOME");
254            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
255            assert_eq!(base_dir.cache_home(), &get_home().join(".cache/prefix"));
256        }
257
258        #[test]
259        #[serial]
260        fn cache_home_xdg_set_returns_correct_path() {
261            set_var("XDG_CACHE_HOME", "/tmp/cache");
262            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
263            assert_eq!(base_dir.cache_home(), Path::new("/tmp/cache/prefix"));
264        }
265
266        #[test]
267        #[serial]
268        fn data_home_no_xdg_set_returns_correct_path() {
269            unset_var("XDG_DATA_HOME");
270            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
271            assert_eq!(
272                base_dir.data_home(),
273                &get_home().join(".local/share/prefix")
274            );
275        }
276
277        #[test]
278        #[serial]
279        fn data_home_xdg_set_returns_correct_path() {
280            set_var("XDG_DATA_HOME", "/tmp/data");
281            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
282            assert_eq!(base_dir.data_home(), Path::new("/tmp/data/prefix"));
283        }
284
285        #[test]
286        #[serial]
287        fn state_home_no_xdg_set_returns_correct_path() {
288            unset_var("XDG_STATE_HOME");
289            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
290            assert_eq!(
291                base_dir.state_home(),
292                &get_home().join(".local/state/prefix")
293            );
294        }
295
296        #[test]
297        #[serial]
298        fn state_home_xdg_set_returns_correct_path() {
299            set_var("XDG_STATE_HOME", "/tmp/state");
300            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
301            assert_eq!(base_dir.state_home(), Path::new("/tmp/state/prefix"));
302        }
303
304        #[test]
305        #[serial]
306        fn runtime_dir_no_xdg_set_returns_none() {
307            unset_var("XDG_RUNTIME_DIR");
308            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
309            assert_eq!(base_dir.runtime_dir(), None);
310        }
311
312        #[test]
313        #[serial]
314        fn runtime_dir_xdg_set_returns_correct_path() {
315            set_var("XDG_RUNTIME_DIR", "/tmp/runtime");
316            let base_dir = BaseDirs::with_prefix("prefix").unwrap();
317            assert_eq!(
318                base_dir.runtime_dir(),
319                Some(Path::new("/tmp/runtime/prefix"))
320            );
321        }
322    }
323}