use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
type EnvReader<'lookup> = &'lookup dyn Fn(&str) -> Option<OsString>;
fn process_env(name: &str) -> Option<OsString> {
env::var_os(name)
}
fn absolute_env_path(env_reader: EnvReader<'_>, variable: &str) -> Option<PathBuf> {
let value = env_reader(variable)?;
if value.is_empty() {
return None;
}
let path = PathBuf::from(value);
path.is_absolute().then_some(path)
}
fn home_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
let variable = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
absolute_env_path(env_reader, variable)
}
fn data_local_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
if cfg!(windows) {
absolute_env_path(env_reader, "LOCALAPPDATA")
} else if cfg!(target_os = "macos") {
home_dir_from(env_reader).map(|home| home.join("Library").join("Application Support"))
} else {
absolute_env_path(env_reader, "XDG_DATA_HOME")
.or_else(|| home_dir_from(env_reader).map(|home| home.join(".local").join("share")))
}
}
fn cache_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
if cfg!(windows) {
absolute_env_path(env_reader, "LOCALAPPDATA")
} else if cfg!(target_os = "macos") {
home_dir_from(env_reader).map(|home| home.join("Library").join("Caches"))
} else {
absolute_env_path(env_reader, "XDG_CACHE_HOME")
.or_else(|| home_dir_from(env_reader).map(|home| home.join(".cache")))
}
}
fn data_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
if cfg!(windows) {
absolute_env_path(env_reader, "APPDATA")
} else {
data_local_dir_from(env_reader)
}
}
fn config_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
if cfg!(windows) {
absolute_env_path(env_reader, "APPDATA")
} else if cfg!(target_os = "macos") {
home_dir_from(env_reader).map(|home| home.join("Library").join("Application Support"))
} else {
absolute_env_path(env_reader, "XDG_CONFIG_HOME")
.or_else(|| home_dir_from(env_reader).map(|home| home.join(".config")))
}
}
fn runtime_dir_from(env_reader: EnvReader<'_>) -> Option<PathBuf> {
if cfg!(all(unix, not(target_os = "macos"))) {
absolute_env_path(env_reader, "XDG_RUNTIME_DIR")
} else {
None
}
}
#[must_use]
pub fn home_dir() -> Option<PathBuf> {
home_dir_from(&process_env)
}
#[must_use]
pub fn data_local_dir() -> Option<PathBuf> {
data_local_dir_from(&process_env)
}
#[must_use]
pub fn cache_dir() -> Option<PathBuf> {
cache_dir_from(&process_env)
}
#[must_use]
pub fn data_dir() -> Option<PathBuf> {
data_dir_from(&process_env)
}
#[must_use]
pub fn config_dir() -> Option<PathBuf> {
config_dir_from(&process_env)
}
#[must_use]
pub fn runtime_dir() -> Option<PathBuf> {
runtime_dir_from(&process_env)
}
#[cfg(test)]
mod tests {
use super::{
absolute_env_path, cache_dir_from, config_dir_from, data_dir_from, data_local_dir_from,
home_dir_from, runtime_dir_from,
};
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::PathBuf;
fn env_of(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(name, value)| ((*name).to_string(), OsString::from(value)))
.collect();
move |name: &str| map.get(name).cloned()
}
#[test]
fn empty_and_relative_env_values_are_ignored() {
let empty = env_of(&[("XDG_CACHE_HOME", "")]);
assert_eq!(absolute_env_path(&empty, "XDG_CACHE_HOME"), None);
let relative = env_of(&[("XDG_CACHE_HOME", "relative/cache")]);
assert_eq!(
absolute_env_path(&relative, "XDG_CACHE_HOME"),
None,
"relative XDG values must be treated as unset per the spec"
);
let unset = env_of(&[]);
assert_eq!(absolute_env_path(&unset, "XDG_CACHE_HOME"), None);
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn xdg_overrides_win_then_home_then_none() {
let with_xdg = env_of(&[
("HOME", "/test-home"),
("XDG_DATA_HOME", "/xdg-data"),
("XDG_CACHE_HOME", "/xdg-cache"),
]);
assert_eq!(
data_local_dir_from(&with_xdg),
Some(PathBuf::from("/xdg-data"))
);
assert_eq!(cache_dir_from(&with_xdg), Some(PathBuf::from("/xdg-cache")));
let home_only = env_of(&[("HOME", "/test-home")]);
assert_eq!(
data_local_dir_from(&home_only),
Some(PathBuf::from("/test-home/.local/share"))
);
assert_eq!(
cache_dir_from(&home_only),
Some(PathBuf::from("/test-home/.cache"))
);
let relative_xdg = env_of(&[("HOME", "/test-home"), ("XDG_CACHE_HOME", "rel")]);
assert_eq!(
cache_dir_from(&relative_xdg),
Some(PathBuf::from("/test-home/.cache"))
);
let bare = env_of(&[]);
assert_eq!(home_dir_from(&bare), None);
assert_eq!(data_local_dir_from(&bare), None);
assert_eq!(cache_dir_from(&bare), None);
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn config_data_and_runtime_follow_xdg_rules() {
let env = env_of(&[
("HOME", "/test-home"),
("XDG_CONFIG_HOME", "/xdg-config"),
("XDG_RUNTIME_DIR", "/run/user/1000"),
]);
assert_eq!(config_dir_from(&env), Some(PathBuf::from("/xdg-config")));
assert_eq!(
runtime_dir_from(&env),
Some(PathBuf::from("/run/user/1000"))
);
assert_eq!(
data_dir_from(&env),
Some(PathBuf::from("/test-home/.local/share"))
);
let home_only = env_of(&[("HOME", "/test-home")]);
assert_eq!(
config_dir_from(&home_only),
Some(PathBuf::from("/test-home/.config"))
);
assert_eq!(
runtime_dir_from(&home_only),
None,
"XDG_RUNTIME_DIR has no fallback by specification"
);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_uses_library_locations() {
let env = env_of(&[("HOME", "/Users/test")]);
assert_eq!(home_dir_from(&env), Some(PathBuf::from("/Users/test")));
assert_eq!(
data_local_dir_from(&env),
Some(PathBuf::from("/Users/test/Library/Application Support"))
);
assert_eq!(
data_dir_from(&env),
Some(PathBuf::from("/Users/test/Library/Application Support"))
);
assert_eq!(
config_dir_from(&env),
Some(PathBuf::from("/Users/test/Library/Application Support"))
);
assert_eq!(
cache_dir_from(&env),
Some(PathBuf::from("/Users/test/Library/Caches"))
);
assert_eq!(runtime_dir_from(&env), None);
}
#[cfg(windows)]
#[test]
fn windows_uses_profile_and_local_appdata() {
let env = env_of(&[
("USERPROFILE", r"C:\Users\test"),
("LOCALAPPDATA", r"C:\Users\test\AppData\Local"),
]);
assert_eq!(home_dir_from(&env), Some(PathBuf::from(r"C:\Users\test")));
assert_eq!(
data_local_dir_from(&env),
Some(PathBuf::from(r"C:\Users\test\AppData\Local"))
);
assert_eq!(
cache_dir_from(&env),
Some(PathBuf::from(r"C:\Users\test\AppData\Local"))
);
}
}