use std::path::PathBuf;
pub const ENV_CONFIG_DIR: &str = "CM_CRABMATE_CONFIG_DIR";
pub const ENV_CACHE_DIR: &str = "CM_CRABMATE_CACHE_DIR";
fn env_nonempty(key: &str) -> Option<PathBuf> {
std::env::var(key)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.map(PathBuf::from)
}
fn home_join(rel: &str) -> Option<PathBuf> {
std::env::var("HOME")
.ok()
.filter(|s| !s.trim().is_empty())
.map(|h| PathBuf::from(h).join(rel))
}
fn xdg_app_dir(
override_env: &str,
xdg_home_env: &str,
fallback_under_home: &str,
cwd_fallback: &str,
app: &str,
) -> PathBuf {
if let Some(p) = env_nonempty(override_env) {
return p;
}
let base = env_nonempty(xdg_home_env)
.or_else(|| home_join(fallback_under_home))
.unwrap_or_else(|| PathBuf::from(cwd_fallback));
base.join(app)
}
#[must_use]
pub fn user_config_dir() -> PathBuf {
xdg_app_dir(
ENV_CONFIG_DIR,
"XDG_CONFIG_HOME",
".config",
".config",
"crabmate",
)
}
#[must_use]
pub fn user_cache_dir() -> PathBuf {
xdg_app_dir(
ENV_CACHE_DIR,
"XDG_CACHE_HOME",
".cache",
".cache",
"crabmate",
)
}
pub fn ensure_user_cache_subdir(name: &str) -> Result<PathBuf, String> {
let name = name.trim().trim_matches('/');
if name.is_empty() || name.contains('/') || name.contains('\\') || name == ".." {
return Err(format!("非法缓存子目录名: {name:?}"));
}
let dir = user_cache_dir().join(name);
std::fs::create_dir_all(&dir)
.map_err(|e| format!("无法创建缓存目录 \"{}\": {e}", dir.display()))?;
Ok(dir)
}
pub fn ensure_fastembed_cache_dir() -> Result<PathBuf, String> {
ensure_user_cache_subdir("fastembed")
}
#[cfg(test)]
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
pub(crate) fn test_env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_subdir_rejects_path_segments() {
let _g = test_env_lock();
assert!(ensure_user_cache_subdir("../x").is_err());
assert!(ensure_user_cache_subdir("a/b").is_err());
assert!(ensure_user_cache_subdir("").is_err());
}
#[test]
fn cache_override_env_used() {
let _g = test_env_lock();
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path().join("cache-root");
unsafe {
std::env::set_var(ENV_CACHE_DIR, &root);
}
let got = user_cache_dir();
unsafe {
std::env::remove_var(ENV_CACHE_DIR);
}
assert_eq!(got, root);
}
}