Skip to main content

rgx/
paths.rs

1//! Where a project's daemon keeps its socket and index snapshot.
2//!
3//! State lives under the user cache dir (`$RGX_CACHE_DIR`, else the config file's `cache_dir`, else
4//! `$XDG_CACHE_HOME/rgx`, else `~/.cache/rgx`), with a `<hash>` subdir keyed by the canonical root
5//! path, so rgx never writes into the repo it indexes.
6
7use std::collections::hash_map::DefaultHasher;
8use std::hash::{Hash, Hasher};
9use std::path::{Path, PathBuf};
10
11use crate::config::Config;
12
13/// Canonicalize a root argument (defaulting to the current directory).
14pub fn resolve_root(arg: Option<&str>) -> PathBuf {
15    let raw = arg.unwrap_or(".");
16    std::fs::canonicalize(raw).unwrap_or_else(|_| PathBuf::from(raw))
17}
18
19/// The per-root state directory, created on demand.
20///
21/// `$RGX_CACHE_DIR` overrides the base and is used verbatim; otherwise the config file's `cache_dir`,
22/// then `$XDG_CACHE_HOME/rgx`, then `~/.cache/rgx`, then a temp dir.
23pub fn state_dir(root: &Path) -> PathBuf {
24    let mut h = DefaultHasher::new();
25    root.hash(&mut h);
26    let hash = format!("{:016x}", h.finish());
27    // Windows has no XDG/HOME convention, so fall back to the standard per-user cache roots there.
28    // An exported-but-empty var is treated as unset, so a stray `FOO=` never yields a relative base.
29    let xdg = non_empty(std::env::var_os("XDG_CACHE_HOME").or_else(win_var("LOCALAPPDATA")));
30    let home = non_empty(std::env::var_os("HOME").or_else(win_var("USERPROFILE")));
31    let rgx_cache = non_empty(std::env::var_os("RGX_CACHE_DIR"));
32    let cfg = Config::get().cache_dir.clone();
33    let base = cache_base(rgx_cache, cfg, xdg, home);
34    base.join(hash)
35}
36
37#[cfg(windows)]
38pub(crate) fn win_var(name: &'static str) -> impl FnOnce() -> Option<std::ffi::OsString> {
39    move || std::env::var_os(name)
40}
41
42#[cfg(not(windows))]
43pub(crate) fn win_var(_name: &'static str) -> impl FnOnce() -> Option<std::ffi::OsString> {
44    || None
45}
46
47/// Treat an exported-but-empty environment value as unset, so `FOO=` never resolves to a path.
48pub(crate) fn non_empty(v: Option<std::ffi::OsString>) -> Option<std::ffi::OsString> {
49    v.filter(|s| !s.is_empty())
50}
51
52fn cache_base(
53    rgx_cache_dir: Option<std::ffi::OsString>,
54    config_cache_dir: Option<PathBuf>,
55    xdg_cache_home: Option<std::ffi::OsString>,
56    home: Option<std::ffi::OsString>,
57) -> PathBuf {
58    if let Some(dir) = rgx_cache_dir {
59        return PathBuf::from(dir);
60    }
61    if let Some(dir) = config_cache_dir {
62        return dir;
63    }
64    xdg_cache_home
65        .map(PathBuf::from)
66        .or_else(|| home.map(|h| PathBuf::from(h).join(".cache")))
67        .unwrap_or_else(std::env::temp_dir)
68        .join("rgx")
69}
70
71pub fn snapshot_path(root: &Path) -> PathBuf {
72    state_dir(root).join("index.bin")
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::ffi::OsString;
79
80    fn os(s: &str) -> Option<OsString> {
81        Some(OsString::from(s))
82    }
83
84    #[test]
85    fn cache_base_precedence() {
86        let cfg = || Some(PathBuf::from("/cfg/cache"));
87        assert_eq!(
88            cache_base(os("/custom/rgx"), cfg(), os("/xdg"), os("/home/u")),
89            PathBuf::from("/custom/rgx")
90        );
91        assert_eq!(
92            cache_base(None, cfg(), os("/xdg"), os("/home/u")),
93            PathBuf::from("/cfg/cache")
94        );
95        assert_eq!(
96            cache_base(None, None, os("/xdg"), os("/home/u")),
97            PathBuf::from("/xdg/rgx")
98        );
99        assert_eq!(
100            cache_base(None, None, None, os("/home/u")),
101            PathBuf::from("/home/u/.cache/rgx")
102        );
103    }
104}