openlogi_core/paths.rs
1//! Per-OS application directories, following the XDG Base Directory spec on
2//! **every** platform — including macOS, so configuration lives at the
3//! familiar `~/.config/openlogi/` rather than macOS's
4//! `~/Library/Application Support/`.
5//!
6//! | kind | env override | default |
7//! |--------|---------------------|-------------------------------|
8//! | config | `$XDG_CONFIG_HOME` | `~/.config/openlogi` |
9//! | data | `$XDG_DATA_HOME` | `~/.local/share/openlogi` |
10//!
11//! On Windows `$HOME` falls back to `%USERPROFILE%`, so paths resolve to
12//! `%USERPROFILE%\.config\openlogi` etc. — best-effort until a real Windows
13//! port lands.
14
15use std::ffi::OsString;
16use std::path::{Path, PathBuf};
17
18use thiserror::Error;
19
20/// Subdirectory created under each XDG base directory.
21const APP_DIR: &str = "openlogi";
22
23#[derive(Debug, Error)]
24pub enum PathsError {
25 #[error("could not resolve a home directory for the current user")]
26 HomeNotFound,
27}
28
29/// The user's home directory: `$HOME`, falling back to `%USERPROFILE%`.
30fn home() -> Result<PathBuf, PathsError> {
31 std::env::var_os("HOME")
32 .or_else(|| std::env::var_os("USERPROFILE"))
33 .filter(|h| !h.is_empty())
34 .map(PathBuf::from)
35 .ok_or(PathsError::HomeNotFound)
36}
37
38/// Resolve an XDG base directory plus the [`APP_DIR`] subdir.
39///
40/// Honours `env_value` only when it is an absolute path — per the spec a
41/// relative `$XDG_*_HOME` is invalid and must be ignored — otherwise falls
42/// back to `$HOME/<fallback>`. Split from the `std::env` read so the
43/// branching can be unit-tested without mutating process-global env vars.
44fn xdg_base(env_value: Option<OsString>, fallback: &[&str]) -> Result<PathBuf, PathsError> {
45 match env_value {
46 Some(v) if Path::new(&v).is_absolute() => Ok(PathBuf::from(v).join(APP_DIR)),
47 _ => {
48 let mut dir = home()?;
49 dir.extend(fallback);
50 dir.push(APP_DIR);
51 Ok(dir)
52 }
53 }
54}
55
56/// Directory holding the user's `config.toml`.
57///
58/// `$XDG_CONFIG_HOME/openlogi`, default `~/.config/openlogi`.
59pub fn config_dir() -> Result<PathBuf, PathsError> {
60 xdg_base(std::env::var_os("XDG_CONFIG_HOME"), &[".config"])
61}
62
63/// Full path to the user config file.
64pub fn config_path() -> Result<PathBuf, PathsError> {
65 Ok(config_dir()?.join("config.toml"))
66}
67
68/// Directory for downloaded application data; the device-render asset cache
69/// lives under `data_dir()/assets`.
70///
71/// `$XDG_DATA_HOME/openlogi`, default `~/.local/share/openlogi`.
72pub fn data_dir() -> Result<PathBuf, PathsError> {
73 xdg_base(std::env::var_os("XDG_DATA_HOME"), &[".local", "share"])
74}
75
76#[cfg(all(test, unix))]
77#[allow(clippy::expect_used, reason = "expect/unwrap are idiomatic in tests")]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn absolute_xdg_override_is_used_verbatim() {
83 let dir = xdg_base(Some("/tmp/xdg-config".into()), &[".config"])
84 .expect("absolute override needs no home dir");
85 assert_eq!(dir, PathBuf::from("/tmp/xdg-config/openlogi"));
86 }
87
88 #[test]
89 fn relative_xdg_value_is_ignored_per_spec() {
90 // A relative $XDG_*_HOME is invalid, so this must fall back to
91 // $HOME/.config/openlogi rather than honour the relative value.
92 let dir = xdg_base(Some("relative/dir".into()), &[".config"]).expect("home dir resolves");
93 assert!(dir.ends_with("openlogi"));
94 assert!(!dir.to_string_lossy().contains("relative"));
95 }
96}