kanade_shared/
default_paths.rs1use std::path::{Path, PathBuf};
31
32pub fn config_dir() -> PathBuf {
34 #[cfg(target_os = "windows")]
35 {
36 program_data().join("Kanade").join("config")
37 }
38 #[cfg(not(target_os = "windows"))]
39 {
40 PathBuf::from("/etc/kanade")
41 }
42}
43
44pub fn data_dir() -> PathBuf {
54 if let Some(os_path) = std::env::var_os("KANADE_AGENT_DATA_DIR").filter(|s| !s.is_empty()) {
55 let path = PathBuf::from(&os_path);
62 if path.is_absolute() {
63 return path;
64 }
65 return std::env::current_dir()
66 .map(|cwd| cwd.join(&path))
67 .unwrap_or_else(|_| PathBuf::from(os_path));
68 }
69 #[cfg(target_os = "windows")]
70 {
71 program_data().join("Kanade").join("data")
72 }
73 #[cfg(not(target_os = "windows"))]
74 {
75 PathBuf::from("/var/lib/kanade")
76 }
77}
78
79pub fn log_dir() -> PathBuf {
81 #[cfg(target_os = "windows")]
82 {
83 program_data().join("Kanade").join("logs")
84 }
85 #[cfg(not(target_os = "windows"))]
86 {
87 PathBuf::from("/var/log/kanade")
88 }
89}
90
91#[cfg(target_os = "windows")]
92fn program_data() -> PathBuf {
93 std::env::var_os("ProgramData")
94 .map(PathBuf::from)
95 .unwrap_or_else(|| PathBuf::from(r"C:\ProgramData"))
96}
97
98pub fn find_config(flag: Option<&Path>, env_var: &str, basename: &str) -> anyhow::Result<PathBuf> {
110 if let Some(p) = flag {
111 return Ok(p.to_path_buf());
112 }
113 if let Ok(raw) = std::env::var(env_var)
114 && !raw.is_empty()
115 {
116 return Ok(PathBuf::from(raw));
117 }
118 let std_path = config_dir().join(basename);
119 if std_path.exists() {
120 return Ok(std_path);
121 }
122 Err(anyhow::anyhow!(
123 "config not found — pass `--config <path>`, set `{env_var}`, or place `{basename}` at `{}`",
124 std_path.display(),
125 ))
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use std::path::PathBuf;
132
133 fn unique_env(test_name: &str) -> String {
137 format!("KANADE_TEST_CFG_{test_name}_{}", std::process::id())
138 }
139
140 static DATA_DIR_ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
147
148 fn lock_data_dir_env() -> std::sync::MutexGuard<'static, ()> {
149 DATA_DIR_ENV_LOCK
150 .lock()
151 .unwrap_or_else(|poisoned| poisoned.into_inner())
152 }
153
154 #[test]
155 fn find_config_prefers_flag_over_env_and_default() {
156 let env = unique_env("flag_wins");
157 unsafe {
159 std::env::set_var(&env, "env-path.toml");
160 }
161 let flag = PathBuf::from("flag-path.toml");
162 let got = find_config(Some(&flag), &env, "agent.toml").expect("ok");
163 assert_eq!(got, flag);
164 unsafe { std::env::remove_var(&env) };
165 }
166
167 #[test]
168 fn find_config_uses_env_when_flag_missing() {
169 let env = unique_env("env_wins");
170 unsafe {
171 std::env::set_var(&env, "env-path.toml");
172 }
173 let got = find_config(None, &env, "agent.toml").expect("ok");
174 assert_eq!(got, PathBuf::from("env-path.toml"));
175 unsafe { std::env::remove_var(&env) };
176 }
177
178 #[test]
179 fn find_config_skips_empty_env() {
180 let env = unique_env("empty_env");
181 unsafe {
182 std::env::set_var(&env, "");
183 }
184 let r = find_config(None, &env, "non-existent-kanade-test.toml");
187 assert!(r.is_err(), "expected error, got {:?}", r);
188 unsafe { std::env::remove_var(&env) };
189 }
190
191 #[test]
192 fn find_config_errors_with_helpful_message() {
193 let env = unique_env("missing");
194 let r = find_config(None, &env, "definitely-not-here-kanade-test.toml");
195 let err = r.expect_err("should error");
196 let msg = format!("{err}");
197 assert!(msg.contains("--config"), "msg: {msg}");
198 assert!(msg.contains(&env), "msg: {msg}");
199 assert!(
200 msg.contains("definitely-not-here-kanade-test.toml"),
201 "msg: {msg}"
202 );
203 }
204
205 #[test]
213 fn data_dir_promotes_relative_env_to_absolute() {
214 let _g = lock_data_dir_env();
215 unsafe {
216 std::env::set_var("KANADE_AGENT_DATA_DIR", "target/dev-data/agents/dev-pc-x");
217 }
218 let p = data_dir();
219 assert!(p.is_absolute(), "expected absolute path, got {p:?}");
220 assert!(
221 p.ends_with("target/dev-data/agents/dev-pc-x")
222 || p.ends_with(r"target\dev-data\agents\dev-pc-x"),
223 "trailing components should match the env value, got {p:?}",
224 );
225 unsafe {
226 std::env::remove_var("KANADE_AGENT_DATA_DIR");
227 }
228 }
229
230 #[test]
231 fn dirs_are_os_appropriate() {
232 let _g = lock_data_dir_env();
233 let cfg = config_dir();
234 let data = data_dir();
235 let logs = log_dir();
236 assert!(cfg.is_absolute(), "config_dir = {cfg:?}");
238 assert!(data.is_absolute(), "data_dir = {data:?}");
239 assert!(logs.is_absolute(), "log_dir = {logs:?}");
240 assert_ne!(cfg, data);
242 assert_ne!(data, logs);
243 assert_ne!(cfg, logs);
244 }
245
246 #[cfg(target_os = "windows")]
247 #[test]
248 fn windows_dirs_root_at_program_data_kanade() {
249 let _g = lock_data_dir_env();
250 let cfg = config_dir();
251 let data = data_dir();
252 let logs = log_dir();
253 assert!(cfg.ends_with("Kanade\\config"), "{cfg:?}");
254 assert!(data.ends_with("Kanade\\data"), "{data:?}");
255 assert!(logs.ends_with("Kanade\\logs"), "{logs:?}");
256 }
257
258 #[cfg(not(target_os = "windows"))]
259 #[test]
260 fn unix_dirs_match_fhs_conventions() {
261 let _g = lock_data_dir_env();
262 assert_eq!(config_dir(), PathBuf::from("/etc/kanade"));
263 assert_eq!(data_dir(), PathBuf::from("/var/lib/kanade"));
264 assert_eq!(log_dir(), PathBuf::from("/var/log/kanade"));
265 }
266}