1use std::path::{Path, PathBuf};
32
33use super::data_dir::{ensure_dir_permissions, has_data_files, lean_ctx_data_dir};
34
35fn env_path(name: &str) -> Option<PathBuf> {
37 std::env::var(name)
38 .ok()
39 .map(|v| v.trim().to_string())
40 .filter(|v| !v.is_empty())
41 .map(PathBuf::from)
42}
43
44fn xdg_base(env_name: &str, home_fallback: &str) -> Result<PathBuf, String> {
48 if let Some(p) = env_path(env_name) {
49 return Ok(p);
50 }
51 dirs::home_dir()
52 .map(|h| h.join(home_fallback))
53 .ok_or_else(|| "Cannot determine home directory".to_string())
54}
55
56fn resolve(
59 category_override: Option<PathBuf>,
60 single: Option<PathBuf>,
61 xdg_base_dir: &Path,
62) -> PathBuf {
63 category_override
64 .or(single)
65 .unwrap_or_else(|| xdg_base_dir.join("lean-ctx"))
66}
67
68pub(crate) fn single_dir_override() -> Option<PathBuf> {
75 if let Some(p) = env_path("LEAN_CTX_DATA_DIR") {
76 return Some(p);
77 }
78 let home = dirs::home_dir()?;
79 let xdg_config_base = xdg_base("XDG_CONFIG_HOME", ".config").ok()?;
80 single_dir_override_fs(&home, &xdg_config_base)
81}
82
83fn single_dir_override_fs(home: &Path, xdg_config_base: &Path) -> Option<PathBuf> {
85 if crate::core::layout_pin::is_xdg_pinned_in(xdg_config_base) {
89 return None;
90 }
91 let legacy = home.join(".lean-ctx");
92 if legacy.exists() && has_data_files(&legacy) {
93 return Some(legacy);
94 }
95 let mixed = xdg_config_base.join("lean-ctx");
96 if mixed.exists() && has_data_files(&mixed) {
97 return Some(mixed);
98 }
99 None
100}
101
102#[cfg_attr(test, allow(clippy::unnecessary_wraps))]
106fn category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
107 let category_override = env_path(cat_env);
108
109 #[cfg(test)]
112 {
113 if let Some(p) = category_override {
114 ensure_dir_permissions(&p);
115 return Ok(p);
116 }
117 let _ = (xdg_env, home_fallback);
121 Ok(super::data_dir::test_sandbox_dir())
122 }
123 #[cfg(not(test))]
124 {
125 let base = xdg_base(xdg_env, home_fallback)?;
126 let dir = resolve(category_override, single_dir_override(), &base);
127 ensure_dir_permissions(&dir);
128 Ok(dir)
129 }
130}
131
132pub fn config_dir() -> Result<PathBuf, String> {
135 category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
136}
137
138pub fn data_dir() -> Result<PathBuf, String> {
145 lean_ctx_data_dir()
146}
147
148pub fn state_dir() -> Result<PathBuf, String> {
151 category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
152}
153
154pub fn cache_dir() -> Result<PathBuf, String> {
157 category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
158}
159
160pub fn runtime_dir() -> Result<PathBuf, String> {
166 if let Some(base) = env_path("XDG_RUNTIME_DIR") {
167 return Ok(base.join("lean-ctx"));
168 }
169 state_dir()
170}
171
172fn raw_category_dir(cat_env: &str, xdg_env: &str, home_fallback: &str) -> Result<PathBuf, String> {
180 if let Some(p) = env_path(cat_env) {
181 return Ok(p);
182 }
183 Ok(xdg_base(xdg_env, home_fallback)?.join("lean-ctx"))
184}
185
186pub(crate) fn config_split_target() -> Result<PathBuf, String> {
188 raw_category_dir("LEAN_CTX_CONFIG_DIR", "XDG_CONFIG_HOME", ".config")
189}
190
191pub(crate) fn xdg_config_lean_ctx_dir() -> Option<PathBuf> {
196 xdg_base("XDG_CONFIG_HOME", ".config")
197 .ok()
198 .map(|b| b.join("lean-ctx"))
199}
200
201pub(crate) fn data_split_target() -> Result<PathBuf, String> {
203 raw_category_dir("LEAN_CTX_DATA_DIR", "XDG_DATA_HOME", ".local/share")
204}
205
206pub(crate) fn state_split_target() -> Result<PathBuf, String> {
208 raw_category_dir("LEAN_CTX_STATE_DIR", "XDG_STATE_HOME", ".local/state")
209}
210
211pub(crate) fn cache_split_target() -> Result<PathBuf, String> {
213 raw_category_dir("LEAN_CTX_CACHE_DIR", "XDG_CACHE_HOME", ".cache")
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn resolve_prefers_override_then_single_then_xdg() {
222 let over = PathBuf::from("/over/ride");
223 let single = PathBuf::from("/single/dir");
224 let base = PathBuf::from("/xdg/base");
225
226 assert_eq!(
227 resolve(Some(over.clone()), Some(single.clone()), &base),
228 over
229 );
230 assert_eq!(resolve(None, Some(single.clone()), &base), single);
231 assert_eq!(
232 resolve(None, None, &base),
233 PathBuf::from("/xdg/base/lean-ctx")
234 );
235 }
236
237 #[test]
238 fn single_dir_fs_detects_legacy_with_data() {
239 let home = tempfile::tempdir().unwrap();
240 let xdg = tempfile::tempdir().unwrap();
241 let legacy = home.path().join(".lean-ctx");
242 std::fs::create_dir_all(&legacy).unwrap();
243 std::fs::write(legacy.join("stats.json"), "{}").unwrap();
244
245 assert_eq!(
246 single_dir_override_fs(home.path(), xdg.path()),
247 Some(legacy)
248 );
249 }
250
251 #[test]
252 fn single_dir_fs_detects_mixed_with_data() {
253 let home = tempfile::tempdir().unwrap();
254 let xdg = tempfile::tempdir().unwrap();
255 let mixed = xdg.path().join("lean-ctx");
256 std::fs::create_dir_all(&mixed).unwrap();
257 std::fs::write(mixed.join("stats.json"), "{}").unwrap();
260
261 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), Some(mixed));
262 }
263
264 #[test]
265 fn single_dir_fs_ignores_config_only_dir() {
266 let home = tempfile::tempdir().unwrap();
269 let xdg = tempfile::tempdir().unwrap();
270 let mixed = xdg.path().join("lean-ctx");
271 std::fs::create_dir_all(&mixed).unwrap();
272 std::fs::write(mixed.join("config.toml"), "").unwrap();
273 std::fs::write(mixed.join("shell-hook.zsh"), "").unwrap();
274
275 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
276 }
277
278 #[test]
279 fn single_dir_fs_prefers_legacy_over_mixed() {
280 let home = tempfile::tempdir().unwrap();
281 let xdg = tempfile::tempdir().unwrap();
282 let legacy = home.path().join(".lean-ctx");
283 std::fs::create_dir_all(&legacy).unwrap();
284 std::fs::write(legacy.join("sessions"), "x").unwrap();
285 let mixed = xdg.path().join("lean-ctx");
286 std::fs::create_dir_all(&mixed).unwrap();
287 std::fs::write(mixed.join("stats.json"), "{}").unwrap();
288
289 assert_eq!(
290 single_dir_override_fs(home.path(), xdg.path()),
291 Some(legacy)
292 );
293 }
294
295 #[test]
296 fn xdg_pinned_install_ignores_stray_legacy_marker() {
297 let home = tempfile::tempdir().unwrap();
301 let xdg = tempfile::tempdir().unwrap();
302 crate::core::layout_pin::write_xdg_pin_in(xdg.path()).unwrap();
303
304 let legacy = home.path().join(".lean-ctx");
305 std::fs::create_dir_all(&legacy).unwrap();
306 std::fs::write(legacy.join("stats.json"), "{}").unwrap();
307
308 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
309 }
310
311 #[test]
312 fn xdg_pinned_install_ignores_stray_mixed_marker() {
313 let home = tempfile::tempdir().unwrap();
316 let xdg = tempfile::tempdir().unwrap();
317 crate::core::layout_pin::write_xdg_pin_in(xdg.path()).unwrap();
318
319 let mixed = xdg.path().join("lean-ctx");
320 std::fs::write(mixed.join("stats.json"), "{}").unwrap();
321
322 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
323 }
324
325 #[test]
326 fn single_dir_fs_ignores_empty_dirs() {
327 let home = tempfile::tempdir().unwrap();
328 let xdg = tempfile::tempdir().unwrap();
329 std::fs::create_dir_all(home.path().join(".lean-ctx")).unwrap();
330 std::fs::create_dir_all(xdg.path().join("lean-ctx")).unwrap();
331
332 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
333 }
334
335 #[test]
336 fn single_dir_fs_ignores_non_marker_files() {
337 let home = tempfile::tempdir().unwrap();
338 let xdg = tempfile::tempdir().unwrap();
339 let mixed = xdg.path().join("lean-ctx");
340 std::fs::create_dir_all(&mixed).unwrap();
341 std::fs::write(mixed.join("random.txt"), "x").unwrap();
342
343 assert_eq!(single_dir_override_fs(home.path(), xdg.path()), None);
344 }
345
346 #[test]
347 fn xdg_base_honors_env_then_home_fallback() {
348 let _lock = crate::core::data_dir::test_env_lock();
349 let tmp = tempfile::tempdir().unwrap();
350 crate::test_env::set_var("XDG_CONFIG_HOME", tmp.path());
351 let from_env = xdg_base("XDG_CONFIG_HOME", ".config").unwrap();
352 crate::test_env::remove_var("XDG_CONFIG_HOME");
353 assert_eq!(from_env, tmp.path());
354
355 let fallback = xdg_base("LEAN_CTX_NONEXISTENT_XDG_VAR", ".cache").unwrap();
357 assert!(fallback.ends_with(".cache"), "got: {}", fallback.display());
358 }
359
360 #[test]
361 fn single_dir_override_honors_data_dir_env() {
362 let _lock = crate::core::data_dir::test_env_lock();
363 let tmp = tempfile::tempdir().unwrap();
364 crate::test_env::set_var("LEAN_CTX_DATA_DIR", tmp.path());
365 let got = single_dir_override();
366 crate::test_env::remove_var("LEAN_CTX_DATA_DIR");
367 assert_eq!(got, Some(tmp.path().to_path_buf()));
368 }
369
370 #[test]
371 fn config_dir_honors_explicit_override() {
372 let _lock = crate::core::data_dir::test_env_lock();
373 let tmp = tempfile::tempdir().unwrap();
374 crate::test_env::set_var("LEAN_CTX_CONFIG_DIR", tmp.path());
375 let got = config_dir().unwrap();
376 crate::test_env::remove_var("LEAN_CTX_CONFIG_DIR");
377 assert_eq!(got, tmp.path());
378 }
379
380 #[test]
381 fn state_and_cache_dirs_honor_explicit_overrides() {
382 let _lock = crate::core::data_dir::test_env_lock();
383 let state = tempfile::tempdir().unwrap();
384 let cache = tempfile::tempdir().unwrap();
385 crate::test_env::set_var("LEAN_CTX_STATE_DIR", state.path());
386 crate::test_env::set_var("LEAN_CTX_CACHE_DIR", cache.path());
387 let got_state = state_dir().unwrap();
388 let got_cache = cache_dir().unwrap();
389 crate::test_env::remove_var("LEAN_CTX_STATE_DIR");
390 crate::test_env::remove_var("LEAN_CTX_CACHE_DIR");
391 assert_eq!(got_state, state.path());
392 assert_eq!(got_cache, cache.path());
393 }
394
395 #[test]
396 fn data_dir_matches_lean_ctx_data_dir() {
397 let _guard = crate::core::data_dir::isolated_data_dir();
398 assert_eq!(
399 data_dir().unwrap(),
400 crate::core::data_dir::lean_ctx_data_dir().unwrap()
401 );
402 }
403
404 #[test]
405 fn runtime_dir_honors_xdg_runtime_dir() {
406 let _lock = crate::core::data_dir::test_env_lock();
407 let tmp = tempfile::tempdir().unwrap();
408 crate::test_env::set_var("XDG_RUNTIME_DIR", tmp.path());
409 let got = runtime_dir().unwrap();
410 crate::test_env::remove_var("XDG_RUNTIME_DIR");
411 assert_eq!(got, tmp.path().join("lean-ctx"));
412 }
413}