use std::ffi::OsString;
use std::path::{Path, PathBuf};
use crate::join::clean;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub(crate) use windows::{SystemBases, UserBases};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub(crate) use macos::{SystemBases, UserBases};
#[cfg(all(unix, not(target_os = "macos")))]
mod xdg;
#[cfg(all(unix, not(target_os = "macos")))]
pub(crate) use xdg::{SystemBases, UserBases};
#[cfg(any(test, all(unix, not(target_os = "macos"))))]
pub(crate) mod user_dirs;
#[cfg(not(any(unix, windows)))]
compile_error!(
"path-lib supports Unix-like targets (Linux, macOS, BSD) and Windows; \
this target has no home directory model or filesystem layout to resolve"
);
pub(crate) type Env<'a> = &'a dyn Fn(&str) -> Option<OsString>;
pub(crate) fn process_env(key: &str) -> Option<OsString> {
std::env::var_os(key)
}
#[derive(Debug, Clone)]
pub(crate) struct Folders {
pub home: PathBuf,
pub documents: PathBuf,
pub desktop: PathBuf,
pub downloads: PathBuf,
pub pictures: PathBuf,
pub music: PathBuf,
pub videos: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Layout {
pub data: PathBuf,
pub config: PathBuf,
pub cache: PathBuf,
pub logs: PathBuf,
pub state: PathBuf,
pub runtime: PathBuf,
pub temp: PathBuf,
pub documents: PathBuf,
}
impl Layout {
pub(crate) fn under(root: &Path) -> Self {
Self {
data: root.join("data"),
config: root.join("config"),
cache: root.join("cache"),
logs: root.join("logs"),
state: root.join("state"),
runtime: root.join("run"),
temp: root.join("temp"),
documents: root.join("documents"),
}
}
#[cfg(test)]
pub(crate) fn all(&self) -> [&Path; 8] {
[
&self.data,
&self.config,
&self.cache,
&self.logs,
&self.state,
&self.runtime,
&self.temp,
&self.documents,
]
}
}
pub(crate) fn absolute_var(env: Env<'_>, key: &str) -> Option<PathBuf> {
let value = env(key)?;
if value.is_empty() {
return None;
}
let path = Path::new(&value);
if !path.is_absolute() {
return None;
}
Some(clean(path))
}
#[cfg(test)]
pub(crate) mod test_env {
use std::collections::HashMap;
use std::ffi::OsString;
pub(crate) fn env_from(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + use<> {
let map: HashMap<String, OsString> = pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), OsString::from(*v)))
.collect();
move |k: &str| map.get(k).cloned()
}
pub(crate) fn assert_distinct(layout: &super::Layout) {
let all = layout.all();
for (i, a) in all.iter().enumerate() {
for b in &all[i + 1..] {
assert_ne!(a, b, "two kinds share a directory: {a:?}");
}
}
}
}
#[cfg(test)]
mod tests {
use super::test_env::{assert_distinct, env_from};
use super::*;
#[test]
fn test_absolute_var_rejects_relative_and_empty() {
let env = env_from(&[("REL", "relative/dir"), ("EMPTY", ""), ("DOT", ".")]);
assert_eq!(absolute_var(&env, "REL"), None);
assert_eq!(absolute_var(&env, "EMPTY"), None);
assert_eq!(absolute_var(&env, "DOT"), None);
assert_eq!(absolute_var(&env, "MISSING"), None);
}
#[test]
fn test_absolute_var_cleans_value() {
let raw = if cfg!(windows) {
"C:/Users//x/./y/"
} else {
"/home//x/./y/"
};
let want = if cfg!(windows) {
r"C:\Users\x\y"
} else {
"/home/x/y"
};
let env = env_from(&[("V", raw)]);
assert_eq!(absolute_var(&env, "V"), Some(PathBuf::from(want)));
}
#[test]
fn test_portable_layout_is_distinct_and_under_root() {
let root = PathBuf::from("portable-root");
let layout = Layout::under(&root);
assert_distinct(&layout);
for p in layout.all() {
assert!(p.starts_with(&root));
}
}
}