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