xdg/
base_directories.rs

1use std::collections::HashSet;
2use std::ffi::OsString;
3use std::os::unix::fs::PermissionsExt;
4use std::path::{Path, PathBuf};
5use std::{env, error, fmt, fs, io};
6
7#[cfg(feature = "serde")]
8use serde::{Deserialize, Serialize};
9
10use self::ErrorKind::*;
11
12/// BaseDirectories allows to look up paths to configuration, data,
13/// cache and runtime files in well-known locations according to
14/// the [X Desktop Group Base Directory specification][xdg-basedir].
15///
16/// [xdg-basedir]: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
17///
18/// The Base Directory specification defines five kinds of files:
19///
20///   * **Configuration files** store the application's settings and
21///     are often modified during runtime;
22///   * **Data files** store supplementary data, such as graphic assets,
23///     precomputed tables, documentation, or architecture-independent
24///     source code;
25///   * **Cache files** store non-essential, transient data that provides
26///     a runtime speedup;
27///   * **State files** store logs, history, recently used files and application
28///     state (window size, open files, unsaved changes, …);
29///   * **Runtime files** include filesystem objects such are sockets or
30///     named pipes that are used for communication internal to the application.
31///     Runtime files must not be accessible to anyone except current user.
32///
33/// # Examples
34///
35/// To configure paths for application `myapp`:
36///
37/// ```
38/// let xdg_dirs = xdg::BaseDirectories::with_prefix("myapp");
39/// ```
40///
41/// To store configuration:
42///
43/// ```
44/// # use std::fs::File;
45/// # use std::io::{Error, Write};
46/// # fn main() -> Result<(), Error> {
47/// # let xdg_dirs = xdg::BaseDirectories::with_prefix("myapp");
48/// let config_path = xdg_dirs
49///     .place_config_file("config.ini")
50///     .expect("cannot create configuration directory");
51/// let mut config_file = File::create(config_path)?;
52/// write!(&mut config_file, "configured = 1")?;
53/// #   Ok(())
54/// # }
55/// ```
56///
57/// The `config.ini` file will appear in the proper location for desktop
58/// configuration files, most likely `~/.config/myapp/config.ini`.
59/// The leading directories will be automatically created.
60///
61/// To retrieve supplementary data:
62///
63/// ```no_run
64/// # use std::fs::File;
65/// # use std::io::{Error, Read, Write};
66/// # fn main() -> Result<(), Error> {
67/// # let xdg_dirs = xdg::BaseDirectories::with_prefix("myapp");
68/// let logo_path = xdg_dirs
69///     .find_data_file("logo.png")
70///     .expect("application data not present");
71/// let mut logo_file = File::open(logo_path)?;
72/// let mut logo = Vec::new();
73/// logo_file.read_to_end(&mut logo)?;
74/// #   Ok(())
75/// # }
76/// ```
77///
78/// The `logo.png` will be searched in the proper locations for
79/// supplementary data files, most likely `~/.local/share/myapp/logo.png`,
80/// then `/usr/local/share/myapp/logo.png` and `/usr/share/myapp/logo.png`.
81#[derive(Debug, Clone)]
82#[non_exhaustive]
83#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84pub struct BaseDirectories {
85    /// Prefix path appended to all path lookups in system directories as described in [`BaseDirectories::with_prefix`].
86    /// May be the empty path.
87    pub shared_prefix: PathBuf,
88    /// Prefix path appended to all path lookups in user directories as described in [`BaseDirectories::with_profile`].
89    /// Note that this value already contains `shared_prefix` as prefix, and is identical to it when constructed with
90    /// [`BaseDirectories::with_prefix`]. May be the empty path.
91    pub user_prefix: PathBuf,
92    /// Like [`BaseDirectories::get_data_home`], but without any prefixes applied.
93    /// Is guaranteed to not be `None` unless no HOME could be found.
94    pub data_home: Option<PathBuf>,
95    /// Like [`BaseDirectories::get_config_home`], but without any prefixes applied.
96    /// Is guaranteed to not be `None` unless no HOME could be found.
97    pub config_home: Option<PathBuf>,
98    /// Like [`BaseDirectories::get_cache_home`], but without any prefixes applied.
99    /// Is guaranteed to not be `None` unless no HOME could be found.
100    pub cache_home: Option<PathBuf>,
101    /// Like [`BaseDirectories::get_state_home`], but without any prefixes applied.
102    /// Is guaranteed to not be `None` unless no HOME could be found.
103    pub state_home: Option<PathBuf>,
104    /// Like [`BaseDirectories::get_data_dirs`], but without any prefixes applied.
105    pub data_dirs: Vec<PathBuf>,
106    /// Like [`BaseDirectories::get_config_dirs`], but without any prefixes applied.
107    pub config_dirs: Vec<PathBuf>,
108    /// Like [`BaseDirectories::get_runtime_directory`], but without any of the sanity checks
109    /// on the directory (like permissions).
110    pub runtime_dir: Option<PathBuf>,
111}
112
113pub struct Error {
114    kind: ErrorKind,
115}
116
117impl Error {
118    const fn new(kind: ErrorKind) -> Error {
119        Error { kind }
120    }
121}
122
123impl fmt::Debug for Error {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        self.kind.fmt(f)
126    }
127}
128
129impl error::Error for Error {
130    fn description(&self) -> &str {
131        match self.kind {
132            HomeMissing => "$HOME must be set",
133            XdgRuntimeDirInaccessible(_, _) => {
134                "$XDG_RUNTIME_DIR must be accessible by the current user"
135            }
136            XdgRuntimeDirInsecure(_, _) => "$XDG_RUNTIME_DIR must be secure: have permissions 0700",
137            XdgRuntimeDirMissing => "$XDG_RUNTIME_DIR is not set",
138        }
139    }
140    fn cause(&self) -> Option<&dyn error::Error> {
141        match self.kind {
142            XdgRuntimeDirInaccessible(_, ref e) => Some(e),
143            _ => None,
144        }
145    }
146}
147
148impl fmt::Display for Error {
149    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150        match self.kind {
151            HomeMissing => write!(f, "$HOME must be set"),
152            XdgRuntimeDirInaccessible(ref dir, ref error) => {
153                write!(
154                    f,
155                    "$XDG_RUNTIME_DIR (`{}`) must be accessible \
156                           by the current user (error: {})",
157                    dir.display(),
158                    error
159                )
160            }
161            XdgRuntimeDirInsecure(ref dir, permissions) => {
162                write!(
163                    f,
164                    "$XDG_RUNTIME_DIR (`{}`) must be secure: must have \
165                           permissions 0o700, got {}",
166                    dir.display(),
167                    permissions
168                )
169            }
170            XdgRuntimeDirMissing => {
171                write!(f, "$XDG_RUNTIME_DIR must be set")
172            }
173        }
174    }
175}
176
177impl From<Error> for io::Error {
178    fn from(error: Error) -> io::Error {
179        match error.kind {
180            HomeMissing | XdgRuntimeDirMissing => io::Error::new(io::ErrorKind::NotFound, error),
181            _ => io::Error::new(io::ErrorKind::Other, error),
182        }
183    }
184}
185
186#[derive(Copy, Clone)]
187struct Permissions(u32);
188
189impl fmt::Debug for Permissions {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        let Permissions(p) = *self;
192        write!(f, "{:#05o}", p)
193    }
194}
195
196impl fmt::Display for Permissions {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        fmt::Debug::fmt(self, f)
199    }
200}
201
202#[derive(Debug)]
203enum ErrorKind {
204    HomeMissing,
205    XdgRuntimeDirInaccessible(PathBuf, io::Error),
206    XdgRuntimeDirInsecure(PathBuf, Permissions),
207    XdgRuntimeDirMissing,
208}
209
210impl BaseDirectories {
211    /// Reads the process environment, determines the XDG base directories,
212    /// and returns a value that can be used for lookup.
213    /// The following environment variables are examined:
214    ///
215    ///   * `HOME`; if not set: use the same fallback as `std::env::home_dir()`;
216    ///   * `XDG_DATA_HOME`; if not set: assumed to be `$HOME/.local/share`.
217    ///   * `XDG_CONFIG_HOME`; if not set: assumed to be `$HOME/.config`.
218    ///   * `XDG_CACHE_HOME`; if not set: assumed to be `$HOME/.cache`.
219    ///   * `XDG_STATE_HOME`; if not set: assumed to be `$HOME/.local/state`.
220    ///   * `XDG_DATA_DIRS`; if not set: assumed to be `/usr/local/share:/usr/share`.
221    ///   * `XDG_CONFIG_DIRS`; if not set: assumed to be `/etc/xdg`.
222    ///   * `XDG_RUNTIME_DIR`; if not accessible or permissions are not `0700`:
223    ///     record as inaccessible (can be queried with
224    ///     [has_runtime_directory](method.has_runtime_directory)).
225    ///
226    /// As per specification, if an environment variable contains a relative path,
227    /// the behavior is the same as if it was not set.
228    pub fn new() -> BaseDirectories {
229        BaseDirectories::with_env("", "", &|name| env::var_os(name))
230    }
231
232    /// Same as [`new()`](#method.new), but `prefix` is implicitly prepended to
233    /// every path that is looked up. This is usually the application's name,
234    /// preferably in [Reverse domain name notation](https://en.wikipedia.org/wiki/Reverse_domain_name_notation)
235    /// (The spec does not mandate this though, it's just a convention).
236    pub fn with_prefix<P: AsRef<Path>>(prefix: P) -> BaseDirectories {
237        BaseDirectories::with_env(prefix, "", &|name| env::var_os(name))
238    }
239
240    /// Same as [`with_prefix()`](#method.with_prefix),
241    /// with `profile` also implicitly prepended to every path that is looked up,
242    /// but only for user-specific directories.
243    ///
244    /// This allows each user to have mutliple "profiles" with different user-specific data.
245    ///
246    /// For example:
247    ///
248    /// ```
249    /// # extern crate xdg;
250    /// # use xdg::BaseDirectories;
251    /// let dirs = BaseDirectories::with_profile("program-name", "profile-name");
252    /// dirs.find_data_file("bar.jpg");
253    /// dirs.find_config_file("foo.conf");
254    /// ```
255    ///
256    /// will find `/usr/share/program-name/bar.jpg` (without `profile-name`)
257    /// and `~/.config/program-name/profile-name/foo.conf`.
258    pub fn with_profile<P1, P2>(prefix: P1, profile: P2) -> BaseDirectories
259    where
260        P1: AsRef<Path>,
261        P2: AsRef<Path>,
262    {
263        BaseDirectories::with_env(prefix, profile, &|name| env::var_os(name))
264    }
265
266    fn with_env<P1, P2, T>(prefix: P1, profile: P2, env_var: &T) -> BaseDirectories
267        where
268            P1: AsRef<Path>,
269            P2: AsRef<Path>,
270            T: ?Sized + Fn(&str) -> Option<OsString>,
271        {
272        BaseDirectories::with_env_impl(prefix.as_ref(), profile.as_ref(), env_var)
273    }
274
275    fn with_env_impl<T>(prefix: &Path, profile: &Path, env_var: &T) -> BaseDirectories
276        where
277            T: ?Sized + Fn(&str) -> Option<OsString>,
278        {
279        fn abspath(path: OsString) -> Option<PathBuf> {
280            let path: PathBuf = PathBuf::from(path);
281            if path.is_absolute() {
282                Some(path)
283            } else {
284                None
285            }
286        }
287
288        fn abspaths(paths: OsString) -> Option<Vec<PathBuf>> {
289            let paths: Vec<PathBuf> = env::split_paths(&paths)
290                .map(PathBuf::from)
291                .filter(|path| path.is_absolute())
292                .collect::<Vec<_>>();
293            if paths.is_empty() {
294                None
295            } else {
296                Some(paths)
297            }
298        }
299
300        // This crate only supports Unix, and the behavior of `std::env::home_dir()` is only
301        // problematic on Windows.
302        #[allow(deprecated)]
303        let home: Option<PathBuf> = std::env::home_dir();
304
305        let data_home = env_var("XDG_DATA_HOME")
306            .and_then(abspath)
307            .or_else(|| home.as_ref().map(|home| home.join(".local/share")));
308        let config_home = env_var("XDG_CONFIG_HOME")
309            .and_then(abspath)
310            .or_else(|| home.as_ref().map(|home| home.join(".config")));
311        let cache_home = env_var("XDG_CACHE_HOME")
312            .and_then(abspath)
313            .or_else(|| home.as_ref().map(|home| home.join(".cache")));
314        let state_home = env_var("XDG_STATE_HOME")
315            .and_then(abspath)
316            .or_else(|| home.as_ref().map(|home| home.join(".local/state")));
317        let data_dirs = env_var("XDG_DATA_DIRS").and_then(abspaths).unwrap_or(vec![
318            PathBuf::from("/usr/local/share"),
319            PathBuf::from("/usr/share"),
320        ]);
321        let config_dirs = env_var("XDG_CONFIG_DIRS")
322            .and_then(abspaths)
323            .unwrap_or(vec![PathBuf::from("/etc/xdg")]);
324        let runtime_dir = env_var("XDG_RUNTIME_DIR").and_then(abspath); // optional
325
326        let prefix: PathBuf = PathBuf::from(prefix);
327        BaseDirectories {
328            user_prefix: prefix.join(profile),
329            shared_prefix: prefix,
330            data_home,
331            config_home,
332            cache_home,
333            state_home,
334            data_dirs,
335            config_dirs,
336            runtime_dir,
337        }
338    }
339
340    /// Returns the user-specific runtime directory (set by `XDG_RUNTIME_DIR`).
341    pub fn get_runtime_directory(&self) -> Result<&PathBuf, Error> {
342        if let Some(ref runtime_dir) = self.runtime_dir {
343            // If XDG_RUNTIME_DIR is in the environment but not secure,
344            // do not allow recovery.
345            fs::read_dir(runtime_dir)
346                .map_err(|e| Error::new(XdgRuntimeDirInaccessible(runtime_dir.clone(), e)))?;
347            let permissions: u32 = fs::metadata(runtime_dir)
348                .map_err(|e| Error::new(XdgRuntimeDirInaccessible(runtime_dir.clone(), e)))?
349                .permissions()
350                .mode();
351            if permissions & 0o077 != 0 {
352                Err(Error::new(XdgRuntimeDirInsecure(
353                    runtime_dir.clone(),
354                    Permissions(permissions),
355                )))
356            } else {
357                Ok(runtime_dir)
358            }
359        } else {
360            Err(Error::new(XdgRuntimeDirMissing))
361        }
362    }
363
364    /// Returns `true` if `XDG_RUNTIME_DIR` is available, `false` otherwise.
365    pub fn has_runtime_directory(&self) -> bool {
366        self.get_runtime_directory().is_ok()
367    }
368
369    /// Like [`place_config_file()`](#method.place_config_file), but does
370    /// not create any directories.
371    pub fn get_config_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
372        self.config_home
373            .as_ref()
374            .map(|home| home.join(self.user_prefix.join(path)))
375    }
376
377    /// Like [`place_data_file()`](#method.place_data_file), but does
378    /// not create any directories.
379    pub fn get_data_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
380        self.data_home
381            .as_ref()
382            .map(|home| home.join(self.user_prefix.join(path)))
383    }
384
385    /// Like [`place_cache_file()`](#method.place_cache_file), but does
386    /// not create any directories.
387    pub fn get_cache_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
388        self.cache_home
389            .as_ref()
390            .map(|home| home.join(self.user_prefix.join(path)))
391    }
392
393    /// Like [`place_state_file()`](#method.place_state_file), but does
394    /// not create any directories.
395    pub fn get_state_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
396        self.state_home
397            .as_ref()
398            .map(|home| home.join(self.user_prefix.join(path)))
399    }
400
401    /// Like [`place_runtime_file()`](#method.place_runtime_file), but does
402    /// not create any directories.
403    /// If `XDG_RUNTIME_DIR` is not available, returns an error.
404    pub fn get_runtime_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
405        let runtime_dir = self.get_runtime_directory()?;
406        Ok(runtime_dir.join(self.user_prefix.join(path)))
407    }
408
409    /// Given a relative path `path`, returns an absolute path in
410    /// `XDG_CONFIG_HOME` where a configuration file may be stored.
411    /// Leading directories in the returned path are pre-created;
412    /// if that is not possible, an error is returned.
413    pub fn place_config_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
414        let config_home = self.config_home.as_ref().ok_or(Error::new(HomeMissing))?;
415        write_file(config_home, &self.user_prefix.join(path))
416    }
417
418    /// Like [`place_config_file()`](#method.place_config_file), but for
419    /// a data file in `XDG_DATA_HOME`.
420    pub fn place_data_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
421        let data_home = self.data_home.as_ref().ok_or(Error::new(HomeMissing))?;
422        write_file(data_home, &self.user_prefix.join(path))
423    }
424
425    /// Like [`place_config_file()`](#method.place_config_file), but for
426    /// a cache file in `XDG_CACHE_HOME`.
427    pub fn place_cache_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
428        let cache_home = self.cache_home.as_ref().ok_or(Error::new(HomeMissing))?;
429        write_file(cache_home, &self.user_prefix.join(path))
430    }
431
432    /// Like [`place_config_file()`](#method.place_config_file), but for
433    /// an application state file in `XDG_STATE_HOME`.
434    pub fn place_state_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
435        let state_home = self.state_home.as_ref().ok_or(Error::new(HomeMissing))?;
436        write_file(state_home, &self.user_prefix.join(path))
437    }
438
439    /// Like [`place_config_file()`](#method.place_config_file), but for
440    /// a runtime file in `XDG_RUNTIME_DIR`.
441    /// If `XDG_RUNTIME_DIR` is not available, returns an error.
442    pub fn place_runtime_file<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
443        write_file(self.get_runtime_directory()?, &self.user_prefix.join(path))
444    }
445
446    /// Given a relative path `path`, returns an absolute path to an existing
447    /// configuration file, or `None`. Searches `XDG_CONFIG_HOME` and then
448    /// `XDG_CONFIG_DIRS`.
449    pub fn find_config_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
450        read_file(
451            self.config_home.as_deref(),
452            &self.config_dirs,
453            &self.user_prefix,
454            &self.shared_prefix,
455            path.as_ref(),
456        )
457    }
458
459    /// Given a relative path `path`, returns an iterator yielding absolute
460    /// paths to existing configuration files, in `XDG_CONFIG_DIRS` and
461    /// `XDG_CONFIG_HOME`. Paths are produced in order from lowest priority
462    /// to highest.
463    pub fn find_config_files<P: AsRef<Path>>(&self, path: P) -> FileFindIterator {
464        FileFindIterator::new(
465            self.config_home.as_deref(),
466            &self.config_dirs,
467            &self.user_prefix,
468            &self.shared_prefix,
469            path.as_ref(),
470        )
471    }
472
473    /// Given a relative path `path`, returns an absolute path to an existing
474    /// data file, or `None`. Searches `XDG_DATA_HOME` and then
475    /// `XDG_DATA_DIRS`.
476    pub fn find_data_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
477        read_file(
478            self.data_home.as_deref(),
479            &self.data_dirs,
480            &self.user_prefix,
481            &self.shared_prefix,
482            path.as_ref(),
483        )
484    }
485
486    /// Given a relative path `path`, returns an iterator yielding absolute
487    /// paths to existing data files, in `XDG_DATA_DIRS` and
488    /// `XDG_DATA_HOME`. Paths are produced in order from lowest priority
489    /// to highest.
490    pub fn find_data_files<P: AsRef<Path>>(&self, path: P) -> FileFindIterator {
491        FileFindIterator::new(
492            self.data_home.as_deref(),
493            &self.data_dirs,
494            &self.user_prefix,
495            &self.shared_prefix,
496            path.as_ref(),
497        )
498    }
499
500    /// Given a relative path `path`, returns an absolute path to an existing
501    /// cache file, or `None`. Searches `XDG_CACHE_HOME`.
502    pub fn find_cache_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
503        read_file(
504            self.cache_home.as_deref(),
505            &Vec::new(),
506            &self.user_prefix,
507            &self.shared_prefix,
508            path.as_ref(),
509        )
510    }
511
512    /// Given a relative path `path`, returns an absolute path to an existing
513    /// application state file, or `None`. Searches `XDG_STATE_HOME`.
514    pub fn find_state_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
515        read_file(
516            self.state_home.as_deref(),
517            &Vec::new(),
518            &self.user_prefix,
519            &self.shared_prefix,
520            path.as_ref(),
521        )
522    }
523
524    /// Given a relative path `path`, returns an absolute path to an existing
525    /// runtime file, or `None`. Searches `XDG_RUNTIME_DIR`.
526    /// If `XDG_RUNTIME_DIR` is not available, returns `None`.
527    pub fn find_runtime_file<P: AsRef<Path>>(&self, path: P) -> Option<PathBuf> {
528        let runtime_dir = self.get_runtime_directory().ok()?;
529        read_file(
530            Some(runtime_dir),
531            &Vec::new(),
532            &self.user_prefix,
533            &self.shared_prefix,
534            path.as_ref(),
535        )
536    }
537
538    /// Given a relative path `path`, returns an absolute path to a configuration
539    /// directory in `XDG_CONFIG_HOME`. The directory and all directories
540    /// leading to it are created if they did not exist;
541    /// if that is not possible, an error is returned.
542    pub fn create_config_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
543        create_directory(
544            self.config_home.as_deref(),
545            &self.user_prefix.join(path),
546        )
547    }
548
549    /// Like [`create_config_directory()`](#method.create_config_directory),
550    /// but for a data directory in `XDG_DATA_HOME`.
551    pub fn create_data_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
552        create_directory(
553            self.data_home.as_deref(),
554            &self.user_prefix.join(path),
555        )
556    }
557
558    /// Like [`create_config_directory()`](#method.create_config_directory),
559    /// but for a cache directory in `XDG_CACHE_HOME`.
560    pub fn create_cache_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
561        create_directory(
562            self.cache_home.as_deref(),
563            &self.user_prefix.join(path),
564        )
565    }
566
567    /// Like [`create_config_directory()`](#method.create_config_directory),
568    /// but for an application state directory in `XDG_STATE_HOME`.
569    pub fn create_state_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
570        create_directory(
571            self.state_home.as_deref(),
572            &self.user_prefix.join(path),
573        )
574    }
575
576    /// Like [`create_config_directory()`](#method.create_config_directory),
577    /// but for a runtime directory in `XDG_RUNTIME_DIR`.
578    /// If `XDG_RUNTIME_DIR` is not available, returns an error.
579    pub fn create_runtime_directory<P: AsRef<Path>>(&self, path: P) -> io::Result<PathBuf> {
580        create_directory(
581            Some(self.get_runtime_directory()?),
582            &self.user_prefix.join(path),
583        )
584    }
585
586    /// Given a relative path `path`, list absolute paths to all files
587    /// in directories with path `path` in `XDG_CONFIG_HOME` and
588    /// `XDG_CONFIG_DIRS`.
589    pub fn list_config_files<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
590        list_files(
591            self.config_home.as_deref(),
592            &self.config_dirs,
593            &self.user_prefix,
594            &self.shared_prefix,
595            path.as_ref(),
596        )
597    }
598
599    /// Like [`list_config_files`](#method.list_config_files), but
600    /// only the first occurence of every distinct filename is returned.
601    pub fn list_config_files_once<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
602        list_files_once(
603            self.config_home.as_deref(),
604            &self.config_dirs,
605            &self.user_prefix,
606            &self.shared_prefix,
607            path.as_ref(),
608        )
609    }
610
611    /// Given a relative path `path`, lists absolute paths to all files
612    /// in directories with path `path` in `XDG_DATA_HOME` and
613    /// `XDG_DATA_DIRS`.
614    pub fn list_data_files<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
615        list_files(
616            self.data_home.as_deref(),
617            &self.data_dirs,
618            &self.user_prefix,
619            &self.shared_prefix,
620            path.as_ref(),
621        )
622    }
623
624    /// Like [`list_data_files`](#method.list_data_files), but
625    /// only the first occurence of every distinct filename is returned.
626    pub fn list_data_files_once<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
627        list_files_once(
628            self.data_home.as_deref(),
629            &self.data_dirs,
630            &self.user_prefix,
631            &self.shared_prefix,
632            path.as_ref(),
633        )
634    }
635
636    /// Given a relative path `path`, lists absolute paths to all files
637    /// in directories with path `path` in `XDG_CACHE_HOME`.
638    pub fn list_cache_files<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
639        list_files(
640            self.cache_home.as_deref(),
641            &Vec::new(),
642            &self.user_prefix,
643            &self.shared_prefix,
644            path.as_ref(),
645        )
646    }
647
648    /// Given a relative path `path`, lists absolute paths to all files
649    /// in directories with path `path` in `XDG_STATE_HOME`.
650    pub fn list_state_files<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
651        list_files(
652            self.state_home.as_deref(),
653            &Vec::new(),
654            &self.user_prefix,
655            &self.shared_prefix,
656            path.as_ref(),
657        )
658    }
659
660    /// Given a relative path `path`, lists absolute paths to all files
661    /// in directories with path `path` in `XDG_RUNTIME_DIR`.
662    /// If `XDG_RUNTIME_DIR` is not available, returns an empty `Vec`.
663    pub fn list_runtime_files<P: AsRef<Path>>(&self, path: P) -> Vec<PathBuf> {
664        if let Ok(runtime_dir) = self.get_runtime_directory() {
665            list_files(
666                Some(runtime_dir),
667                &Vec::new(),
668                &self.user_prefix,
669                &self.shared_prefix,
670                path.as_ref(),
671            )
672        } else {
673            Vec::new()
674        }
675    }
676
677    /// Returns the user-specific data directory (set by `XDG_DATA_HOME`).
678    /// Is guaranteed to not return `None` unless no HOME could be found.
679    pub fn get_data_home(&self) -> Option<PathBuf> {
680        self.data_home
681            .as_ref()
682            .map(|home| home.join(&self.user_prefix))
683    }
684
685    /// Returns the user-specific configuration directory (set by
686    /// `XDG_CONFIG_HOME` or default fallback, plus the prefix and profile if configured).
687    /// Is guaranteed to not return `None` unless no HOME could be found.
688    pub fn get_config_home(&self) -> Option<PathBuf> {
689        self.config_home
690            .as_ref()
691            .map(|home| home.join(&self.user_prefix))
692    }
693
694    /// Returns the user-specific directory for non-essential (cached) data
695    /// (set by `XDG_CACHE_HOME` or default fallback, plus the prefix and profile if configured).
696    /// Is guaranteed to not return `None` unless no HOME could be found.
697    pub fn get_cache_home(&self) -> Option<PathBuf> {
698        self.cache_home
699            .as_ref()
700            .map(|home| home.join(&self.user_prefix))
701    }
702
703    /// Returns the user-specific directory for application state data
704    /// (set by `XDG_STATE_HOME` or default fallback, plus the prefix and profile if configured).
705    /// Is guaranteed to not return `None` unless no HOME could be found.
706    pub fn get_state_home(&self) -> Option<PathBuf> {
707        self.state_home
708            .as_ref()
709            .map(|home| home.join(&self.user_prefix))
710    }
711
712    /// Returns a preference ordered (preferred to less preferred) list of
713    /// supplementary data directories, ordered by preference (set by
714    /// `XDG_DATA_DIRS` or default fallback, plus the prefix if configured).
715    pub fn get_data_dirs(&self) -> Vec<PathBuf> {
716        self.data_dirs
717            .iter()
718            .map(|p| p.join(&self.shared_prefix))
719            .collect()
720    }
721
722    /// Returns a preference ordered (preferred to less preferred) list of
723    /// supplementary configuration directories (set by `XDG_CONFIG_DIRS`
724    /// or default fallback, plus the prefix if configured).
725    pub fn get_config_dirs(&self) -> Vec<PathBuf> {
726        self.config_dirs
727            .iter()
728            .map(|p| p.join(&self.shared_prefix))
729            .collect()
730    }
731}
732
733impl Default for BaseDirectories {
734    fn default() -> Self {
735        Self::new()
736    }
737}
738
739fn write_file(home: &Path, path: &Path) -> io::Result<PathBuf> {
740    match path.parent() {
741        Some(parent) => fs::create_dir_all(home.join(parent))?,
742        None => fs::create_dir_all(home)?,
743    }
744    Ok(home.join(path))
745}
746
747fn create_directory(home: Option<&Path>, path: &Path) -> io::Result<PathBuf> {
748    let full_path = home.ok_or(Error::new(HomeMissing))?.join(path);
749    fs::create_dir_all(&full_path)?;
750    Ok(full_path)
751}
752
753fn path_exists(path: &Path) -> bool {
754    fs::metadata(path).is_ok()
755}
756
757fn read_file(
758    home: Option<&Path>,
759    dirs: &[PathBuf],
760    user_prefix: &Path,
761    shared_prefix: &Path,
762    path: &Path,
763) -> Option<PathBuf> {
764    if let Some(home) = home {
765        let full_path = home.join(user_prefix).join(path);
766        if path_exists(&full_path) {
767            return Some(full_path);
768        }
769    }
770    for dir in dirs.iter() {
771        let full_path = dir.join(shared_prefix).join(path);
772        if path_exists(&full_path) {
773            return Some(full_path);
774        }
775    }
776    None
777}
778
779use std::vec::IntoIter as VecIter;
780pub struct FileFindIterator {
781    search_dirs: VecIter<PathBuf>,
782    relpath: PathBuf,
783}
784
785impl FileFindIterator {
786    fn new(
787        home: Option<&Path>,
788        dirs: &[PathBuf],
789        user_prefix: &Path,
790        shared_prefix: &Path,
791        path: &Path,
792    ) -> FileFindIterator {
793        let mut search_dirs = Vec::new();
794        for dir in dirs.iter().rev() {
795            search_dirs.push(dir.join(shared_prefix));
796        }
797        if let Some(home) = home {
798            search_dirs.push(home.join(user_prefix));
799        }
800        FileFindIterator {
801            search_dirs: search_dirs.into_iter(),
802            relpath: path.to_path_buf(),
803        }
804    }
805}
806
807impl Iterator for FileFindIterator {
808    type Item = PathBuf;
809
810    fn next(&mut self) -> Option<Self::Item> {
811        loop {
812            let dir = self.search_dirs.next()?;
813            let candidate = dir.join(&self.relpath);
814            if path_exists(&candidate) {
815                return Some(candidate);
816            }
817        }
818    }
819}
820
821impl DoubleEndedIterator for FileFindIterator {
822    fn next_back(&mut self) -> Option<Self::Item> {
823        loop {
824            let dir = self.search_dirs.next_back()?;
825            let candidate = dir.join(&self.relpath);
826            if path_exists(&candidate) {
827                return Some(candidate);
828            }
829        }
830    }
831}
832
833fn list_files(
834    home: Option<&Path>,
835    dirs: &[PathBuf],
836    user_prefix: &Path,
837    shared_prefix: &Path,
838    path: &Path,
839) -> Vec<PathBuf> {
840    fn read_dir(dir: &Path, into: &mut Vec<PathBuf>) {
841        if let Ok(entries) = fs::read_dir(dir) {
842            into.extend(
843                entries
844                    .filter_map(|entry| entry.ok())
845                    .map(|entry| entry.path()),
846            )
847        }
848    }
849    let mut files = Vec::new();
850    if let Some(home) = home {
851        read_dir(&home.join(user_prefix).join(path), &mut files);
852    }
853    for dir in dirs {
854        read_dir(&dir.join(shared_prefix).join(path), &mut files);
855    }
856    files
857}
858
859fn list_files_once(
860    home: Option<&Path>,
861    dirs: &[PathBuf],
862    user_prefix: &Path,
863    shared_prefix: &Path,
864    path: &Path,
865) -> Vec<PathBuf> {
866    let mut seen = HashSet::new();
867    list_files(home, dirs, user_prefix, shared_prefix, path)
868        .into_iter()
869        .filter(|path| match path.file_name() {
870            None => false,
871            Some(filename) => {
872                if seen.contains(filename) {
873                    false
874                } else {
875                    seen.insert(filename.to_owned());
876                    true
877                }
878            }
879        })
880        .collect::<Vec<_>>()
881}
882
883#[cfg(test)]
884mod test {
885    use super::*;
886
887    const TARGET_TMPDIR: Option<&'static str> = option_env!("CARGO_TARGET_TMPDIR");
888    const TARGET_DIR: Option<&'static str> = option_env!("CARGO_TARGET_DIR");
889
890    fn get_test_dir() -> PathBuf {
891        match TARGET_TMPDIR {
892            Some(dir) => PathBuf::from(dir),
893            None => match TARGET_DIR {
894                Some(dir) => PathBuf::from(dir),
895                None => env::current_dir().unwrap(),
896            },
897        }
898    }
899
900    fn path_exists<P: AsRef<Path> + ?Sized>(path: &P) -> bool {
901        super::path_exists(path.as_ref())
902    }
903
904    fn path_is_dir<P: ?Sized + AsRef<Path>>(path: &P) -> bool {
905        fn inner(path: &Path) -> bool {
906            fs::metadata(path).map(|m| m.is_dir()).unwrap_or(false)
907        }
908        inner(path.as_ref())
909    }
910
911    fn make_absolute<P: AsRef<Path>>(path: P) -> PathBuf {
912        get_test_dir().join(path.as_ref())
913    }
914
915    fn iter_after<A, I, J>(mut iter: I, mut prefix: J) -> Option<I>
916    where
917        I: Iterator<Item = A> + Clone,
918        J: Iterator<Item = A>,
919        A: PartialEq,
920    {
921        loop {
922            let mut iter_next = iter.clone();
923            match (iter_next.next(), prefix.next()) {
924                (Some(x), Some(y)) => {
925                    if x != y {
926                        return None;
927                    }
928                }
929                (Some(_), None) => return Some(iter),
930                (None, None) => return Some(iter),
931                (None, Some(_)) => return None,
932            }
933            iter = iter_next;
934        }
935    }
936
937    fn make_relative<P: AsRef<Path>>(path: P, reference: P) -> PathBuf {
938        iter_after(path.as_ref().components(), reference.as_ref().components())
939            .unwrap()
940            .as_path()
941            .to_owned()
942    }
943
944    fn make_env(vars: Vec<(&'static str, String)>) -> Box<dyn Fn(&str) -> Option<OsString>> {
945        return Box::new(move |name| {
946            for &(key, ref value) in vars.iter() {
947                if key == name {
948                    return Some(OsString::from(value));
949                }
950            }
951            None
952        });
953    }
954
955    #[test]
956    fn test_files_exists() {
957        assert!(path_exists("test_files"));
958        assert!(
959            fs::metadata("test_files/runtime-bad")
960                .unwrap()
961                .permissions()
962                .mode()
963                & 0o077
964                != 0
965        );
966    }
967
968    #[test]
969    fn test_bad_environment() {
970        let xd = BaseDirectories::with_env(
971            "",
972            "",
973            &*make_env(vec![
974                ("HOME", "test_files/user".to_string()),
975                ("XDG_DATA_HOME", "test_files/user/data".to_string()),
976                ("XDG_CONFIG_HOME", "test_files/user/config".to_string()),
977                ("XDG_CACHE_HOME", "test_files/user/cache".to_string()),
978                ("XDG_DATA_DIRS", "test_files/user/data".to_string()),
979                ("XDG_CONFIG_DIRS", "test_files/user/config".to_string()),
980                ("XDG_RUNTIME_DIR", "test_files/runtime-bad".to_string()),
981            ]),
982        );
983        assert_eq!(xd.find_data_file("everywhere"), None);
984        assert_eq!(xd.find_config_file("everywhere"), None);
985        assert_eq!(xd.find_cache_file("everywhere"), None);
986    }
987
988    #[test]
989    fn test_good_environment() {
990        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
991        let xd = BaseDirectories::with_env("", "", &*make_env(vec![
992                ("HOME", format!("{}/test_files/user", cwd)),
993                ("XDG_DATA_HOME", format!("{}/test_files/user/data", cwd)),
994                ("XDG_CONFIG_HOME", format!("{}/test_files/user/config", cwd)),
995                ("XDG_CACHE_HOME", format!("{}/test_files/user/cache", cwd)),
996                ("XDG_DATA_DIRS", format!("{}/test_files/system0/data:{}/test_files/system1/data:{}/test_files/system2/data:{}/test_files/system3/data", cwd, cwd, cwd, cwd)),
997                ("XDG_CONFIG_DIRS", format!("{}/test_files/system0/config:{}/test_files/system1/config:{}/test_files/system2/config:{}/test_files/system3/config", cwd, cwd, cwd, cwd)),
998                // ("XDG_RUNTIME_DIR", format!("{}/test_files/runtime-bad", cwd)),
999            ]));
1000        assert!(xd.find_data_file("everywhere") != None);
1001        assert!(xd.find_config_file("everywhere") != None);
1002        assert!(xd.find_cache_file("everywhere") != None);
1003
1004        let mut config_files = xd.find_config_files("everywhere");
1005        assert_eq!(
1006            config_files.next(),
1007            Some(PathBuf::from(format!(
1008                "{}/test_files/system2/config/everywhere",
1009                cwd
1010            )))
1011        );
1012        assert_eq!(
1013            config_files.next(),
1014            Some(PathBuf::from(format!(
1015                "{}/test_files/system1/config/everywhere",
1016                cwd
1017            )))
1018        );
1019        assert_eq!(
1020            config_files.next(),
1021            Some(PathBuf::from(format!(
1022                "{}/test_files/user/config/everywhere",
1023                cwd
1024            )))
1025        );
1026        assert_eq!(config_files.next(), None);
1027
1028        let mut data_files = xd.find_data_files("everywhere");
1029        assert_eq!(
1030            data_files.next(),
1031            Some(PathBuf::from(format!(
1032                "{}/test_files/system2/data/everywhere",
1033                cwd
1034            )))
1035        );
1036        assert_eq!(
1037            data_files.next(),
1038            Some(PathBuf::from(format!(
1039                "{}/test_files/system1/data/everywhere",
1040                cwd
1041            )))
1042        );
1043        assert_eq!(
1044            data_files.next(),
1045            Some(PathBuf::from(format!(
1046                "{}/test_files/user/data/everywhere",
1047                cwd
1048            )))
1049        );
1050        assert_eq!(data_files.next(), None);
1051    }
1052
1053    #[test]
1054    fn test_runtime_bad() {
1055        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1056        let xd = BaseDirectories::with_env(
1057            "",
1058            "",
1059            &*make_env(vec![
1060                ("HOME", format!("{}/test_files/user", cwd)),
1061                ("XDG_RUNTIME_DIR", format!("{}/test_files/runtime-bad", cwd)),
1062            ]),
1063        );
1064        assert!(xd.has_runtime_directory() == false);
1065    }
1066
1067    #[test]
1068    fn test_runtime_good() {
1069        use std::fs::File;
1070
1071        let test_runtime_dir = make_absolute(&"test_files/runtime-good");
1072        fs::create_dir_all(&test_runtime_dir).unwrap();
1073
1074        let mut perms = fs::metadata(&test_runtime_dir).unwrap().permissions();
1075        perms.set_mode(0o700);
1076        fs::set_permissions(&test_runtime_dir, perms).unwrap();
1077
1078        let test_dir = get_test_dir().to_string_lossy().into_owned();
1079        let xd = BaseDirectories::with_env(
1080            "",
1081            "",
1082            &*make_env(vec![
1083                ("HOME", format!("{}/test_files/user", test_dir)),
1084                (
1085                    "XDG_RUNTIME_DIR",
1086                    format!("{}/test_files/runtime-good", test_dir),
1087                ),
1088            ]),
1089        );
1090
1091        xd.create_runtime_directory("foo").unwrap();
1092        assert!(path_is_dir(&format!(
1093            "{}/test_files/runtime-good/foo",
1094            test_dir
1095        )));
1096        let w = xd.place_runtime_file("bar/baz").unwrap();
1097        assert!(path_is_dir(&format!(
1098            "{}/test_files/runtime-good/bar",
1099            test_dir
1100        )));
1101        assert!(!path_exists(&format!(
1102            "{}/test_files/runtime-good/bar/baz",
1103            test_dir
1104        )));
1105        File::create(&w).unwrap();
1106        assert!(path_exists(&format!(
1107            "{}/test_files/runtime-good/bar/baz",
1108            test_dir
1109        )));
1110        assert!(xd.find_runtime_file("bar/baz") == Some(w.clone()));
1111        File::open(&w).unwrap();
1112        fs::remove_file(&w).unwrap();
1113        let root = xd.list_runtime_files(".");
1114        let mut root = root
1115            .into_iter()
1116            .map(|p| make_relative(&p, &get_test_dir()))
1117            .collect::<Vec<_>>();
1118        root.sort();
1119        assert_eq!(
1120            root,
1121            vec![
1122                PathBuf::from("test_files/runtime-good/bar"),
1123                PathBuf::from("test_files/runtime-good/foo")
1124            ]
1125        );
1126        assert!(xd.list_runtime_files("bar").is_empty());
1127        assert!(xd.find_runtime_file("foo/qux").is_none());
1128        assert!(xd.find_runtime_file("qux/foo").is_none());
1129        assert!(!path_exists(&format!(
1130            "{}/test_files/runtime-good/qux",
1131            test_dir
1132        )));
1133    }
1134
1135    #[test]
1136    fn test_lists() {
1137        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1138        let xd = BaseDirectories::with_env("", "", &*make_env(vec![
1139                ("HOME", format!("{}/test_files/user", cwd)),
1140                ("XDG_DATA_HOME", format!("{}/test_files/user/data", cwd)),
1141                ("XDG_CONFIG_HOME", format!("{}/test_files/user/config", cwd)),
1142                ("XDG_CACHE_HOME", format!("{}/test_files/user/cache", cwd)),
1143                ("XDG_DATA_DIRS", format!("{}/test_files/system0/data:{}/test_files/system1/data:{}/test_files/system2/data:{}/test_files/system3/data", cwd, cwd, cwd, cwd)),
1144                ("XDG_CONFIG_DIRS", format!("{}/test_files/system0/config:{}/test_files/system1/config:{}/test_files/system2/config:{}/test_files/system3/config", cwd, cwd, cwd, cwd)),
1145            ]));
1146
1147        let files = xd.list_config_files(".");
1148        let mut files = files
1149            .into_iter()
1150            .map(|p| make_relative(&p, &env::current_dir().unwrap()))
1151            .collect::<Vec<_>>();
1152        files.sort();
1153        assert_eq!(
1154            files,
1155            [
1156                "test_files/system1/config/both_system_config.file",
1157                "test_files/system1/config/everywhere",
1158                "test_files/system1/config/myapp",
1159                "test_files/system1/config/system1_config.file",
1160                "test_files/system2/config/both_system_config.file",
1161                "test_files/system2/config/everywhere",
1162                "test_files/system2/config/system2_config.file",
1163                "test_files/user/config/everywhere",
1164                "test_files/user/config/myapp",
1165                "test_files/user/config/user_config.file",
1166            ]
1167            .iter()
1168            .map(PathBuf::from)
1169            .collect::<Vec<_>>()
1170        );
1171
1172        let files = xd.list_config_files_once(".");
1173        let mut files = files
1174            .into_iter()
1175            .map(|p| make_relative(&p, &env::current_dir().unwrap()))
1176            .collect::<Vec<_>>();
1177        files.sort();
1178        assert_eq!(
1179            files,
1180            [
1181                "test_files/system1/config/both_system_config.file",
1182                "test_files/system1/config/system1_config.file",
1183                "test_files/system2/config/system2_config.file",
1184                "test_files/user/config/everywhere",
1185                "test_files/user/config/myapp",
1186                "test_files/user/config/user_config.file",
1187            ]
1188            .iter()
1189            .map(PathBuf::from)
1190            .collect::<Vec<_>>()
1191        );
1192    }
1193
1194    #[test]
1195    fn test_get_file() {
1196        let test_dir = get_test_dir().to_string_lossy().into_owned();
1197        let xd = BaseDirectories::with_env(
1198            "",
1199            "",
1200            &*make_env(vec![
1201                ("HOME", format!("{}/test_files/user", test_dir)),
1202                (
1203                    "XDG_DATA_HOME",
1204                    format!("{}/test_files/user/data", test_dir),
1205                ),
1206                (
1207                    "XDG_CONFIG_HOME",
1208                    format!("{}/test_files/user/config", test_dir),
1209                ),
1210                (
1211                    "XDG_CACHE_HOME",
1212                    format!("{}/test_files/user/cache", test_dir),
1213                ),
1214                (
1215                    "XDG_RUNTIME_DIR",
1216                    format!("{}/test_files/user/runtime", test_dir),
1217                ),
1218            ]),
1219        );
1220
1221        let path = format!("{}/test_files/user/runtime/", test_dir);
1222        fs::create_dir_all(&path).unwrap();
1223        let metadata = fs::metadata(&path).expect("Could not read metadata for runtime directory");
1224        let mut perms = metadata.permissions();
1225        perms.set_mode(0o700);
1226        fs::set_permissions(&path, perms).expect("Could not set permissions for runtime directory");
1227
1228        let file = xd.get_config_file("myapp/user_config.file").unwrap();
1229        assert_eq!(
1230            file,
1231            PathBuf::from(&format!(
1232                "{}/test_files/user/config/myapp/user_config.file",
1233                test_dir
1234            ))
1235        );
1236
1237        let file = xd.get_data_file("user_data.file").unwrap();
1238        assert_eq!(
1239            file,
1240            PathBuf::from(&format!("{}/test_files/user/data/user_data.file", test_dir))
1241        );
1242
1243        let file = xd.get_cache_file("user_cache.file").unwrap();
1244        assert_eq!(
1245            file,
1246            PathBuf::from(&format!(
1247                "{}/test_files/user/cache/user_cache.file",
1248                test_dir
1249            ))
1250        );
1251
1252        let file = xd.get_runtime_file("user_runtime.file").unwrap();
1253        assert_eq!(
1254            file,
1255            PathBuf::from(&format!(
1256                "{}/test_files/user/runtime/user_runtime.file",
1257                test_dir
1258            ))
1259        );
1260    }
1261
1262    #[test]
1263    fn test_prefix() {
1264        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1265        let xd = BaseDirectories::with_env(
1266            "myapp",
1267            "",
1268            &*make_env(vec![
1269                ("HOME", format!("{}/test_files/user", cwd)),
1270                ("XDG_CACHE_HOME", format!("{}/test_files/user/cache", cwd)),
1271            ]),
1272        );
1273        assert_eq!(
1274            xd.get_cache_file("cache.db").unwrap(),
1275            PathBuf::from(&format!("{}/test_files/user/cache/myapp/cache.db", cwd))
1276        );
1277        assert_eq!(
1278            xd.place_cache_file("cache.db").unwrap(),
1279            PathBuf::from(&format!("{}/test_files/user/cache/myapp/cache.db", cwd))
1280        );
1281    }
1282
1283    #[test]
1284    fn test_profile() {
1285        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1286        let xd = BaseDirectories::with_env(
1287            "myapp",
1288            "default_profile",
1289            &*make_env(vec![
1290                ("HOME", format!("{}/test_files/user", cwd)),
1291                ("XDG_CONFIG_HOME", format!("{}/test_files/user/config", cwd)),
1292                (
1293                    "XDG_CONFIG_DIRS",
1294                    format!("{}/test_files/system1/config", cwd),
1295                ),
1296            ]),
1297        );
1298        assert_eq!(
1299            xd.find_config_file("system1_config.file").unwrap(),
1300            // Does *not* include default_profile
1301            PathBuf::from(&format!(
1302                "{}/test_files/system1/config/myapp/system1_config.file",
1303                cwd
1304            ))
1305        );
1306        assert_eq!(
1307            xd.find_config_file("user_config.file").unwrap(),
1308            // Includes default_profile
1309            PathBuf::from(&format!(
1310                "{}/test_files/user/config/myapp/default_profile/user_config.file",
1311                cwd
1312            ))
1313        );
1314    }
1315
1316    /// Ensure that entries in XDG_CONFIG_DIRS can be replaced with symlinks.
1317    #[test]
1318    fn test_symlinks() {
1319        let cwd = env::current_dir().unwrap().to_string_lossy().into_owned();
1320        let symlinks_dir = format!("{}/test_files/symlinks", cwd);
1321        let config_dir = format!("{}/config", symlinks_dir);
1322        let myapp_dir = format!("{}/myapp", config_dir);
1323
1324        if Path::new(&myapp_dir).exists() {
1325            fs::remove_file(&myapp_dir).unwrap();
1326        }
1327        std::os::unix::fs::symlink("../../user/config/myapp", &myapp_dir).unwrap();
1328
1329        assert!(path_exists(&myapp_dir));
1330        assert!(path_exists(&config_dir));
1331        assert!(path_exists(&symlinks_dir));
1332
1333        let xd = BaseDirectories::with_env(
1334            "myapp",
1335            "",
1336            &*make_env(vec![
1337                ("HOME", symlinks_dir),
1338                ("XDG_CONFIG_HOME", config_dir),
1339            ]),
1340        );
1341        assert_eq!(
1342            xd.find_config_file("user_config.file").unwrap(),
1343            PathBuf::from(&format!("{}/user_config.file", myapp_dir))
1344        );
1345
1346        fs::remove_file(&myapp_dir).unwrap();
1347    }
1348}