use std::collections::BTreeSet;
use std::path::Path;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
static DIR_OVERRIDE: Mutex<Option<std::path::PathBuf>> = Mutex::new(None);
#[cfg(test)]
fn set_dir_override(p: Option<std::path::PathBuf>) {
*DIR_OVERRIDE.lock().unwrap() = p;
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct TrustStore {
pub trusted: BTreeSet<String>,
}
impl TrustStore {
fn path() -> std::path::PathBuf {
if let Some(p) = DIR_OVERRIDE.lock().unwrap().clone() {
return p.join("trust.json");
}
dirs::config_dir()
.unwrap_or_else(std::env::temp_dir)
.join("hey")
.join("trust.json")
}
pub fn load() -> Self {
std::fs::read_to_string(Self::path())
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
fn normalize(root: &Path) -> String {
let canon = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
canon.to_string_lossy().trim_end_matches('/').to_string()
}
pub fn is_trusted(&self, root: &Path) -> bool {
self.trusted.contains(&Self::normalize(root))
}
pub fn trust(&mut self, root: &Path) {
self.trusted.insert(Self::normalize(root));
if let Some(parent) = Self::path().parent() {
let _ = std::fs::create_dir_all(parent);
}
let _ = std::fs::write(
Self::path(),
serde_json::to_string_pretty(self).unwrap_or_default(),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
static TEST_LOCK: Mutex<()> = Mutex::new(());
fn with_tmp<T>(f: impl FnOnce(std::path::PathBuf) -> T) -> T {
let dir = std::env::temp_dir().join(format!("hey-trust-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
let r = f(dir.clone());
let _ = std::fs::remove_dir_all(&dir);
r
}
#[test]
fn load_trust_roundtrip_via_override() {
let _guard = TEST_LOCK.lock().unwrap();
let dir = std::env::temp_dir().join(format!("hey-trust-real-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
set_dir_override(Some(dir.clone()));
let proj = dir.join("some-project");
std::fs::create_dir_all(&proj).unwrap();
let mut s = TrustStore::load();
assert!(!s.is_trusted(&proj));
s.trust(&proj);
let reloaded = TrustStore::load();
assert!(reloaded.is_trusted(&proj), "trust() 持久化后可 load 命中");
assert!(reloaded.is_trusted(&proj.canonicalize().unwrap()));
assert!(!reloaded.is_trusted(&dir));
let _ = std::fs::remove_dir_all(&dir);
set_dir_override(None);
}
#[test]
fn corrupted_trust_file_loads_empty() {
let _guard = TEST_LOCK.lock().unwrap();
let dir = std::env::temp_dir().join(format!("hey-trust-bad-{}", std::process::id()));
let _ = std::fs::create_dir_all(&dir);
set_dir_override(Some(dir.clone()));
std::fs::write(dir.join("trust.json"), "NOT JSON").unwrap();
let s = TrustStore::load();
assert!(s.trusted.is_empty(), "损坏文件 → 空存储");
let _ = std::fs::remove_dir_all(&dir);
set_dir_override(None);
}
#[test]
fn trust_roundtrip_via_file() {
with_tmp(|dir| {
let store_path = dir.join("trust.json");
let proj = dir.join("some-project");
std::fs::create_dir_all(&proj).unwrap();
let proj_canon = proj.canonicalize().unwrap();
let mut s = TrustStore::default();
s.trusted.insert(TrustStore::normalize(&proj_canon));
std::fs::write(&store_path, serde_json::to_string(&s).unwrap()).unwrap();
let loaded: TrustStore =
serde_json::from_str(&std::fs::read_to_string(&store_path).unwrap()).unwrap();
assert!(loaded.is_trusted(&proj_canon));
assert!(loaded.is_trusted(&proj), "规范化后相同路径命中");
assert!(!loaded.is_trusted(&dir));
});
}
#[test]
fn trust_marks_and_persists() {
with_tmp(|dir| {
let proj = dir.join("p2");
std::fs::create_dir_all(&proj).unwrap();
let mut s = TrustStore::default();
s.trusted.insert(TrustStore::normalize(&proj));
assert!(s.is_trusted(&proj));
let json = serde_json::to_string(&s).unwrap();
let again: TrustStore = serde_json::from_str(&json).unwrap();
assert!(again.is_trusted(&proj));
assert!(json.contains("p2"), "路径写入: {json}");
});
}
}