├── .clippy.toml ├── test_files ├── runtime-bad │ ├── everywhere │ └── runtime.file ├── user │ ├── cache │ │ ├── everywhere │ │ └── user_cache.file │ ├── config │ │ ├── everywhere │ │ ├── user_config.file │ │ └── myapp │ │ │ ├── user_config.file │ │ │ └── default_profile │ │ │ └── user_config.file │ ├── data │ │ ├── everywhere │ │ └── user_data.file │ └── runtime │ │ └── user_runtime.file ├── symlinks │ └── config │ │ └── .gitkeep ├── system1 │ ├── config │ │ ├── everywhere │ │ ├── system1_config.file │ │ ├── both_system_config.file │ │ └── myapp │ │ │ ├── system1_config.file │ │ │ └── default_profile │ │ │ └── system1_config.file │ └── data │ │ ├── everywhere │ │ ├── system1_data.file │ │ └── both_system_data.file └── system2 │ ├── config │ ├── everywhere │ ├── system2_config.file │ └── both_system_config.file │ └── data │ ├── everywhere │ ├── system2_data.file │ └── both_system_data.file ├── .gitignore ├── src ├── lib.rs └── base_directories.rs ├── Cargo.toml ├── .github └── workflows │ ├── docs.yml │ └── ci.yml ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.60.0" 2 | -------------------------------------------------------------------------------- /test_files/runtime-bad/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/cache/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/config/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/data/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/runtime-bad/runtime.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/symlinks/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/config/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/data/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/config/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/data/everywhere: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/cache/user_cache.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/data/user_data.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /test_files/system1/data/system1_data.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/data/system2_data.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/config/user_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/runtime/user_runtime.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/config/system1_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/data/both_system_data.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/config/system2_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/data/both_system_data.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/config/myapp/user_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/config/both_system_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/config/myapp/system1_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system2/config/both_system_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/user/config/myapp/default_profile/user_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test_files/system1/config/myapp/default_profile/system1_config.file: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(unix, target_os = "redox"))] 2 | 3 | mod base_directories; 4 | pub use crate::base_directories::{ 5 | BaseDirectories, Error as BaseDirectoriesError, FileFindIterator, 6 | }; 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xdg" 3 | version = "3.0.0" 4 | description = "A library for storing and retrieving files according to XDG Base Directory specification" 5 | homepage = "https://github.com/whitequark/rust-xdg" 6 | repository = "https://github.com/whitequark/rust-xdg" 7 | documentation = "https://docs.rs/xdg/" 8 | readme = "README.md" 9 | edition = "2021" 10 | rust-version = "1.60.0" # Also update in .github/workflows/ci.yml 11 | categories = ["filesystem"] 12 | keywords = ["linux", "configuration", "xdg"] 13 | license = "Apache-2.0 OR MIT" 14 | authors = [ 15 | "Ben Longbons ", 16 | "whitequark " 17 | ] 18 | 19 | [dependencies] 20 | serde = { version = "1.0", features = ["derive"], optional = true } 21 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: Swatinem/rust-cache@v2 15 | - uses: dtolnay/rust-toolchain@nightly 16 | 17 | - name: Build docs 18 | run: cargo doc --no-deps --all-features 19 | - name: Prepare docs 20 | run: | 21 | mkdir -p _site 22 | echo '' > _site/index.html 23 | mv target/doc/* _site 24 | 25 | - uses: actions/upload-pages-artifact@v3 26 | 27 | deploy: 28 | needs: build 29 | 30 | permissions: 31 | pages: write 32 | id-token: write 33 | 34 | environment: 35 | name: github-pages 36 | url: ${{ steps.deployment.outputs.page_url }} 37 | 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 The Rust Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Lints: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: Swatinem/rust-cache@v2 11 | - uses: dtolnay/rust-toolchain@stable 12 | with: 13 | components: clippy, rustfmt 14 | - name: Check formatting 15 | run: cargo fmt --check 16 | continue-on-error: true 17 | - name: Run clippy 18 | run: cargo clippy -- -D warnings 19 | 20 | if: always() 21 | 22 | Test: 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: Swatinem/rust-cache@v2 28 | - uses: dtolnay/rust-toolchain@stable 29 | - name: Cargo check on serde feature 30 | run: cargo check --features serde 31 | - run: cargo test 32 | 33 | MSRV: 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: Swatinem/rust-cache@v2 39 | - name: Install cargo-msrv 40 | run: cargo install cargo-msrv 41 | - name: Run cargo-msrv verify 42 | run: cargo msrv verify 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-xdg 2 | 3 | [![CI](https://github.com/whitequark/rust-xdg/actions/workflows/ci.yml/badge.svg)](https://github.com/whitequark/rust-xdg/actions/workflows/ci.yml) 4 | [![Documentation](https://github.com/whitequark/rust-xdg/actions/workflows/docs.yml/badge.svg)](https://github.com/whitequark/rust-xdg/actions/workflows/docs.yml) 5 | ![Crates.io Version](https://img.shields.io/crates/v/xdg?color=%23a55e08&link=https%3A%2F%2Fcrates.io%2Fcrates%2Fxdg) 6 | ![rustc version](https://img.shields.io/badge/msrv-1.60.0-lightgray.svg) 7 | 8 | 9 | rust-xdg is a library that makes it easy to follow the X Desktop Group 10 | specifications. 11 | 12 | Currently, only [XDG Base Directory][basedir] specification is implemented. 13 | 14 | [basedir]: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html 15 | 16 | ## Installation 17 | 18 | Add the following to `Cargo.toml`: 19 | 20 | ```toml 21 | [dependencies] 22 | xdg = "^2.6" 23 | ``` 24 | 25 | ## Examples 26 | 27 | See [documentation](https://whitequark.github.io/rust-xdg/xdg/). 28 | 29 | ## License 30 | 31 | **rust-xdg** is distributed under the terms of both the MIT license 32 | and the Apache License (Version 2.0). 33 | 34 | See [LICENSE-APACHE](LICENSE-APACHE) and [LICENSE-MIT](LICENSE-MIT) 35 | for details. 36 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/base_directories.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | use std::ffi::OsString; 3 | use std::os::unix::fs::PermissionsExt; 4 | use std::path::{Path, PathBuf}; 5 | use std::{env, error, fmt, fs, io}; 6 | 7 | #[cfg(feature = "serde")] 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use 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))] 84 | pub 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, 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, 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, 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, 104 | /// Like [`BaseDirectories::get_data_dirs`], but without any prefixes applied. 105 | pub data_dirs: Vec, 106 | /// Like [`BaseDirectories::get_config_dirs`], but without any prefixes applied. 107 | pub config_dirs: Vec, 108 | /// Like [`BaseDirectories::get_runtime_directory`], but without any of the sanity checks 109 | /// on the directory (like permissions). 110 | pub runtime_dir: Option, 111 | } 112 | 113 | pub struct Error { 114 | kind: ErrorKind, 115 | } 116 | 117 | impl Error { 118 | const fn new(kind: ErrorKind) -> Error { 119 | Error { kind } 120 | } 121 | } 122 | 123 | impl fmt::Debug for Error { 124 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 125 | self.kind.fmt(f) 126 | } 127 | } 128 | 129 | impl 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 | 148 | impl 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 | 177 | impl From 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)] 187 | struct Permissions(u32); 188 | 189 | impl 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 | 196 | impl 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)] 203 | enum ErrorKind { 204 | HomeMissing, 205 | XdgRuntimeDirInaccessible(PathBuf, io::Error), 206 | XdgRuntimeDirInsecure(PathBuf, Permissions), 207 | XdgRuntimeDirMissing, 208 | } 209 | 210 | impl 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>(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(prefix: P1, profile: P2) -> BaseDirectories 259 | where 260 | P1: AsRef, 261 | P2: AsRef, 262 | { 263 | BaseDirectories::with_env(prefix, profile, &|name| env::var_os(name)) 264 | } 265 | 266 | fn with_env(prefix: P1, profile: P2, env_var: &T) -> BaseDirectories 267 | where 268 | P1: AsRef, 269 | P2: AsRef, 270 | T: ?Sized + Fn(&str) -> Option, 271 | { 272 | BaseDirectories::with_env_impl(prefix.as_ref(), profile.as_ref(), env_var) 273 | } 274 | 275 | fn with_env_impl(prefix: &Path, profile: &Path, env_var: &T) -> BaseDirectories 276 | where 277 | T: ?Sized + Fn(&str) -> Option, 278 | { 279 | fn abspath(path: OsString) -> Option { 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> { 289 | let paths: Vec = env::split_paths(&paths) 290 | .map(PathBuf::from) 291 | .filter(|path| path.is_absolute()) 292 | .collect::>(); 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 = 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>(&self, path: P) -> Option { 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>(&self, path: P) -> Option { 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>(&self, path: P) -> Option { 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>(&self, path: P) -> Option { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> Option { 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>(&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>(&self, path: P) -> Option { 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>(&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>(&self, path: P) -> Option { 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>(&self, path: P) -> Option { 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>(&self, path: P) -> Option { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> io::Result { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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>(&self, path: P) -> Vec { 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 { 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 { 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 { 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 { 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 { 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 { 726 | self.config_dirs 727 | .iter() 728 | .map(|p| p.join(&self.shared_prefix)) 729 | .collect() 730 | } 731 | } 732 | 733 | impl Default for BaseDirectories { 734 | fn default() -> Self { 735 | Self::new() 736 | } 737 | } 738 | 739 | fn write_file(home: &Path, path: &Path) -> io::Result { 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 | 747 | fn create_directory(home: Option<&Path>, path: &Path) -> io::Result { 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 | 753 | fn path_exists(path: &Path) -> bool { 754 | fs::metadata(path).is_ok() 755 | } 756 | 757 | fn read_file( 758 | home: Option<&Path>, 759 | dirs: &[PathBuf], 760 | user_prefix: &Path, 761 | shared_prefix: &Path, 762 | path: &Path, 763 | ) -> Option { 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 | 779 | use std::vec::IntoIter as VecIter; 780 | pub struct FileFindIterator { 781 | search_dirs: VecIter, 782 | relpath: PathBuf, 783 | } 784 | 785 | impl 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 | 807 | impl Iterator for FileFindIterator { 808 | type Item = PathBuf; 809 | 810 | fn next(&mut self) -> Option { 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 | 821 | impl DoubleEndedIterator for FileFindIterator { 822 | fn next_back(&mut self) -> Option { 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 | 833 | fn list_files( 834 | home: Option<&Path>, 835 | dirs: &[PathBuf], 836 | user_prefix: &Path, 837 | shared_prefix: &Path, 838 | path: &Path, 839 | ) -> Vec { 840 | fn read_dir(dir: &Path, into: &mut Vec) { 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 | 859 | fn list_files_once( 860 | home: Option<&Path>, 861 | dirs: &[PathBuf], 862 | user_prefix: &Path, 863 | shared_prefix: &Path, 864 | path: &Path, 865 | ) -> Vec { 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::>() 881 | } 882 | 883 | #[cfg(test)] 884 | mod 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 + ?Sized>(path: &P) -> bool { 901 | super::path_exists(path.as_ref()) 902 | } 903 | 904 | fn path_is_dir>(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>(path: P) -> PathBuf { 912 | get_test_dir().join(path.as_ref()) 913 | } 914 | 915 | fn iter_after(mut iter: I, mut prefix: J) -> Option 916 | where 917 | I: Iterator + Clone, 918 | J: Iterator, 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>(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 Option> { 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::>(); 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::>(); 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::>() 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::>(); 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::>() 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 | } 1349 | --------------------------------------------------------------------------------