1use serde::{Deserialize, Serialize};
19use std::collections::HashMap;
20use std::path::{Path, PathBuf};
21use std::sync::{Arc, RwLock};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "lowercase")]
26pub enum TrustDecision {
27 Trusted,
29 NotTrusted,
31 Undecided,
33}
34
35impl Default for TrustDecision {
36 fn default() -> Self {
37 TrustDecision::Undecided
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProjectTrust {
44 pub path: String,
46 pub decision: TrustDecision,
48 pub decided_at: Option<i64>,
50 pub decided_by: Option<String>,
52}
53
54#[derive(Debug, Clone)]
56pub struct TrustStore {
57 decisions: Arc<RwLock<HashMap<String, ProjectTrust>>>,
59 store_path: PathBuf,
61}
62
63impl TrustStore {
64 pub fn new(store_path: PathBuf) -> Self {
66 Self {
67 decisions: Arc::new(RwLock::new(HashMap::new())),
68 store_path,
69 }
70 }
71
72 pub fn load(&self) -> Result<(), TrustError> {
74 if !self.store_path.exists() {
75 return Ok(());
76 }
77
78 let content = std::fs::read_to_string(&self.store_path)
79 .map_err(|e| TrustError::IoError(e.to_string()))?;
80
81 let decisions: HashMap<String, ProjectTrust> = serde_json::from_str(&content)
82 .map_err(|e| TrustError::ParseError(e.to_string()))?;
83
84 let mut store = self.decisions.write().map_err(|_| TrustError::LockError)?;
85 *store = decisions;
86
87 Ok(())
88 }
89
90 pub fn save(&self) -> Result<(), TrustError> {
92 let decisions = self.decisions.read().map_err(|_| TrustError::LockError)?;
93 let content = serde_json::to_string_pretty(&*decisions)
94 .map_err(|e| TrustError::SerializeError(e.to_string()))?;
95
96 if let Some(parent) = self.store_path.parent() {
98 std::fs::create_dir_all(parent)
99 .map_err(|e| TrustError::IoError(e.to_string()))?;
100 }
101
102 std::fs::write(&self.store_path, content)
103 .map_err(|e| TrustError::IoError(e.to_string()))?;
104
105 Ok(())
106 }
107
108 pub fn get_decision(&self, project_path: &Path) -> TrustDecision {
110 let normalized = normalize_path(project_path);
111 let decisions = self.decisions.read().unwrap_or_else(|e| e.into_inner());
112
113 decisions
114 .get(&normalized)
115 .map(|t| t.decision)
116 .unwrap_or(TrustDecision::Undecided)
117 }
118
119 pub fn set_decision(
121 &self,
122 project_path: &Path,
123 decision: TrustDecision,
124 decided_by: Option<String>,
125 ) -> Result<(), TrustError> {
126 let normalized = normalize_path(project_path);
127 let now = std::time::SystemTime::now()
128 .duration_since(std::time::UNIX_EPOCH)
129 .map(|d| d.as_millis() as i64)
130 .unwrap_or(0);
131
132 let trust = ProjectTrust {
133 path: normalized.clone(),
134 decision,
135 decided_at: Some(now),
136 decided_by,
137 };
138
139 let mut decisions = self.decisions.write().map_err(|_| TrustError::LockError)?;
140 decisions.insert(normalized, trust);
141
142 Ok(())
143 }
144
145 pub fn is_trusted(&self, project_path: &Path) -> bool {
147 self.get_decision(project_path) == TrustDecision::Trusted
148 }
149
150 pub fn remove_decision(&self, project_path: &Path) -> Result<(), TrustError> {
152 let normalized = normalize_path(project_path);
153 let mut decisions = self.decisions.write().map_err(|_| TrustError::LockError)?;
154 decisions.remove(&normalized);
155 Ok(())
156 }
157
158 pub fn all_decisions(&self) -> Vec<ProjectTrust> {
160 let decisions = self.decisions.read().unwrap_or_else(|e| e.into_inner());
161 decisions.values().cloned().collect()
162 }
163}
164
165fn normalize_path(path: &Path) -> String {
167 let absolute = if path.is_absolute() {
169 path.to_path_buf()
170 } else {
171 std::env::current_dir()
172 .unwrap_or_else(|_| PathBuf::from("."))
173 .join(path)
174 };
175
176 absolute
178 .to_string_lossy()
179 .replace('\\', "/")
180 .trim_end_matches('/')
181 .to_string()
182}
183
184#[derive(Debug, Clone)]
186pub enum TrustError {
187 IoError(String),
189 ParseError(String),
191 SerializeError(String),
193 LockError,
195}
196
197impl std::fmt::Display for TrustError {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 match self {
200 TrustError::IoError(e) => write!(f, "I/O error: {}", e),
201 TrustError::ParseError(e) => write!(f, "Parse error: {}", e),
202 TrustError::SerializeError(e) => write!(f, "Serialize error: {}", e),
203 TrustError::LockError => write!(f, "Lock error"),
204 }
205 }
206}
207
208impl std::error::Error for TrustError {}
209
210pub fn should_trust_project(
217 project_path: &Path,
218 trust_store: &TrustStore,
219 cli_approve: Option<bool>,
220) -> bool {
221 if let Some(approve) = cli_approve {
223 return approve;
224 }
225
226 match trust_store.get_decision(project_path) {
228 TrustDecision::Trusted => return true,
229 TrustDecision::NotTrusted => return false,
230 TrustDecision::Undecided => {}
231 }
232
233 if let Some(home) = dirs::home_dir() {
235 if project_path.starts_with(&home) {
236 return true;
237 }
238 }
239
240 false
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use tempfile::TempDir;
248
249 #[test]
250 fn test_trust_decision_default() {
251 assert_eq!(TrustDecision::default(), TrustDecision::Undecided);
252 }
253
254 #[test]
255 fn test_trust_store_new() {
256 let temp_dir = TempDir::new().unwrap();
257 let store_path = temp_dir.path().join("trust.json");
258 let store = TrustStore::new(store_path);
259
260 assert_eq!(store.all_decisions().len(), 0);
261 }
262
263 #[test]
264 fn test_trust_store_set_and_get() {
265 let temp_dir = TempDir::new().unwrap();
266 let store_path = temp_dir.path().join("trust.json");
267 let store = TrustStore::new(store_path);
268
269 let project_path = Path::new("/test/project");
270
271 assert_eq!(store.get_decision(project_path), TrustDecision::Undecided);
273
274 store.set_decision(project_path, TrustDecision::Trusted, Some("test".to_string())).unwrap();
276 assert_eq!(store.get_decision(project_path), TrustDecision::Trusted);
277 assert!(store.is_trusted(project_path));
278
279 store.set_decision(project_path, TrustDecision::NotTrusted, None).unwrap();
281 assert_eq!(store.get_decision(project_path), TrustDecision::NotTrusted);
282 assert!(!store.is_trusted(project_path));
283 }
284
285 #[test]
286 fn test_trust_store_save_and_load() {
287 let temp_dir = TempDir::new().unwrap();
288 let store_path = temp_dir.path().join("trust.json");
289
290 let store1 = TrustStore::new(store_path.clone());
292 let project1 = Path::new("/test/project1");
293 let project2 = Path::new("/test/project2");
294
295 store1.set_decision(project1, TrustDecision::Trusted, Some("user".to_string())).unwrap();
296 store1.set_decision(project2, TrustDecision::NotTrusted, None).unwrap();
297 store1.save().unwrap();
298
299 let store2 = TrustStore::new(store_path);
301 store2.load().unwrap();
302
303 assert_eq!(store2.get_decision(project1), TrustDecision::Trusted);
304 assert_eq!(store2.get_decision(project2), TrustDecision::NotTrusted);
305 }
306
307 #[test]
308 fn test_normalize_path() {
309 let path1 = Path::new("/test/project");
310 let path2 = Path::new("/test/project/");
311
312 assert_eq!(normalize_path(path1), normalize_path(path2));
313 }
314
315 #[test]
316 fn test_should_trust_project_with_cli_flag() {
317 let temp_dir = TempDir::new().unwrap();
318 let store_path = temp_dir.path().join("trust.json");
319 let store = TrustStore::new(store_path);
320 let project_path = Path::new("/test/project");
321
322 assert!(should_trust_project(project_path, &store, Some(true)));
324 assert!(!should_trust_project(project_path, &store, Some(false)));
325 }
326
327 #[test]
328 fn test_should_trust_project_with_store() {
329 let temp_dir = TempDir::new().unwrap();
330 let store_path = temp_dir.path().join("trust.json");
331 let store = TrustStore::new(store_path);
332 let project_path = Path::new("/test/project");
333
334 store.set_decision(project_path, TrustDecision::Trusted, None).unwrap();
336 assert!(should_trust_project(project_path, &store, None));
337
338 store.set_decision(project_path, TrustDecision::NotTrusted, None).unwrap();
339 assert!(!should_trust_project(project_path, &store, None));
340 }
341}