1use hashbrown::HashMap;
2use std::borrow::Borrow;
10use std::hash::Hash;
11use std::path::PathBuf;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum PermissionGrant {
16 Once,
18 Session,
20 Permanent,
22 Denied,
24 TemporaryDenial,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct PermissionCacheStats {
32 pub cached_entries: usize,
33 pub hits: usize,
34 pub misses: usize,
35 pub total_requests: usize,
36 pub hit_rate: f64,
37}
38
39impl PermissionCacheStats {
40 #[inline]
41 fn compute(entries: usize, hits: usize, misses: usize) -> Self {
42 let total_requests = hits + misses;
43 let hit_rate = if total_requests > 0 {
44 (hits as f64) / (total_requests as f64)
45 } else {
46 0.0
47 };
48 Self {
49 cached_entries: entries,
50 hits,
51 misses,
52 total_requests,
53 hit_rate,
54 }
55 }
56}
57
58#[derive(Debug)]
61pub struct PermissionCache<K: Eq + Hash> {
62 grants: HashMap<K, PermissionGrant>,
63 hits: usize,
64 misses: usize,
65}
66
67impl<K: Eq + Hash> PermissionCache<K> {
68 #[inline]
70 pub fn new() -> Self {
71 Self { grants: HashMap::new(), hits: 0, misses: 0 }
72 }
73
74 #[inline]
76 pub fn get_permission<Q>(&mut self, key: &Q) -> Option<PermissionGrant>
77 where
78 K: Borrow<Q>,
79 Q: Hash + Eq + ?Sized,
80 {
81 if let Some(grant) = self.grants.get(key) {
82 self.hits += 1;
83 Some(*grant)
84 } else {
85 self.misses += 1;
86 None
87 }
88 }
89
90 #[inline]
92 pub fn cache_grant(&mut self, key: K, grant: PermissionGrant) {
93 self.grants.insert(key, grant);
94 }
95
96 #[inline]
98 pub fn invalidate<Q>(&mut self, key: &Q)
99 where
100 K: Borrow<Q>,
101 Q: Hash + Eq + ?Sized,
102 {
103 self.grants.remove(key);
104 }
105
106 pub fn clear_temporary_denials(&mut self) {
108 self.grants.retain(|_, grant| *grant != PermissionGrant::TemporaryDenial);
109 }
110
111 pub fn clear(&mut self) {
113 self.grants.clear();
114 self.hits = 0;
115 self.misses = 0;
116 }
117
118 #[inline]
120 pub fn stats(&self) -> PermissionCacheStats {
121 PermissionCacheStats::compute(self.grants.len(), self.hits, self.misses)
122 }
123
124 #[inline]
126 pub fn is_denied<Q>(&self, key: &Q) -> bool
127 where
128 K: Borrow<Q>,
129 Q: Hash + Eq + ?Sized,
130 {
131 matches!(self.grants.get(key), Some(PermissionGrant::Denied))
132 }
133
134 #[inline]
136 pub fn is_temporarily_denied<Q>(&self, key: &Q) -> bool
137 where
138 K: Borrow<Q>,
139 Q: Hash + Eq + ?Sized,
140 {
141 matches!(self.grants.get(key), Some(PermissionGrant::TemporaryDenial))
142 }
143
144 #[inline]
150 pub fn can_use_cached<Q>(&self, key: &Q) -> bool
151 where
152 K: Borrow<Q>,
153 Q: Hash + Eq + ?Sized,
154 {
155 matches!(
156 self.grants.get(key),
157 Some(
158 PermissionGrant::Once | PermissionGrant::Session | PermissionGrant::Permanent | PermissionGrant::Denied
159 )
160 )
161 }
162}
163
164impl<K: Eq + Hash> Default for PermissionCache<K> {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170pub type AcpPermissionCache = PermissionCache<PathBuf>;
173
174pub type ToolPermissionCache = PermissionCache<String>;
176
177pub type ToolPermissionCacheStats = PermissionCacheStats;
179
180impl ToolPermissionCache {
182 #[inline]
184 pub fn cache_grant_tool(&mut self, tool_name: impl Into<String>, grant: PermissionGrant) {
185 self.cache_grant(tool_name.into(), grant);
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn test_path(name: &str) -> PathBuf {
194 PathBuf::from(format!("/workspace/{name}"))
195 }
196
197 #[test]
198 fn test_creates_empty_cache() {
199 let cache = AcpPermissionCache::new();
200 let stats = cache.stats();
201 assert_eq!(stats.cached_entries, 0);
202 assert_eq!(stats.hits, 0);
203 assert_eq!(stats.misses, 0);
204 }
205
206 #[test]
207 fn test_caches_permission_grant() {
208 let mut cache = AcpPermissionCache::new();
209 let path = test_path("file.rs");
210
211 cache.cache_grant(path.clone(), PermissionGrant::Session);
213 assert_eq!(cache.get_permission(&path), Some(PermissionGrant::Session));
214 }
215
216 #[test]
217 fn test_tracks_hits_and_misses() {
218 let mut cache = AcpPermissionCache::new();
219 let path = test_path("file.rs");
220
221 cache.cache_grant(path.clone(), PermissionGrant::Session);
222
223 let _ = cache.get_permission(&path);
225 assert_eq!(cache.stats().hits, 1);
226
227 let _ = cache.get_permission(&test_path("other.rs"));
229 assert_eq!(cache.stats().misses, 1);
230 }
231
232 #[test]
233 fn test_calculates_hit_rate() {
234 let mut cache = AcpPermissionCache::new();
235 let path1 = test_path("file1.rs");
236 let path2 = test_path("file2.rs");
237
238 let path1_for_cache = path1.clone();
239 cache.cache_grant(path1_for_cache, PermissionGrant::Session);
240
241 cache.get_permission(&path1);
243 cache.get_permission(&path1);
244 cache.get_permission(&path1);
245
246 cache.get_permission(&path2);
248
249 let stats = cache.stats();
250 assert_eq!(stats.hits, 3);
251 assert_eq!(stats.misses, 1);
252 assert_eq!(stats.total_requests, 4);
253 assert!((stats.hit_rate - 0.75).abs() < 0.001);
254 }
255
256 #[test]
257 fn test_invalidates_path() {
258 let mut cache = AcpPermissionCache::new();
259 let path = test_path("file.rs");
260
261 cache.cache_grant(path.clone(), PermissionGrant::Session);
262 assert!(cache.get_permission(&path).is_some());
263
264 cache.invalidate(&path);
265 assert!(cache.get_permission(&path).is_none());
266 }
267
268 #[test]
269 fn test_clears_all() {
270 let mut cache = AcpPermissionCache::new();
271
272 cache.cache_grant(test_path("file1.rs"), PermissionGrant::Session);
273 cache.cache_grant(test_path("file2.rs"), PermissionGrant::Session);
274 cache.get_permission(&test_path("file1.rs"));
275
276 cache.clear();
277 let stats = cache.stats();
278 assert_eq!(stats.cached_entries, 0);
279 assert_eq!(stats.hits, 0);
280 assert_eq!(stats.misses, 0);
281 }
282
283 #[test]
284 fn test_identifies_denied_paths() {
285 let mut cache = AcpPermissionCache::new();
286 let denied_path = test_path("secret.txt");
287 let allowed_path = test_path("public.txt");
288
289 let denied_for_cache = denied_path.clone();
290 let allowed_for_cache = allowed_path.clone();
291 cache.cache_grant(denied_for_cache, PermissionGrant::Denied);
292 cache.cache_grant(allowed_for_cache, PermissionGrant::Session);
293
294 assert!(cache.is_denied(&denied_path));
295 assert!(!cache.is_denied(&allowed_path));
296 assert!(!cache.is_denied(&test_path("unknown.txt")));
297 }
298
299 #[test]
300 fn test_can_use_cached_for_session_grants() {
301 let mut cache = AcpPermissionCache::new();
302 let once_path = test_path("once.rs");
303 let session_path = test_path("session.rs");
304 let denied_path = test_path("denied.rs");
305 let temp_denied_path = test_path("temp_denied.rs");
306
307 cache.cache_grant(once_path.clone(), PermissionGrant::Once);
308 cache.cache_grant(session_path.clone(), PermissionGrant::Session);
309 cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
310 cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
311
312 assert!(cache.can_use_cached(&once_path));
314 assert!(!cache.can_use_cached(&temp_denied_path));
316
317 assert!(cache.can_use_cached(&session_path));
319 assert!(cache.can_use_cached(&denied_path));
320 }
321
322 #[test]
323 fn test_multiple_paths() {
324 let mut cache = AcpPermissionCache::new();
325
326 for i in 0..5 {
327 cache.cache_grant(test_path(&format!("file{i}.rs")), PermissionGrant::Session);
328 }
329
330 assert_eq!(cache.stats().cached_entries, 5);
331
332 for i in 0..5 {
333 let grant = cache.get_permission(&test_path(&format!("file{i}.rs")));
334 assert_eq!(grant, Some(PermissionGrant::Session));
335 }
336
337 assert_eq!(cache.stats().hits, 5);
338 }
339
340 #[test]
341 fn test_distinguishes_denied_from_temporary_denial() {
342 let mut cache = AcpPermissionCache::new();
343 let denied_path = test_path("denied.rs");
344 let temp_denied_path = test_path("temp_denied.rs");
345
346 cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
347 cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
348
349 assert!(cache.is_denied(&denied_path));
351 assert!(!cache.is_denied(&temp_denied_path));
352
353 assert!(!cache.is_temporarily_denied(&denied_path));
354 assert!(cache.is_temporarily_denied(&temp_denied_path));
355 }
356
357 #[test]
358 fn test_clear_temporary_denials_preserves_policy_denials() {
359 let mut cache = AcpPermissionCache::new();
360 let policy_denied = test_path("policy_denied.rs");
361 let temp_denied = test_path("temp_denied.rs");
362 let allowed = test_path("allowed.rs");
363
364 cache.cache_grant(policy_denied.clone(), PermissionGrant::Denied);
365 cache.cache_grant(temp_denied.clone(), PermissionGrant::TemporaryDenial);
366 cache.cache_grant(allowed.clone(), PermissionGrant::Session);
367
368 cache.clear_temporary_denials();
369
370 assert!(cache.is_denied(&policy_denied));
372 assert_eq!(cache.get_permission(&allowed), Some(PermissionGrant::Session));
373
374 assert!(!cache.is_temporarily_denied(&temp_denied));
376 assert_eq!(cache.get_permission(&temp_denied), None);
377 }
378}