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 {
72 grants: HashMap::new(),
73 hits: 0,
74 misses: 0,
75 }
76 }
77
78 #[inline]
80 pub fn get_permission<Q>(&mut self, key: &Q) -> Option<PermissionGrant>
81 where
82 K: Borrow<Q>,
83 Q: Hash + Eq + ?Sized,
84 {
85 if let Some(grant) = self.grants.get(key) {
86 self.hits += 1;
87 Some(*grant)
88 } else {
89 self.misses += 1;
90 None
91 }
92 }
93
94 #[inline]
96 pub fn cache_grant(&mut self, key: K, grant: PermissionGrant) {
97 self.grants.insert(key, grant);
98 }
99
100 #[inline]
102 pub fn invalidate<Q>(&mut self, key: &Q)
103 where
104 K: Borrow<Q>,
105 Q: Hash + Eq + ?Sized,
106 {
107 self.grants.remove(key);
108 }
109
110 pub fn clear_temporary_denials(&mut self) {
112 self.grants
113 .retain(|_, grant| *grant != PermissionGrant::TemporaryDenial);
114 }
115
116 pub fn clear(&mut self) {
118 self.grants.clear();
119 self.hits = 0;
120 self.misses = 0;
121 }
122
123 #[inline]
125 pub fn stats(&self) -> PermissionCacheStats {
126 PermissionCacheStats::compute(self.grants.len(), self.hits, self.misses)
127 }
128
129 #[inline]
131 pub fn is_denied<Q>(&self, key: &Q) -> bool
132 where
133 K: Borrow<Q>,
134 Q: Hash + Eq + ?Sized,
135 {
136 matches!(self.grants.get(key), Some(PermissionGrant::Denied))
137 }
138
139 #[inline]
141 pub fn is_temporarily_denied<Q>(&self, key: &Q) -> bool
142 where
143 K: Borrow<Q>,
144 Q: Hash + Eq + ?Sized,
145 {
146 matches!(self.grants.get(key), Some(PermissionGrant::TemporaryDenial))
147 }
148
149 #[inline]
151 pub fn can_use_cached<Q>(&self, key: &Q) -> bool
152 where
153 K: Borrow<Q>,
154 Q: Hash + Eq + ?Sized,
155 {
156 matches!(
157 self.grants.get(key),
158 Some(PermissionGrant::Session | PermissionGrant::Permanent | PermissionGrant::Denied)
159 )
160 }
161}
162
163impl<K: Eq + Hash> Default for PermissionCache<K> {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169pub type AcpPermissionCache = PermissionCache<PathBuf>;
172
173pub type ToolPermissionCache = PermissionCache<String>;
175
176pub type ToolPermissionCacheStats = PermissionCacheStats;
178
179impl ToolPermissionCache {
181 #[inline]
183 pub fn cache_grant_tool(&mut self, tool_name: impl Into<String>, grant: PermissionGrant) {
184 self.cache_grant(tool_name.into(), grant);
185 }
186}
187
188#[cfg(test)]
189mod tests {
190 use super::*;
191
192 fn test_path(name: &str) -> PathBuf {
193 PathBuf::from(format!("/workspace/{}", name))
194 }
195
196 #[test]
197 fn test_creates_empty_cache() {
198 let cache = AcpPermissionCache::new();
199 let stats = cache.stats();
200 assert_eq!(stats.cached_entries, 0);
201 assert_eq!(stats.hits, 0);
202 assert_eq!(stats.misses, 0);
203 }
204
205 #[test]
206 fn test_caches_permission_grant() {
207 let mut cache = AcpPermissionCache::new();
208 let path = test_path("file.rs");
209
210 cache.cache_grant(path.clone(), PermissionGrant::Session);
212 assert_eq!(cache.get_permission(&path), Some(PermissionGrant::Session));
213 }
214
215 #[test]
216 fn test_tracks_hits_and_misses() {
217 let mut cache = AcpPermissionCache::new();
218 let path = test_path("file.rs");
219
220 cache.cache_grant(path.clone(), PermissionGrant::Session);
221
222 let _ = cache.get_permission(&path);
224 assert_eq!(cache.stats().hits, 1);
225
226 let _ = cache.get_permission(&test_path("other.rs"));
228 assert_eq!(cache.stats().misses, 1);
229 }
230
231 #[test]
232 fn test_calculates_hit_rate() {
233 let mut cache = AcpPermissionCache::new();
234 let path1 = test_path("file1.rs");
235 let path2 = test_path("file2.rs");
236
237 let path1_for_cache = path1.clone();
238 cache.cache_grant(path1_for_cache, PermissionGrant::Session);
239
240 cache.get_permission(&path1);
242 cache.get_permission(&path1);
243 cache.get_permission(&path1);
244
245 cache.get_permission(&path2);
247
248 let stats = cache.stats();
249 assert_eq!(stats.hits, 3);
250 assert_eq!(stats.misses, 1);
251 assert_eq!(stats.total_requests, 4);
252 assert!((stats.hit_rate - 0.75).abs() < 0.001);
253 }
254
255 #[test]
256 fn test_invalidates_path() {
257 let mut cache = AcpPermissionCache::new();
258 let path = test_path("file.rs");
259
260 cache.cache_grant(path.clone(), PermissionGrant::Session);
261 assert!(cache.get_permission(&path).is_some());
262
263 cache.invalidate(&path);
264 assert!(cache.get_permission(&path).is_none());
265 }
266
267 #[test]
268 fn test_clears_all() {
269 let mut cache = AcpPermissionCache::new();
270
271 cache.cache_grant(test_path("file1.rs"), PermissionGrant::Session);
272 cache.cache_grant(test_path("file2.rs"), PermissionGrant::Session);
273 cache.get_permission(&test_path("file1.rs"));
274
275 cache.clear();
276 let stats = cache.stats();
277 assert_eq!(stats.cached_entries, 0);
278 assert_eq!(stats.hits, 0);
279 assert_eq!(stats.misses, 0);
280 }
281
282 #[test]
283 fn test_identifies_denied_paths() {
284 let mut cache = AcpPermissionCache::new();
285 let denied_path = test_path("secret.txt");
286 let allowed_path = test_path("public.txt");
287
288 let denied_for_cache = denied_path.clone();
289 let allowed_for_cache = allowed_path.clone();
290 cache.cache_grant(denied_for_cache, PermissionGrant::Denied);
291 cache.cache_grant(allowed_for_cache, PermissionGrant::Session);
292
293 assert!(cache.is_denied(&denied_path));
294 assert!(!cache.is_denied(&allowed_path));
295 assert!(!cache.is_denied(&test_path("unknown.txt")));
296 }
297
298 #[test]
299 fn test_can_use_cached_for_session_grants() {
300 let mut cache = AcpPermissionCache::new();
301 let once_path = test_path("once.rs");
302 let session_path = test_path("session.rs");
303 let denied_path = test_path("denied.rs");
304 let temp_denied_path = test_path("temp_denied.rs");
305
306 cache.cache_grant(once_path.clone(), PermissionGrant::Once);
307 cache.cache_grant(session_path.clone(), PermissionGrant::Session);
308 cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
309 cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
310
311 assert!(!cache.can_use_cached(&once_path));
313 assert!(!cache.can_use_cached(&temp_denied_path));
314
315 assert!(cache.can_use_cached(&session_path));
317 assert!(cache.can_use_cached(&denied_path));
318 }
319
320 #[test]
321 fn test_multiple_paths() {
322 let mut cache = AcpPermissionCache::new();
323
324 for i in 0..5 {
325 cache.cache_grant(
326 test_path(&format!("file{}.rs", i)),
327 PermissionGrant::Session,
328 );
329 }
330
331 assert_eq!(cache.stats().cached_entries, 5);
332
333 for i in 0..5 {
334 let grant = cache.get_permission(&test_path(&format!("file{}.rs", i)));
335 assert_eq!(grant, Some(PermissionGrant::Session));
336 }
337
338 assert_eq!(cache.stats().hits, 5);
339 }
340
341 #[test]
342 fn test_distinguishes_denied_from_temporary_denial() {
343 let mut cache = AcpPermissionCache::new();
344 let denied_path = test_path("denied.rs");
345 let temp_denied_path = test_path("temp_denied.rs");
346
347 cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
348 cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
349
350 assert!(cache.is_denied(&denied_path));
352 assert!(!cache.is_denied(&temp_denied_path));
353
354 assert!(!cache.is_temporarily_denied(&denied_path));
355 assert!(cache.is_temporarily_denied(&temp_denied_path));
356 }
357
358 #[test]
359 fn test_clear_temporary_denials_preserves_policy_denials() {
360 let mut cache = AcpPermissionCache::new();
361 let policy_denied = test_path("policy_denied.rs");
362 let temp_denied = test_path("temp_denied.rs");
363 let allowed = test_path("allowed.rs");
364
365 cache.cache_grant(policy_denied.clone(), PermissionGrant::Denied);
366 cache.cache_grant(temp_denied.clone(), PermissionGrant::TemporaryDenial);
367 cache.cache_grant(allowed.clone(), PermissionGrant::Session);
368
369 cache.clear_temporary_denials();
370
371 assert!(cache.is_denied(&policy_denied));
373 assert_eq!(
374 cache.get_permission(&allowed),
375 Some(PermissionGrant::Session)
376 );
377
378 assert!(!cache.is_temporarily_denied(&temp_denied));
380 assert_eq!(cache.get_permission(&temp_denied), None);
381 }
382}