├── .github └── workflows │ └── rust.yaml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE.txt ├── README.md ├── appveyor.yml ├── appveyor_rust_install.ps1 ├── examples └── android.rs ├── rustfmt.toml ├── src ├── common.rs ├── imp │ ├── mod.rs │ └── platform │ │ ├── android.rs │ │ ├── macos.rs │ │ ├── unix.rs │ │ ├── unknown.rs │ │ └── windows.rs ├── lib.rs └── utils.rs └── tests ├── fs.rs └── unix.rs /.github/workflows/rust.yaml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | clippy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | toolchain: stable 13 | override: true 14 | profile: minimal 15 | components: clippy 16 | - name: Clippy 17 | uses: actions-rs/cargo@v1 18 | with: 19 | command: clippy 20 | args: --all --all-targets -- -Dwarnings 21 | 22 | test: 23 | strategy: 24 | matrix: 25 | os: 26 | - ubuntu-latest 27 | - windows-latest 28 | - macos-latest 29 | runs-on: ${{ matrix.os }} 30 | steps: 31 | - uses: actions/checkout@v3 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: stable 35 | override: true 36 | profile: minimal 37 | - name: Test all-targets 38 | uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: --workspace --all-targets 42 | - name: Test docs 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: test 46 | args: --workspace --doc 47 | 48 | docs: 49 | runs-on: ubuntu-latest 50 | name: Build-test docs 51 | steps: 52 | - uses: actions/checkout@v3 53 | - uses: actions-rs/toolchain@v1 54 | with: 55 | toolchain: stable 56 | override: true 57 | profile: minimal 58 | - name: Document all crates 59 | uses: actions-rs/cargo@v1 60 | env: 61 | RUSTDOCFLAGS: -Dwarnings 62 | with: 63 | command: doc 64 | args: --all --all-features --no-deps --document-private-items 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - beta 5 | - nightly 6 | matrix: 7 | allow_failures: 8 | - rust: nightly 9 | os: 10 | - linux 11 | - osx -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "app_dirs2" 3 | version = "2.5.5" 4 | authors = ["Andy Barron ", "Simon Heath "] 5 | categories = ["filesystem", "os"] 6 | description = "Put your app's data in the right place on every platform. Community-maintained project." 7 | documentation = "https://docs.rs/app_dirs2" 8 | homepage = "https://lib.rs/app_dirs2" 9 | keywords = ["application", "data", "storage", "location", "directory"] 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/app-dirs-rs/app_dirs2" 13 | edition = "2018" 14 | exclude = ["/appveyor.yml", "/appveyor_rust_install.ps1", "/rustfmt.toml", "/.github/", "/.travis.yml"] 15 | 16 | [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] 17 | xdg = "2.4.1" 18 | 19 | [target.'cfg(target_os = "android")'.dependencies] 20 | jni = "0.21.0" 21 | ndk-context = "0.1.1" 22 | 23 | [target.'cfg(target_os = "windows")'.dependencies] 24 | windows = { version = "0.58.0", features = [ "Win32_System_Com", "Win32_UI_Shell" ] } 25 | 26 | [dev-dependencies] 27 | once_cell = "1.14.0" 28 | tempfile = "3.3.0" 29 | test-case = "3" 30 | 31 | [target.'cfg(target_os = "android")'.dev-dependencies] 32 | ndk-glue = { version = "0.7.0", features = ["logger"] } 33 | log = "0.4.17" 34 | 35 | [badges] 36 | maintenance = { status = "passively-maintained" } 37 | 38 | [[example]] 39 | name = "android" 40 | crate-type = ["cdylib"] 41 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 app-dirs-rs Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # app_dirs2 2 | 3 | *Put your app's data in the right place on every platform* 4 | 5 | [![crates.io:app_dirs2](https://img.shields.io/crates/v/app_dirs2.svg?label=crates.io%3A%20app_dirs2)](https://lib.rs/crates/app_dirs2) 6 | 7 | ## This is the up-to-date version of `app_dirs` 8 | 9 | The original [app_dirs](https://lib.rs/crates/app_dirs) crate is deprecated and unmaintained. This is a drop-in-replacement fork that keeps the crate working and up-to-date. 10 | 11 | This is a *community-maintained project*, so if you find a bug or the crate is missing support for your platform, please help out. 12 | 13 | There are no major changes planned. If you're looking for a crate with more features, check out the [directories](https://lib.rs/crates/directories) crate. 14 | 15 | ## Documentation & examples 16 | 17 | https://docs.rs/app_dirs2 18 | 19 | ## Installation 20 | 21 | Add the following to your `Cargo.toml` under `[dependencies]`: 22 | 23 | ```toml 24 | app_dirs = { package = "app_dirs2", version = "2.5" } 25 | ``` 26 | 27 | The syntax with `package` allows you to keep the old name in the code (`use app_dirs::*`), so you only need to change one line in `Cargo.toml`. 28 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Appveyor configuration template for Rust 2 | # https://github.com/starkat99/appveyor-rust 3 | 4 | ## Operating System (VM environment) ## 5 | 6 | # Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. 7 | os: Visual Studio 2015 8 | 9 | ## Build Matrix ## 10 | 11 | # This configuration will setup a build for each channel & target combination (12 windows 12 | # combinations in all). 13 | # 14 | # There are 3 channels: stable, beta, and nightly. 15 | # 16 | # Alternatively, the full version may be specified for the channel to build using that specific 17 | # version (e.g. channel: 1.5.0) 18 | # 19 | # The values for target are the set of windows Rust build targets. Each value is of the form 20 | # 21 | # ARCH-pc-windows-TOOLCHAIN 22 | # 23 | # Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker 24 | # toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for 25 | # a description of the toolchain differences. 26 | # 27 | # Comment out channel/target combos you do not wish to build in CI. 28 | environment: 29 | matrix: 30 | 31 | ### MSVC Toolchains ### 32 | 33 | # Stable 64-bit MSVC 34 | - channel: stable 35 | target: x86_64-pc-windows-msvc 36 | # Stable 32-bit MSVC 37 | - channel: stable 38 | target: i686-pc-windows-msvc 39 | # Beta 64-bit MSVC 40 | - channel: beta 41 | target: x86_64-pc-windows-msvc 42 | # Beta 32-bit MSVC 43 | - channel: beta 44 | target: i686-pc-windows-msvc 45 | # Nightly 64-bit MSVC 46 | - channel: nightly 47 | target: x86_64-pc-windows-msvc 48 | # Nightly 32-bit MSVC 49 | - channel: nightly 50 | target: i686-pc-windows-msvc 51 | 52 | ### GNU Toolchains ### 53 | 54 | # Stable 64-bit GNU 55 | - channel: stable 56 | target: x86_64-pc-windows-gnu 57 | # Stable 32-bit GNU 58 | - channel: stable 59 | target: i686-pc-windows-gnu 60 | # Beta 64-bit GNU 61 | - channel: beta 62 | target: x86_64-pc-windows-gnu 63 | # Beta 32-bit GNU 64 | - channel: beta 65 | target: i686-pc-windows-gnu 66 | # Nightly 64-bit GNU 67 | - channel: nightly 68 | target: x86_64-pc-windows-gnu 69 | # Nightly 32-bit GNU 70 | - channel: nightly 71 | target: i686-pc-windows-gnu 72 | 73 | ### Allowed failures ### 74 | 75 | # See Appveyor documentation for specific details. In short, place any channel or targets you wish 76 | # to allow build failures on (usually nightly at least is a wise choice). This will prevent a build 77 | # or test failure in the matching channels/targets from failing the entire build. 78 | matrix: 79 | allow_failures: 80 | - channel: nightly 81 | 82 | # If you only care about stable channel build failures, uncomment the following line: 83 | #- channel: beta 84 | 85 | # 32-bit MSVC isn't stablized yet, so you may optionally allow failures there (uncomment line): 86 | #- target: i686-pc-windows-msvc 87 | 88 | ## Install Script ## 89 | 90 | # This is the most important part of the Appveyor configuration. This installs the version of Rust 91 | # specified by the 'channel' and 'target' environment variables from the build matrix. By default, 92 | # Rust will be installed to C:\Rust for easy usage, but this path can be overridden by setting the 93 | # RUST_INSTALL_DIR environment variable. The URL to download rust distributions defaults to 94 | # https://static.rust-lang.org/dist/ but can overridden by setting the RUST_DOWNLOAD_URL environment 95 | # variable. 96 | # 97 | # For simple configurations, instead of using the build matrix, you can override the channel and 98 | # target environment variables with the -channel and -target script arguments. 99 | # 100 | # If no channel or target arguments or environment variables are specified, will default to stable 101 | # channel and x86_64-pc-windows-msvc target. 102 | # 103 | # The file appveyor_rust_install.ps1 must exist in the root directory of the repository. 104 | install: 105 | - ps: .\appveyor_rust_install.ps1 106 | 107 | # Alternative install command for simple configurations without build matrix (uncomment line and 108 | # comment above line): 109 | #- ps: .\appveyor_rust_install.ps1 -channel stable -target x86_64-pc-windows-msvc 110 | 111 | ## Build Script ## 112 | 113 | build: false # fixes appveyor "project or solution file" error 114 | 115 | # Uses 'cargo test' to run tests. Alternatively, the project may call compiled programs directly or 116 | # perform other testing commands. Rust will automatically be placed in the PATH environment 117 | # variable. 118 | test_script: 119 | - cmd: cargo test --verbose 120 | -------------------------------------------------------------------------------- /appveyor_rust_install.ps1: -------------------------------------------------------------------------------- 1 | ##### Appveyor Rust Install Script ##### 2 | 3 | # https://github.com/starkat99/appveyor-rust 4 | 5 | # This is the most important part of the Appveyor configuration. This installs the version of Rust 6 | # specified by the "channel" and "target" environment variables from the build matrix. By default, 7 | # Rust will be installed to C:\Rust for easy usage, but this path can be overridden by setting the 8 | # RUST_INSTALL_DIR environment variable. The URL to download rust distributions defaults to 9 | # https://static.rust-lang.org/dist/ but can overridden by setting the RUST_DOWNLOAD_URL environment 10 | # variable. 11 | # 12 | # For simple configurations, instead of using the build matrix, you can override the channel and 13 | # target environment variables with the --channel and --target script arguments. 14 | # 15 | # If no channel or target arguments or environment variables are specified, will default to stable 16 | # channel and x86_64-pc-windows-msvc target. 17 | 18 | param([string]$channel=${env:channel}, [string]$target=${env:target}) 19 | 20 | # Initialize our parameters from arguments and environment variables, falling back to defaults 21 | if (!$channel) { 22 | $channel = "stable" 23 | } 24 | if (!$target) { 25 | $target = "x86_64-pc-windows-msvc" 26 | } 27 | 28 | $downloadUrl = "https://static.rust-lang.org/dist/" 29 | if ($env:RUST_DOWNLOAD_URL) { 30 | $downloadUrl = $env:RUST_DOWNLOAD_URL 31 | } 32 | 33 | $installDir = "C:\Rust" 34 | if ($env:RUST_INSTALL_DIR) { 35 | $installUrl = $env:RUST_INSTALL_DIR 36 | } 37 | 38 | if ($channel -eq "stable") { 39 | # Download manifest so we can find actual filename of installer to download. Needed for stable. 40 | echo "Downloading $channel channel manifest" 41 | $manifest = "${env:Temp}\channel-rust-${channel}" 42 | Start-FileDownload "${downloadUrl}channel-rust-${channel}" -FileName "$manifest" 43 | 44 | # Search the manifest lines for the correct filename based on target 45 | $match = Get-Content "$manifest" | Select-String -pattern "${target}.exe" -simplematch 46 | 47 | if (!$match -or !$match.line) { 48 | throw "Could not find $target in $channel channel manifest" 49 | } 50 | 51 | $installer = $match.line 52 | } else { 53 | # Otherwise download the file specified by channel directly. 54 | $installer = "rust-${channel}-${target}.exe" 55 | } 56 | 57 | # Download installer 58 | echo "Downloading ${downloadUrl}$installer" 59 | Start-FileDownload "${downloadUrl}$installer" -FileName "${env:Temp}\$installer" 60 | 61 | # Execute installer and wait for it to finish 62 | echo "Installing $installer to $installDir" 63 | &"${env:Temp}\$installer" /VERYSILENT /NORESTART /DIR="$installDir" | Write-Output 64 | 65 | # Add Rust to the path. 66 | $env:Path += ";${installDir}\bin;C:\MinGW\bin" 67 | 68 | echo "Installation of $channel Rust $target completed" 69 | 70 | # Test and display installed version information for rustc and cargo 71 | rustc -V 72 | cargo -V -------------------------------------------------------------------------------- /examples/android.rs: -------------------------------------------------------------------------------- 1 | #![cfg(target_os = "android")] 2 | 3 | use app_dirs2::*; 4 | use log::info; 5 | 6 | const NDK_APP_INFO: AppInfo = AppInfo { 7 | name: "unnecessary", 8 | author: "The Android NDK Authors", 9 | }; 10 | 11 | #[cfg_attr( 12 | target_os = "android", 13 | ndk_glue::main(backtrace = "full", logger(level = "info", tag = "app_dirs2")) 14 | )] 15 | fn main() { 16 | let all = [ 17 | AppDataType::UserConfig, 18 | AppDataType::UserData, 19 | AppDataType::UserCache, 20 | AppDataType::SharedConfig, 21 | AppDataType::SharedData, 22 | ]; 23 | for t in all { 24 | info!("{:?}: {:?}", t, app_root(t, &NDK_APP_INFO)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | match_block_trailing_comma = true 2 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | /// Struct that holds information about your app. 2 | /// 3 | /// It's recommended to create a single `const` instance of `AppInfo`: 4 | /// 5 | /// ``` 6 | /// use app_dirs2::AppInfo; 7 | /// const APP_INFO: AppInfo = AppInfo{name: "Awesome App", author: "Dedicated Dev"}; 8 | /// ``` 9 | /// 10 | /// # Caveats 11 | /// Functions in this library sanitize any characters that could be 12 | /// non-filename-safe from `name` and `author`. The resulting paths will be 13 | /// more human-readable if you stick to **letters, numbers, spaces, hyphens, 14 | /// underscores, and periods** for both properties. 15 | /// 16 | /// The `author` property is currently only used by Windows, as macOS and *nix 17 | /// specifications don't require it. Make sure your `name` string is unique! 18 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 19 | pub struct AppInfo { 20 | /// Name of your app (e.g. "Hearthstone"). 21 | pub name: &'static str, 22 | /// Author of your app (e.g. "Blizzard"). 23 | pub author: &'static str, 24 | } 25 | 26 | /// Enum specifying the type of app data you want to store. 27 | /// 28 | /// **Different platforms are NOT guaranteed to distinguish between each data 29 | /// type.** Keep this in mind when choosing data file paths. 30 | /// 31 | /// Example: Windows does not supported shared application data and does not 32 | /// distinguish between config and data. Therefore, on Windows, all variants 33 | /// except `UserCache` return the same path. 34 | #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] 35 | pub enum AppDataType { 36 | /// User-specific app configuration data. 37 | UserConfig, 38 | /// User-specific arbitrary app data. 39 | UserData, 40 | /// User-specific app cache data. 41 | UserCache, 42 | /// System-wide arbitrary app data. 43 | SharedData, 44 | /// System-wide app configuration data. 45 | SharedConfig, 46 | } 47 | 48 | impl AppDataType { 49 | /// Returns `true` for non-user-specific data types. 50 | #[must_use] 51 | pub fn is_shared(&self) -> bool { 52 | use crate::AppDataType::{SharedConfig, SharedData}; 53 | matches!(self, SharedData | SharedConfig) 54 | } 55 | } 56 | 57 | const ERR_NOT_SUPPORTED: &str = "App data directories not supported"; 58 | const ERR_INVALID_APP_INFO: &str = "Invalid app name or author"; 59 | 60 | /// Error type for any `app_dirs` operation. 61 | #[derive(Debug)] 62 | pub enum AppDirsError { 63 | /// An I/O error occurred during the operation. 64 | Io(std::io::Error), 65 | /// App-specific directories are not properly supported by the system 66 | /// (e.g. required environment variables don't exist). 67 | NotSupported, 68 | /// App info given to this library was invalid (e.g. app name or author 69 | /// were empty). 70 | InvalidAppInfo, 71 | } 72 | 73 | impl std::fmt::Display for AppDirsError { 74 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 75 | use crate::AppDirsError::*; 76 | match *self { 77 | Io(ref e) => std::fmt::Display::fmt(e, f), 78 | NotSupported => f.write_str(ERR_NOT_SUPPORTED), 79 | InvalidAppInfo => f.write_str(ERR_INVALID_APP_INFO), 80 | } 81 | } 82 | } 83 | 84 | impl std::error::Error for AppDirsError { 85 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 86 | use crate::AppDirsError::*; 87 | match *self { 88 | Io(ref e) => Some(e), 89 | NotSupported => None, 90 | InvalidAppInfo => None, 91 | } 92 | } 93 | } 94 | 95 | impl From for AppDirsError { 96 | fn from(e: std::io::Error) -> Self { 97 | AppDirsError::Io(e) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/imp/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{AppDataType, AppDirsError, AppInfo}; 2 | use crate::utils; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | 6 | #[cfg(any(target_os = "macos", target_os = "ios"))] 7 | mod platform { 8 | mod macos; 9 | pub use self::macos::*; 10 | } 11 | #[cfg(all( 12 | unix, 13 | not(target_os = "macos"), 14 | not(target_os = "ios"), 15 | not(target_os = "android") 16 | ))] 17 | mod platform { 18 | mod unix; 19 | pub use self::unix::*; 20 | } 21 | #[cfg(windows)] 22 | mod platform { 23 | mod windows; 24 | pub use self::windows::*; 25 | } 26 | #[cfg(not(any(windows, unix, target_os = "macos", target_os = "ios")))] 27 | mod platform { 28 | mod unknown; 29 | pub use self::unknown::*; 30 | } 31 | #[cfg(target_os = "android")] 32 | mod platform { 33 | // As per #33, Android tries the Unix path to avoid crashes for programs run 34 | // inside Termux. 35 | mod android; 36 | mod unix; 37 | pub use self::android::*; 38 | } 39 | 40 | /// Creates (if necessary) and returns path to **app-specific** data 41 | /// **subdirectory** for provided data type and subdirectory path. 42 | /// 43 | /// The `path` parameter should be a valid relative path separated by 44 | /// **forward slashes** (`/`). 45 | /// 46 | /// If the directory structure does not exist, this function will recursively 47 | /// create the full hierarchy. Therefore, a result of `Ok` guarantees that the 48 | /// returned path exists. 49 | pub fn app_dir(t: AppDataType, app: &AppInfo, path: &str) -> Result { 50 | let path = get_app_dir(t, app, path)?; 51 | match fs::create_dir_all(&path) { 52 | Ok(..) => Ok(path), 53 | Err(e) => Err(e.into()), 54 | } 55 | } 56 | 57 | /// Returns (but **does not create**) path to **app-specific** data 58 | /// **subdirectory** for provided data type and subdirectory path. 59 | /// 60 | /// The `path` parameter should be a valid relative path separated by 61 | /// **forward slashes** (`/`). 62 | /// 63 | /// A result of `Ok` means that we determined where the data SHOULD go, but 64 | /// it DOES NOT guarantee that the directory actually exists. (See 65 | /// [`app_dir`](fn.app_dir.html).) 66 | pub fn get_app_dir(t: AppDataType, app: &AppInfo, path: &str) -> Result { 67 | if app.author.is_empty() || app.name.is_empty() { 68 | return Err(AppDirsError::InvalidAppInfo); 69 | } 70 | get_app_root(t, app).map(|mut root| { 71 | for component in path.split('/').filter(|s| !s.is_empty()) { 72 | root.push(utils::sanitized(component)); 73 | } 74 | root 75 | }) 76 | } 77 | 78 | /// Creates (if necessary) and returns path to **app-specific** data 79 | /// directory for provided data type. 80 | /// 81 | /// If the directory structure does not exist, this function will recursively 82 | /// create the full hierarchy. Therefore, a result of `Ok` guarantees that the 83 | /// returned path exists. 84 | pub fn app_root(t: AppDataType, app: &AppInfo) -> Result { 85 | let path = get_app_root(t, app)?; 86 | match fs::create_dir_all(&path) { 87 | Ok(..) => Ok(path), 88 | Err(e) => Err(e.into()), 89 | } 90 | } 91 | 92 | /// Returns (but **does not create**) path to **app-specific** data directory 93 | /// for provided data type. 94 | /// 95 | /// A result of `Ok` means that we determined where the data SHOULD go, but 96 | /// it DOES NOT guarantee that the directory actually exists. (See 97 | /// [`app_root`](fn.app_root.html).) 98 | pub fn get_app_root(t: AppDataType, app: &AppInfo) -> Result { 99 | if app.author.is_empty() || app.name.is_empty() { 100 | return Err(AppDirsError::InvalidAppInfo); 101 | } 102 | get_data_root(t).map(|mut root| { 103 | if platform::USE_AUTHOR { 104 | root.push(utils::sanitized(app.author)); 105 | } 106 | root.push(utils::sanitized(app.name)); 107 | root 108 | }) 109 | } 110 | 111 | /// Creates (if necessary) and returns path to **top-level** data directory 112 | /// for provided data type. 113 | /// 114 | /// If the directory structure does not exist, this function will recursively 115 | /// create the full hierarchy. Therefore, a result of `Ok` guarantees that the 116 | /// returned path exists. 117 | pub fn data_root(t: AppDataType) -> Result { 118 | let path = platform::get_app_dir(t)?; 119 | match fs::create_dir_all(&path) { 120 | Ok(..) => Ok(path), 121 | Err(e) => Err(e.into()), 122 | } 123 | } 124 | 125 | /// Returns (but **does not create**) path to **top-level** data directory for 126 | /// provided data type. 127 | /// 128 | /// A result of `Ok` means that we determined where the data SHOULD go, but 129 | /// it DOES NOT guarantee that the directory actually exists. (See 130 | /// [`data_root`](fn.data_root.html).) 131 | pub fn get_data_root(t: AppDataType) -> Result { 132 | platform::get_app_dir(t) 133 | } 134 | -------------------------------------------------------------------------------- /src/imp/platform/android.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use std::io::{Error, ErrorKind}; 3 | use std::path::PathBuf; 4 | 5 | pub const USE_AUTHOR: bool = false; 6 | 7 | impl From for AppDirsError { 8 | fn from(error: jni::errors::Error) -> Self { 9 | AppDirsError::Io(Error::new(ErrorKind::Other, error)) 10 | } 11 | } 12 | 13 | fn get_jni_app_dir( 14 | context: &jni::objects::JObject<'_>, 15 | env: &mut jni::JNIEnv<'_>, 16 | method: &str, 17 | has_string_argument: bool, 18 | ) -> Result { 19 | let dir = if has_string_argument { 20 | env.call_method( 21 | context, 22 | method, 23 | "(Ljava/lang/String;)Ljava/io/File;", 24 | &[jni::objects::JValue::Object(&jni::objects::JObject::null())], 25 | ) 26 | } else { 27 | env.call_method(context, method, "()Ljava/io/File;", &[]) 28 | }? 29 | .l()?; 30 | 31 | let path_string = env 32 | .call_method(dir, "getPath", "()Ljava/lang/String;", &[])? 33 | .l()?; 34 | let path_string = jni::objects::JString::from(path_string); 35 | let path_string = env.get_string(&path_string)?; 36 | 37 | Ok(path_string.into()) 38 | } 39 | 40 | pub fn get_app_dir(t: AppDataType) -> Result { 41 | // Issue #33: Android apps run inside Termux do not have an Android JNI 42 | // context, so the call to `ndk_context::android_context()` below will 43 | // panic. However, Termux does provide the usual `$XDG_*` environment 44 | // variables. So as a workaround, we check those variables first. "Regular" 45 | // android apps will not have them, but will have a context. 46 | 47 | let maybe_unix = super::unix::get_app_dir(t); 48 | if maybe_unix.is_ok() { 49 | return maybe_unix; 50 | } 51 | 52 | let android_context = ndk_context::android_context(); 53 | let vm = unsafe { jni::JavaVM::from_raw(android_context.vm().cast()) }?; 54 | let mut env = vm.attach_current_thread()?; 55 | let context = unsafe { jni::objects::JObject::from_raw(android_context.context().cast()) }; 56 | 57 | let path_string = match t { 58 | AppDataType::UserConfig => get_jni_app_dir(&context, &mut env, "getDataDir", false)?, 59 | AppDataType::UserData => get_jni_app_dir(&context, &mut env, "getFilesDir", false)?, 60 | AppDataType::UserCache => get_jni_app_dir(&context, &mut env, "getCacheDir", false)?, 61 | AppDataType::SharedData | AppDataType::SharedConfig => { 62 | get_jni_app_dir(&context, &mut env, "getExternalFilesDir", true)? 63 | }, 64 | // AppDataType::SharedCache => get_jni_app_dir(&context, &mut env, "getExternalCacheDir", false)?, 65 | }; 66 | 67 | Ok(PathBuf::from(path_string)) 68 | } 69 | -------------------------------------------------------------------------------- /src/imp/platform/macos.rs: -------------------------------------------------------------------------------- 1 | use crate::common::{AppDataType, AppDirsError}; 2 | use crate::AppDataType::*; 3 | use std::path::{Component, Path, PathBuf}; 4 | 5 | pub const USE_AUTHOR: bool = false; 6 | 7 | #[allow(deprecated)] // it's fine on macOS 8 | pub fn get_app_dir(t: AppDataType) -> Result { 9 | let dir_base: Result = if t.is_shared() { 10 | Ok(Path::new(&Component::RootDir).into()) 11 | } else { 12 | std::env::home_dir().ok_or(AppDirsError::NotSupported) 13 | }; 14 | dir_base.map(|mut path| { 15 | match t { 16 | UserConfig | UserData | SharedConfig | SharedData => { 17 | path.push("Library"); 18 | path.push("Application Support"); 19 | }, 20 | UserCache => { 21 | path.push("Library"); 22 | path.push("Caches"); 23 | }, 24 | }; 25 | path 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /src/imp/platform/unix.rs: -------------------------------------------------------------------------------- 1 | extern crate xdg; 2 | use self::xdg::BaseDirectories as Xdg; 3 | use crate::common::*; 4 | use crate::AppDataType::*; 5 | use std::path::PathBuf; 6 | 7 | // On Android we build this module to try XDG environment variables (#33), but 8 | // this constant is unused and triggers a compiler warning. 9 | #[cfg(not(target_os = "android"))] 10 | pub const USE_AUTHOR: bool = false; 11 | 12 | pub fn get_app_dir(t: AppDataType) -> Result { 13 | Xdg::new() 14 | .ok() 15 | .as_ref() 16 | .and_then(|x| match t { 17 | UserConfig => Some(x.get_config_home()), 18 | UserData => Some(x.get_data_home()), 19 | UserCache => Some(x.get_cache_home()), 20 | SharedData => x.get_data_dirs().into_iter().next(), 21 | SharedConfig => x.get_config_dirs().into_iter().next(), 22 | }) 23 | .ok_or(AppDirsError::NotSupported) 24 | } 25 | -------------------------------------------------------------------------------- /src/imp/platform/unknown.rs: -------------------------------------------------------------------------------- 1 | use crate::common::*; 2 | use crate::AppDataType::*; 3 | use std::path::PathBuf; 4 | 5 | pub const USE_AUTHOR: bool = false; 6 | 7 | pub fn get_app_dir(_t: AppDataType) -> Result { 8 | Err(AppDirsError::NotSupported) 9 | } 10 | -------------------------------------------------------------------------------- /src/imp/platform/windows.rs: -------------------------------------------------------------------------------- 1 | //! Windows provides three different ways to get the paths to roaming and local 2 | //! app data: environment variables, KNOWNFOLDERID, and CSIDL. From the CSIDL 3 | //! documentation: 4 | //! 5 | //! *"These values supersede the use of environment variables for this purpose. 6 | //! They are in turn superseded in Windows Vista and later by the KNOWNFOLDERID 7 | //! values."* 8 | //! - https://msdn.microsoft.com/en-us/library/windows/desktop/bb762494.aspx 9 | //! 10 | //! -_- 11 | 12 | // The function get_folder_path was adapted from: 13 | // https://github.com/AndyBarron/preferences-rs/blob/f03c7/src/lib.rs#L211-L296 14 | // 15 | // Credit for the above code goes to Connorcpu (https://github.com/Connorcpu). 16 | 17 | use windows::Win32::System::Com::CoTaskMemFree; 18 | use windows::Win32::UI::Shell::*; 19 | use windows::core::{ PWSTR, GUID }; 20 | use crate::common::*; 21 | use crate::AppDataType::*; 22 | use std::ffi::OsString; 23 | use std::os::windows::ffi::OsStringExt; 24 | use std::path::PathBuf; 25 | 26 | pub const USE_AUTHOR: bool = true; 27 | 28 | pub fn get_app_dir(t: AppDataType) -> Result { 29 | let folder_id = match t { 30 | UserConfig => &FOLDERID_RoamingAppData, 31 | SharedConfig | SharedData => &FOLDERID_ProgramData, 32 | UserCache | UserData => &FOLDERID_LocalAppData, 33 | }; 34 | get_folder_path(folder_id).map(|os_str| os_str.into()) 35 | } 36 | 37 | /// Wrapper around `windows::core::PWSTR` to automatically free the string pointer. 38 | /// This ensures the memory is freed when `get_folder_path` scope is left, 39 | /// regardless of whether the call succeeded or failed/panicked. 40 | struct SafePwstr(PWSTR); 41 | impl Drop for SafePwstr { 42 | fn drop(&mut self) { 43 | unsafe { CoTaskMemFree(Some(self.0.as_ptr() as *mut _)) } 44 | } 45 | } 46 | 47 | fn get_folder_path(folder_id: &GUID) -> Result { 48 | unsafe { 49 | // SHGetKnownFolderPath arguments: 50 | // 1. reference to KNOWNFOLDERID 51 | // 2. no flags 52 | // 3. `None` handle -> current user 53 | // 54 | // Returns a PWSTR, which contains the path to requested folder. 55 | match SHGetKnownFolderPath(folder_id, KNOWN_FOLDER_FLAG::default(), None) { 56 | Ok(raw_path) => { 57 | // Ensures that the PWSTR is free when we leave this scope through 58 | // normal execution or a thread panic. 59 | let _cleanup = SafePwstr(raw_path); 60 | Ok(OsStringExt::from_wide(raw_path.as_wide())) 61 | // _cleanup is deallocated, so raw_path is freed 62 | }, 63 | Err(_) => { 64 | Err(AppDirsError::NotSupported) 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! *Put your app's data in the right place on every platform* 3 | //! 4 | //! # Usage 5 | //! 6 | //! ```rust 7 | //! use app_dirs2::*; // or app_dirs::* if you've used package alias in Cargo.toml 8 | //! 9 | //! const APP_INFO: AppInfo = AppInfo{name: "CoolApp", author: "SuperDev"}; 10 | //! 11 | //! fn main () { 12 | //! // Where should I store my app's per-user configuration data? 13 | //! println!("{:?}", get_app_root(AppDataType::UserConfig, &APP_INFO)); 14 | //! // Windows: "%APPDATA%\SuperDev\CoolApp" 15 | //! // (e.g.: "C:\Users\Rusty\AppData\Roaming\SuperDev\CoolApp") 16 | //! // macOS: "$HOME/Library/Application Support/CoolApp" 17 | //! // (e.g.: "/Users/Rusty/Library/Application Support/CoolApp") 18 | //! // *nix: "$HOME/.config/CoolApp" (or "$XDG_CONFIG_HOME/CoolApp", if defined) 19 | //! // (e.g.: "/home/rusty/.config/CoolApp") 20 | //! // Android: "/data/user///CoolApp" 21 | //! // (e.g.: "/data/user/0/org.super_dev.cool_app/CoolApp") 22 | //! 23 | //! // How about nested cache data? 24 | //! println!("{:?}", get_app_dir(AppDataType::UserCache, &APP_INFO, "cache/images")); 25 | //! // Windows: "%LOCALAPPDATA%\SuperDev\CoolApp\cache\images" 26 | //! // (e.g.: "C:\Users\Rusty\AppData\Local\SuperDev\CoolApp\cache\images") 27 | //! // macOS: "$HOME/Library/Caches/CoolApp/cache/images" 28 | //! // (e.g.: "/Users/Rusty/Library/Caches/CoolApp/cache/images") 29 | //! // *nix: "$HOME/.cache/CoolApp/cache/images" 30 | //! // (or "$XDG_CACHE_HOME/CoolApp/cache/images", if defined) 31 | //! // (e.g.: "/home/rusty/.cache/CoolApp/cache/images") 32 | //! // Android: "/data/user///cache/CoolApp" 33 | //! // (e.g.: "/data/user/0/org.super_dev.cool_app/cache/CoolApp") 34 | //! 35 | //! // Remove "get_" prefix to recursively create nonexistent directories: 36 | //! // app_root(AppDataType::UserConfig, &APP_INFO) 37 | //! // app_dir(AppDataType::UserCache, &APP_INFO, "cache/images") 38 | //! } 39 | //! ``` 40 | 41 | mod common; 42 | pub use crate::common::*; 43 | mod imp; 44 | pub use crate::imp::*; 45 | mod utils; 46 | pub use crate::utils::*; 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::*; 51 | use crate::AppDataType::*; 52 | #[test] 53 | fn it_works() { 54 | let info = AppInfo { 55 | name: "Awesome App", 56 | author: "Dedicated Dev", 57 | }; 58 | let path = "/.not-hidden/subfolder!/with?/uni.code/¡Olé!/"; 59 | let types = [UserConfig, UserData, UserCache, SharedData, SharedConfig]; 60 | for &t in &types { 61 | println!("{:?} data root = {:?}", t, get_data_root(t)); 62 | println!("{:?} app root = {:?}", t, get_app_root(t, &info)); 63 | println!("{:?} data dir = {:?}", t, get_app_dir(t, &info, path)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /// Returns a cross-platform-filename-safe version of any string. 2 | /// 3 | /// This is used internally to generate app data directories based on app 4 | /// name/author. App developers can use it for consistency when dealing with 5 | /// file system operations. 6 | /// 7 | /// Do not apply this function to full paths, as it will sanitize '/' and '\'; 8 | /// it should only be used on directory or file names (i.e. path segments). 9 | #[must_use] 10 | pub fn sanitized(component: &str) -> String { 11 | let mut buf = String::with_capacity(component.len()); 12 | for (i, c) in component.chars().enumerate() { 13 | let is_alnum = c.is_ascii_alphanumeric(); 14 | let is_space = c == ' '; 15 | let is_hyphen = c == '-'; 16 | let is_underscore = c == '_'; 17 | let is_period = c == '.' && i != 0; // Disallow accidentally hidden folders 18 | let is_valid = is_alnum || is_space || is_hyphen || is_underscore || is_period; 19 | if is_valid { 20 | buf.push(c); 21 | } else { 22 | use std::fmt::Write; 23 | let _ = write!(&mut buf, ",{},", c as u32); 24 | } 25 | } 26 | buf 27 | } 28 | -------------------------------------------------------------------------------- /tests/fs.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))] 2 | 3 | use std::env; 4 | use std::io; 5 | use std::path; 6 | use std::sync; 7 | 8 | use app_dirs2::AppDataType; 9 | use once_cell::sync::Lazy; 10 | use test_case::test_case; 11 | 12 | // This test suite checks the effects of the app_dirs2 crate on the file system. 13 | // 14 | // The functions with the prefix get_ should not touch the file system. The functions without the 15 | // prefix should create the returned directory if it doesn’t exist. 16 | // 17 | // As only the unix/XDG implementation supports changing the root configuration directory, we can 18 | // only run this test suite on this platform. As we use environment variables to set the 19 | // configuration root, we have to make sure that the tests are run in sequence and don’t overlap, 20 | // see the `ENV_MUTEX` mutex. 21 | 22 | // For test cases that depend on environment variables 23 | static ENV_MUTEX: Lazy> = Lazy::new(|| sync::Mutex::new(())); 24 | 25 | fn set_root_dir(path: &path::Path) -> path::PathBuf { 26 | let root = path.join("root"); 27 | env::set_var("HOME", root.join("home")); 28 | env::set_var("XDG_CACHE_HOME", ""); 29 | env::set_var("XDG_CONFIG_HOME", ""); 30 | env::set_var("XDG_DATA_HOME", ""); 31 | env::set_var("XDG_DATA_DIRS", root.join("data")); 32 | env::set_var("XDG_CONFIG_DIRS", root.join("config")); 33 | root 34 | } 35 | 36 | #[test_case(AppDataType::UserCache; "user cache")] 37 | #[test_case(AppDataType::UserConfig; "user config")] 38 | #[test_case(AppDataType::UserData; "user data")] 39 | #[test_case(AppDataType::SharedConfig; "shared config")] 40 | #[test_case(AppDataType::SharedData; "shared data")] 41 | fn test_no_create(ty: AppDataType) -> io::Result<()> { 42 | let _env_guard = ENV_MUTEX.lock(); 43 | 44 | let dir = tempfile::tempdir()?; 45 | let root_dir = set_root_dir(dir.path()); 46 | 47 | let info = app_dirs2::AppInfo { 48 | name: "test-app", 49 | author: "test-author", 50 | }; 51 | 52 | let data_root = app_dirs2::get_data_root(ty).unwrap(); 53 | assert!( 54 | data_root.starts_with(&root_dir), 55 | "Data root does not start with root dir: data root = {}, root dir = {}", 56 | data_root.display(), 57 | root_dir.display() 58 | ); 59 | assert!(!root_dir.exists()); 60 | 61 | let app_root = app_dirs2::get_app_root(ty, &info).unwrap(); 62 | assert!( 63 | app_root.starts_with(&data_root), 64 | "App root does not start with data root: app root = {}, data root = {}", 65 | app_root.display(), 66 | data_root.display() 67 | ); 68 | assert!(!root_dir.exists()); 69 | 70 | let app_dir = app_dirs2::get_app_dir(ty, &info, "testdir").unwrap(); 71 | assert!( 72 | app_dir.starts_with(&app_root), 73 | "App dir does not start with app root: app dir = {}, app root = {}", 74 | app_dir.display(), 75 | app_root.display() 76 | ); 77 | assert!(!root_dir.exists()); 78 | 79 | dir.close() 80 | } 81 | 82 | #[test_case(AppDataType::UserCache; "user cache")] 83 | #[test_case(AppDataType::UserConfig; "user config")] 84 | #[test_case(AppDataType::UserData; "user data")] 85 | #[test_case(AppDataType::SharedConfig; "shared config")] 86 | #[test_case(AppDataType::SharedData; "shared data")] 87 | fn test_create(ty: AppDataType) -> io::Result<()> { 88 | let _env_guard = ENV_MUTEX.lock(); 89 | 90 | let dir = tempfile::tempdir()?; 91 | let root_dir = set_root_dir(dir.path()); 92 | 93 | let info = app_dirs2::AppInfo { 94 | name: "test-app", 95 | author: "test-author", 96 | }; 97 | 98 | let data_root = app_dirs2::data_root(ty).unwrap(); 99 | assert!( 100 | data_root.starts_with(&root_dir), 101 | "Data root does not start with root dir: data root = {}, root dir = {}", 102 | data_root.display(), 103 | root_dir.display() 104 | ); 105 | assert!(data_root.is_dir()); 106 | 107 | let app_root = app_dirs2::app_root(ty, &info).unwrap(); 108 | assert!( 109 | app_root.starts_with(&data_root), 110 | "App root does not start with data root: app root = {}, data root = {}", 111 | app_root.display(), 112 | data_root.display() 113 | ); 114 | assert!(app_root.is_dir()); 115 | 116 | let app_dir = app_dirs2::app_dir(ty, &info, "testdir").unwrap(); 117 | assert!( 118 | app_dir.starts_with(&app_root), 119 | "App dir does not start with app root: app dir = {}, app root = {}", 120 | app_dir.display(), 121 | app_root.display() 122 | ); 123 | assert!(app_dir.is_dir()); 124 | 125 | dir.close() 126 | } 127 | -------------------------------------------------------------------------------- /tests/unix.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))] 2 | 3 | use std::env; 4 | use std::ffi; 5 | use std::path; 6 | use std::sync; 7 | 8 | use app_dirs2::AppDataType; 9 | use once_cell::sync::Lazy; 10 | use test_case::test_case; 11 | 12 | // For test cases that depend on environment variables 13 | static ENV_MUTEX: Lazy> = Lazy::new(|| sync::Mutex::new(())); 14 | 15 | fn reset_env() { 16 | env::set_var("HOME", ""); 17 | env::set_var("XDG_CACHE_HOME", ""); 18 | env::set_var("XDG_CONFIG_HOME", ""); 19 | env::set_var("XDG_DATA_HOME", ""); 20 | env::set_var("XDG_DATA_DIRS", ""); 21 | env::set_var("XDG_CONFIG_DIRS", ""); 22 | } 23 | 24 | #[test_case(AppDataType::UserCache, ".cache"; "user cache")] 25 | #[test_case(AppDataType::UserConfig, ".config"; "user config")] 26 | #[test_case(AppDataType::UserData, ".local/share"; "user data")] 27 | #[test_case(AppDataType::SharedConfig, "/etc/xdg"; "shared config")] 28 | #[test_case(AppDataType::SharedData, "/usr/local/share"; "shared data")] 29 | fn test_home(ty: AppDataType, path: impl AsRef) { 30 | let _env_guard = ENV_MUTEX.lock(); 31 | 32 | let dir = tempfile::tempdir().unwrap(); 33 | reset_env(); 34 | env::set_var("HOME", dir.path()); 35 | 36 | let data_root = app_dirs2::get_data_root(ty).unwrap(); 37 | if ty.is_shared() { 38 | assert_eq!(path.as_ref(), data_root.as_path()); 39 | } else { 40 | assert_eq!(dir.path().join(path.as_ref()), data_root); 41 | } 42 | 43 | let app_info = app_dirs2::AppInfo { 44 | name: "app-name", 45 | author: "app-author", 46 | }; 47 | 48 | let app_root = app_dirs2::get_app_root(ty, &app_info).unwrap(); 49 | assert_eq!(data_root.join(app_info.name), app_root); 50 | 51 | let subdir = "testdir"; 52 | let app_dir = app_dirs2::get_app_dir(ty, &app_info, subdir).unwrap(); 53 | assert_eq!(app_root.join(subdir), app_dir); 54 | } 55 | 56 | #[test_case(AppDataType::UserCache, "XDG_CACHE_HOME"; "user cache")] 57 | #[test_case(AppDataType::UserConfig, "XDG_CONFIG_HOME"; "user config")] 58 | #[test_case(AppDataType::UserData, "XDG_DATA_HOME"; "user data")] 59 | #[test_case(AppDataType::SharedConfig, "XDG_CONFIG_DIRS"; "shared config")] 60 | #[test_case(AppDataType::SharedData, "XDG_DATA_DIRS"; "shared data")] 61 | fn test_xdg_dirs(ty: AppDataType, env_var: impl AsRef) { 62 | let _env_guard = ENV_MUTEX.lock(); 63 | 64 | let dir = tempfile::tempdir().unwrap(); 65 | reset_env(); 66 | env::set_var(env_var.as_ref(), dir.path()); 67 | 68 | let data_root = app_dirs2::get_data_root(ty).unwrap(); 69 | assert_eq!(dir.path(), data_root.as_path()); 70 | 71 | let app_info = app_dirs2::AppInfo { 72 | name: "app-name", 73 | author: "app-author", 74 | }; 75 | 76 | let app_root = app_dirs2::get_app_root(ty, &app_info).unwrap(); 77 | assert_eq!(data_root.join(app_info.name), app_root); 78 | 79 | let subdir = "testdir"; 80 | let app_dir = app_dirs2::get_app_dir(ty, &app_info, subdir).unwrap(); 81 | assert_eq!(app_root.join(subdir), app_dir); 82 | } 83 | 84 | #[test_case(AppDataType::UserCache, "XDG_CACHE_HOME"; "user cache")] 85 | #[test_case(AppDataType::UserConfig, "XDG_CONFIG_HOME"; "user config")] 86 | #[test_case(AppDataType::UserData, "XDG_DATA_HOME"; "user data")] 87 | #[test_case(AppDataType::SharedConfig, "XDG_CONFIG_DIRS"; "shared config")] 88 | #[test_case(AppDataType::SharedData, "XDG_DATA_DIRS"; "shared data")] 89 | fn test_home_and_xdg_dirs(ty: AppDataType, env_var: impl AsRef) { 90 | let _env_guard = ENV_MUTEX.lock(); 91 | 92 | let home_dir = tempfile::tempdir().unwrap(); 93 | let xdg_dir = tempfile::tempdir().unwrap(); 94 | reset_env(); 95 | env::set_var("HOME", home_dir.path()); 96 | env::set_var(env_var.as_ref(), xdg_dir.path()); 97 | 98 | let data_root = app_dirs2::get_data_root(ty).unwrap(); 99 | assert_eq!(xdg_dir.path(), data_root.as_path()); 100 | 101 | let app_info = app_dirs2::AppInfo { 102 | name: "app-name", 103 | author: "app-author", 104 | }; 105 | 106 | let app_root = app_dirs2::get_app_root(ty, &app_info).unwrap(); 107 | assert_eq!(data_root.join(app_info.name), app_root); 108 | 109 | let subdir = "testdir"; 110 | let app_dir = app_dirs2::get_app_dir(ty, &app_info, subdir).unwrap(); 111 | assert_eq!(app_root.join(subdir), app_dir); 112 | } 113 | --------------------------------------------------------------------------------