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