use camino::Utf8PathBuf;
use sha2::{Digest, Sha256};
const DOCTRINE_CACHE_ENV: &str = "CORDANCE_DOCTRINE_CACHE_DIR";
const FALLBACK_CACHE_REL: &str = ".cordance-cache/doctrine";
#[must_use]
pub fn segment_matches_any_ascii_case_insensitive(segment: &str, names: &[&str]) -> bool {
names.iter().any(|name| segment.eq_ignore_ascii_case(name))
}
#[must_use]
pub fn segments_match_ascii_case_insensitive(left: &[&str], right: &[&str]) -> bool {
left.len() == right.len()
&& left
.iter()
.zip(right.iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
#[must_use]
pub fn doctrine_cache_root() -> Utf8PathBuf {
if let Ok(env_val) = std::env::var(DOCTRINE_CACHE_ENV) {
if !env_val.is_empty() {
return Utf8PathBuf::from(env_val);
}
}
if let Some(cache_dir) = dirs::cache_dir() {
let joined = cache_dir.join("cordance").join("doctrine");
if let Ok(utf8) = Utf8PathBuf::from_path_buf(joined) {
return utf8;
}
}
if let Some(home) = dirs::home_dir() {
let joined = home.join(FALLBACK_CACHE_REL);
if let Ok(utf8) = Utf8PathBuf::from_path_buf(joined) {
return utf8;
}
}
Utf8PathBuf::from(FALLBACK_CACHE_REL)
}
#[must_use]
pub fn doctrine_cache_dir_for_url(fallback_repo: &str) -> Utf8PathBuf {
let mut hasher = Sha256::new();
hasher.update(fallback_repo.as_bytes());
let digest = hasher.finalize();
let prefix = hex::encode(&digest[..8]);
doctrine_cache_root().join(prefix)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvOverrideGuard {
key: &'static str,
prev: Option<String>,
_guard: std::sync::MutexGuard<'static, ()>,
}
impl EnvOverrideGuard {
fn set(key: &'static str, value: &str) -> Self {
let guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev = std::env::var(key).ok();
std::env::set_var(key, value);
Self {
key,
prev,
_guard: guard,
}
}
}
impl Drop for EnvOverrideGuard {
fn drop(&mut self) {
match &self.prev {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
#[test]
fn doctrine_cache_root_honours_env_override() {
let override_path = "/tmp/cordance-doctrine-cache-test-override";
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, override_path);
let resolved = doctrine_cache_root();
assert_eq!(resolved, Utf8PathBuf::from(override_path));
}
#[test]
fn doctrine_cache_root_ignores_empty_env_override() {
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, "");
let resolved = doctrine_cache_root();
assert!(
!resolved.as_str().is_empty(),
"doctrine_cache_root must not return an empty path when env override is empty"
);
}
#[test]
fn doctrine_cache_dir_for_url_is_deterministic() {
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, "/tmp/cordance-determinism-test");
let url = "https://github.com/0ryant/engineering-doctrine";
let a = doctrine_cache_dir_for_url(url);
let b = doctrine_cache_dir_for_url(url);
assert_eq!(a, b, "same URL must produce the same cache dir");
}
#[test]
fn doctrine_cache_dir_for_url_distinguishes_urls() {
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, "/tmp/cordance-distinguish-test");
let a = doctrine_cache_dir_for_url("https://github.com/0ryant/engineering-doctrine");
let b = doctrine_cache_dir_for_url("https://github.com/0ryant/other-doctrine");
assert_ne!(a, b, "different URLs must produce different cache dirs");
}
#[test]
fn doctrine_cache_dir_for_url_is_under_root() {
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, "/tmp/cordance-paths-test");
let url = "https://github.com/0ryant/engineering-doctrine";
let root = doctrine_cache_root();
let leaf = doctrine_cache_dir_for_url(url);
assert!(
leaf.starts_with(&root),
"url-namespaced dir {leaf} must live under cache root {root}"
);
}
#[test]
fn doctrine_cache_root_returns_absolute_when_env_unset() {
let guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prev = std::env::var(DOCTRINE_CACHE_ENV).ok();
std::env::remove_var(DOCTRINE_CACHE_ENV);
let resolved = doctrine_cache_root();
if let Some(v) = prev {
std::env::set_var(DOCTRINE_CACHE_ENV, v);
}
drop(guard);
if dirs::cache_dir().is_some() || dirs::home_dir().is_some() {
assert!(
resolved.is_absolute(),
"doctrine_cache_root must be absolute when dirs::* resolves; got {resolved}"
);
}
}
#[test]
fn url_namespace_prefix_is_16_hex_chars() {
let _guard = EnvOverrideGuard::set(DOCTRINE_CACHE_ENV, "/tmp/cordance-paths-prefix-test");
let url = "https://example.com/repo";
let dir = doctrine_cache_dir_for_url(url);
let leaf = dir
.file_name()
.expect("namespaced dir must have a final component");
assert_eq!(leaf.len(), 16, "URL namespace prefix must be 16 hex chars");
assert!(
leaf.chars().all(|c| c.is_ascii_hexdigit()),
"URL namespace prefix must be ascii hex; got {leaf}"
);
}
#[test]
fn segment_policy_helpers_are_ascii_case_insensitive_and_segment_exact() {
let names = [".cordance", ".git", "target"];
assert!(segment_matches_any_ascii_case_insensitive(".GIT", &names));
assert!(segment_matches_any_ascii_case_insensitive(
".Cordance",
&names
));
assert!(segment_matches_any_ascii_case_insensitive("Target", &names));
assert!(!segment_matches_any_ascii_case_insensitive(
"myTarget", &names
));
assert!(!segment_matches_any_ascii_case_insensitive(
".Cordance-cache",
&names
));
assert!(segments_match_ascii_case_insensitive(
&[".CLAUDE", "Sessions"],
&[".claude", "sessions"],
));
assert!(!segments_match_ascii_case_insensitive(
&[".CLAUDE", "mySessions"],
&[".claude", "sessions"],
));
}
}