1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, anyhow};
4const APP_DIR: &str = ".mag";
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct AppPaths {
8 pub home_dir: PathBuf,
9 pub data_root: PathBuf,
10 pub database_path: PathBuf,
11 pub model_root: PathBuf,
12 pub benchmark_root: PathBuf,
13}
14
15#[allow(dead_code)] pub fn xdg_config_home(home: &Path) -> PathBuf {
19 std::env::var_os("XDG_CONFIG_HOME")
20 .map(PathBuf::from)
21 .filter(|p| p.is_absolute())
22 .unwrap_or_else(|| home.join(".config"))
23}
24
25pub fn home_dir() -> Result<PathBuf> {
26 std::env::var_os("HOME")
27 .or_else(|| std::env::var_os("USERPROFILE"))
28 .map(PathBuf::from)
29 .ok_or_else(|| anyhow!("neither HOME nor USERPROFILE is set"))
30}
31
32pub fn resolve_data_root(home: &Path) -> Result<PathBuf> {
38 match std::env::var_os("MAG_DATA_ROOT") {
39 Some(val) => {
40 if val.is_empty() {
41 return Err(anyhow!(
42 "MAG_DATA_ROOT is set but empty; unset it or provide an absolute path"
43 ));
44 }
45 let path = PathBuf::from(val);
46 if path.is_absolute() {
47 Ok(path)
48 } else {
49 Err(anyhow!(
50 "MAG_DATA_ROOT must be an absolute path, got: {}",
51 path.display()
52 ))
53 }
54 }
55 None => Ok(home.join(APP_DIR)),
56 }
57}
58
59pub fn resolve_app_paths() -> Result<AppPaths> {
60 let home = home_dir()?;
61 let data_root = resolve_data_root(&home)?;
62 Ok(app_paths_for(home, data_root))
63}
64
65fn app_paths_for(home: PathBuf, data_root: PathBuf) -> AppPaths {
66 AppPaths {
67 home_dir: home,
68 database_path: data_root.join("memory.db"),
69 model_root: data_root.join("models"),
70 benchmark_root: data_root.join("benchmarks"),
71 data_root,
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn uses_mag_root() {
81 let home = std::env::temp_dir().join(format!("mag-paths-{}", uuid::Uuid::new_v4()));
82 let data_root = home.join(APP_DIR);
83 let paths = app_paths_for(home.clone(), data_root);
84 assert_eq!(paths.data_root, home.join(APP_DIR));
85 assert_eq!(paths.database_path, home.join(APP_DIR).join("memory.db"));
86 assert_eq!(paths.model_root, home.join(APP_DIR).join("models"));
87 assert_eq!(paths.benchmark_root, home.join(APP_DIR).join("benchmarks"));
88 }
89
90 #[test]
91 fn mag_data_root_override_absolute_path() {
92 let override_dir =
93 std::env::temp_dir().join(format!("mag-override-{}", uuid::Uuid::new_v4()));
94 let _guard = crate::test_helpers::HOME_MUTEX
96 .lock()
97 .unwrap_or_else(|e| e.into_inner());
98 let prev = std::env::var_os("MAG_DATA_ROOT");
99 unsafe { std::env::set_var("MAG_DATA_ROOT", &override_dir) };
101 let home = PathBuf::from("/home/testuser");
102 let result = resolve_data_root(&home);
103 unsafe {
105 match prev {
106 Some(v) => std::env::set_var("MAG_DATA_ROOT", v),
107 None => std::env::remove_var("MAG_DATA_ROOT"),
108 }
109 }
110 let data_root = result.expect("absolute MAG_DATA_ROOT should be accepted");
111 assert_eq!(data_root, override_dir);
112 }
113
114 #[test]
115 fn mag_data_root_fallback_to_home_dot_mag() {
116 let _guard = crate::test_helpers::HOME_MUTEX
117 .lock()
118 .unwrap_or_else(|e| e.into_inner());
119 let prev = std::env::var_os("MAG_DATA_ROOT");
120 unsafe { std::env::remove_var("MAG_DATA_ROOT") };
122 let home = PathBuf::from("/home/testuser");
123 let result = resolve_data_root(&home);
124 unsafe {
126 if let Some(v) = prev {
127 std::env::set_var("MAG_DATA_ROOT", v);
128 }
129 }
130 assert_eq!(result.unwrap(), PathBuf::from("/home/testuser/.mag"));
131 }
132
133 #[test]
134 fn mag_data_root_rejects_empty_string() {
135 let _guard = crate::test_helpers::HOME_MUTEX
136 .lock()
137 .unwrap_or_else(|e| e.into_inner());
138 let prev = std::env::var_os("MAG_DATA_ROOT");
139 unsafe { std::env::set_var("MAG_DATA_ROOT", "") };
141 let home = PathBuf::from("/home/testuser");
142 let result = resolve_data_root(&home);
143 unsafe {
145 match prev {
146 Some(v) => std::env::set_var("MAG_DATA_ROOT", v),
147 None => std::env::remove_var("MAG_DATA_ROOT"),
148 }
149 }
150 assert!(result.is_err(), "empty MAG_DATA_ROOT should be rejected");
151 let err = result.unwrap_err().to_string();
152 assert!(err.contains("empty"), "error should mention 'empty'");
153 }
154
155 #[test]
156 fn mag_data_root_rejects_relative_path() {
157 let _guard = crate::test_helpers::HOME_MUTEX
158 .lock()
159 .unwrap_or_else(|e| e.into_inner());
160 let prev = std::env::var_os("MAG_DATA_ROOT");
161 unsafe { std::env::set_var("MAG_DATA_ROOT", "relative/path") };
163 let home = PathBuf::from("/home/testuser");
164 let result = resolve_data_root(&home);
165 unsafe {
167 match prev {
168 Some(v) => std::env::set_var("MAG_DATA_ROOT", v),
169 None => std::env::remove_var("MAG_DATA_ROOT"),
170 }
171 }
172 assert!(result.is_err(), "relative MAG_DATA_ROOT should be rejected");
173 let err = result.unwrap_err().to_string();
174 assert!(err.contains("absolute"), "error should mention 'absolute'");
175 }
176}