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> =
82 serde_json::from_str(&content).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).map_err(|e| TrustError::IoError(e.to_string()))?;
99 }
100
101 std::fs::write(&self.store_path, content)
102 .map_err(|e| TrustError::IoError(e.to_string()))?;
103
104 Ok(())
105 }
106
107 pub fn get_decision(&self, project_path: &Path) -> TrustDecision {
109 let normalized = normalize_path(project_path);
110 let decisions = self.decisions.read().unwrap_or_else(|e| e.into_inner());
111
112 decisions
113 .get(&normalized)
114 .map(|t| t.decision)
115 .unwrap_or(TrustDecision::Undecided)
116 }
117
118 pub fn set_decision(
120 &self,
121 project_path: &Path,
122 decision: TrustDecision,
123 decided_by: Option<String>,
124 ) -> Result<(), TrustError> {
125 let normalized = normalize_path(project_path);
126 let now = std::time::SystemTime::now()
127 .duration_since(std::time::UNIX_EPOCH)
128 .map(|d| d.as_millis() as i64)
129 .unwrap_or(0);
130
131 let trust = ProjectTrust {
132 path: normalized.clone(),
133 decision,
134 decided_at: Some(now),
135 decided_by,
136 };
137
138 let mut decisions = self.decisions.write().map_err(|_| TrustError::LockError)?;
139 decisions.insert(normalized, trust);
140
141 Ok(())
142 }
143
144 pub fn is_trusted(&self, project_path: &Path) -> bool {
146 self.get_decision(project_path) == TrustDecision::Trusted
147 }
148
149 pub fn remove_decision(&self, project_path: &Path) -> Result<(), TrustError> {
151 let normalized = normalize_path(project_path);
152 let mut decisions = self.decisions.write().map_err(|_| TrustError::LockError)?;
153 decisions.remove(&normalized);
154 Ok(())
155 }
156
157 pub fn all_decisions(&self) -> Vec<ProjectTrust> {
159 let decisions = self.decisions.read().unwrap_or_else(|e| e.into_inner());
160 decisions.values().cloned().collect()
161 }
162}
163
164fn normalize_path(path: &Path) -> String {
166 let absolute = if path.is_absolute() {
168 path.to_path_buf()
169 } else {
170 std::env::current_dir()
171 .unwrap_or_else(|_| PathBuf::from("."))
172 .join(path)
173 };
174
175 absolute
177 .to_string_lossy()
178 .replace('\\', "/")
179 .trim_end_matches('/')
180 .to_string()
181}
182
183#[derive(Debug, Clone)]
185pub enum TrustError {
186 IoError(String),
188 ParseError(String),
190 SerializeError(String),
192 LockError,
194}
195
196impl std::fmt::Display for TrustError {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 match self {
199 TrustError::IoError(e) => write!(f, "I/O error: {}", e),
200 TrustError::ParseError(e) => write!(f, "Parse error: {}", e),
201 TrustError::SerializeError(e) => write!(f, "Serialize error: {}", e),
202 TrustError::LockError => write!(f, "Lock error"),
203 }
204 }
205}
206
207impl std::error::Error for TrustError {}
208
209pub fn should_trust_project(
216 project_path: &Path,
217 trust_store: &TrustStore,
218 cli_approve: Option<bool>,
219) -> bool {
220 if let Some(approve) = cli_approve {
222 return approve;
223 }
224
225 match trust_store.get_decision(project_path) {
227 TrustDecision::Trusted => return true,
228 TrustDecision::NotTrusted => return false,
229 TrustDecision::Undecided => {}
230 }
231
232 if let Some(home) = dirs::home_dir() {
234 if project_path.starts_with(&home) {
235 return true;
236 }
237 }
238
239 false
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use tempfile::TempDir;
247
248 #[test]
249 fn test_trust_decision_default() {
250 assert_eq!(TrustDecision::default(), TrustDecision::Undecided);
251 }
252
253 #[test]
254 fn test_trust_store_new() {
255 let temp_dir = TempDir::new().unwrap();
256 let store_path = temp_dir.path().join("trust.json");
257 let store = TrustStore::new(store_path);
258
259 assert_eq!(store.all_decisions().len(), 0);
260 }
261
262 #[test]
263 fn test_trust_store_set_and_get() {
264 let temp_dir = TempDir::new().unwrap();
265 let store_path = temp_dir.path().join("trust.json");
266 let store = TrustStore::new(store_path);
267
268 let project_path = Path::new("/test/project");
269
270 assert_eq!(store.get_decision(project_path), TrustDecision::Undecided);
272
273 store
275 .set_decision(
276 project_path,
277 TrustDecision::Trusted,
278 Some("test".to_string()),
279 )
280 .unwrap();
281 assert_eq!(store.get_decision(project_path), TrustDecision::Trusted);
282 assert!(store.is_trusted(project_path));
283
284 store
286 .set_decision(project_path, TrustDecision::NotTrusted, None)
287 .unwrap();
288 assert_eq!(store.get_decision(project_path), TrustDecision::NotTrusted);
289 assert!(!store.is_trusted(project_path));
290 }
291
292 #[test]
293 fn test_trust_store_save_and_load() {
294 let temp_dir = TempDir::new().unwrap();
295 let store_path = temp_dir.path().join("trust.json");
296
297 let store1 = TrustStore::new(store_path.clone());
299 let project1 = Path::new("/test/project1");
300 let project2 = Path::new("/test/project2");
301
302 store1
303 .set_decision(project1, TrustDecision::Trusted, Some("user".to_string()))
304 .unwrap();
305 store1
306 .set_decision(project2, TrustDecision::NotTrusted, None)
307 .unwrap();
308 store1.save().unwrap();
309
310 let store2 = TrustStore::new(store_path);
312 store2.load().unwrap();
313
314 assert_eq!(store2.get_decision(project1), TrustDecision::Trusted);
315 assert_eq!(store2.get_decision(project2), TrustDecision::NotTrusted);
316 }
317
318 #[test]
319 fn test_normalize_path() {
320 let path1 = Path::new("/test/project");
321 let path2 = Path::new("/test/project/");
322
323 assert_eq!(normalize_path(path1), normalize_path(path2));
324 }
325
326 #[test]
327 fn test_should_trust_project_with_cli_flag() {
328 let temp_dir = TempDir::new().unwrap();
329 let store_path = temp_dir.path().join("trust.json");
330 let store = TrustStore::new(store_path);
331 let project_path = Path::new("/test/project");
332
333 assert!(should_trust_project(project_path, &store, Some(true)));
335 assert!(!should_trust_project(project_path, &store, Some(false)));
336 }
337
338 #[test]
339 fn test_should_trust_project_with_store() {
340 let temp_dir = TempDir::new().unwrap();
341 let store_path = temp_dir.path().join("trust.json");
342 let store = TrustStore::new(store_path);
343 let project_path = Path::new("/test/project");
344
345 store
347 .set_decision(project_path, TrustDecision::Trusted, None)
348 .unwrap();
349 assert!(should_trust_project(project_path, &store, None));
350
351 store
352 .set_decision(project_path, TrustDecision::NotTrusted, None)
353 .unwrap();
354 assert!(!should_trust_project(project_path, &store, None));
355 }
356}