use hashbrown::HashMap;
use std::borrow::Borrow;
use std::hash::Hash;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PermissionGrant {
Once,
Session,
Permanent,
Denied,
TemporaryDenial,
}
#[derive(Debug, Clone, Default)]
pub struct PermissionCacheStats {
pub cached_entries: usize,
pub hits: usize,
pub misses: usize,
pub total_requests: usize,
pub hit_rate: f64,
}
impl PermissionCacheStats {
#[inline]
fn compute(entries: usize, hits: usize, misses: usize) -> Self {
let total_requests = hits + misses;
let hit_rate = if total_requests > 0 {
(hits as f64) / (total_requests as f64)
} else {
0.0
};
Self {
cached_entries: entries,
hits,
misses,
total_requests,
hit_rate,
}
}
}
#[derive(Debug)]
pub struct PermissionCache<K: Eq + Hash> {
grants: HashMap<K, PermissionGrant>,
hits: usize,
misses: usize,
}
impl<K: Eq + Hash> PermissionCache<K> {
#[inline]
pub fn new() -> Self {
Self {
grants: HashMap::new(),
hits: 0,
misses: 0,
}
}
#[inline]
pub fn get_permission<Q>(&mut self, key: &Q) -> Option<PermissionGrant>
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
if let Some(grant) = self.grants.get(key) {
self.hits += 1;
Some(*grant)
} else {
self.misses += 1;
None
}
}
#[inline]
pub fn cache_grant(&mut self, key: K, grant: PermissionGrant) {
self.grants.insert(key, grant);
}
#[inline]
pub fn invalidate<Q>(&mut self, key: &Q)
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
self.grants.remove(key);
}
pub fn clear_temporary_denials(&mut self) {
self.grants
.retain(|_, grant| *grant != PermissionGrant::TemporaryDenial);
}
pub fn clear(&mut self) {
self.grants.clear();
self.hits = 0;
self.misses = 0;
}
#[inline]
pub fn stats(&self) -> PermissionCacheStats {
PermissionCacheStats::compute(self.grants.len(), self.hits, self.misses)
}
#[inline]
pub fn is_denied<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
matches!(self.grants.get(key), Some(PermissionGrant::Denied))
}
#[inline]
pub fn is_temporarily_denied<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
matches!(self.grants.get(key), Some(PermissionGrant::TemporaryDenial))
}
#[inline]
pub fn can_use_cached<Q>(&self, key: &Q) -> bool
where
K: Borrow<Q>,
Q: Hash + Eq + ?Sized,
{
matches!(
self.grants.get(key),
Some(PermissionGrant::Session | PermissionGrant::Permanent | PermissionGrant::Denied)
)
}
}
impl<K: Eq + Hash> Default for PermissionCache<K> {
fn default() -> Self {
Self::new()
}
}
pub type AcpPermissionCache = PermissionCache<PathBuf>;
pub type ToolPermissionCache = PermissionCache<String>;
pub type ToolPermissionCacheStats = PermissionCacheStats;
impl ToolPermissionCache {
#[inline]
pub fn cache_grant_tool(&mut self, tool_name: impl Into<String>, grant: PermissionGrant) {
self.cache_grant(tool_name.into(), grant);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_path(name: &str) -> PathBuf {
PathBuf::from(format!("/workspace/{}", name))
}
#[test]
fn test_creates_empty_cache() {
let cache = AcpPermissionCache::new();
let stats = cache.stats();
assert_eq!(stats.cached_entries, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_caches_permission_grant() {
let mut cache = AcpPermissionCache::new();
let path = test_path("file.rs");
cache.cache_grant(path.clone(), PermissionGrant::Session);
assert_eq!(cache.get_permission(&path), Some(PermissionGrant::Session));
}
#[test]
fn test_tracks_hits_and_misses() {
let mut cache = AcpPermissionCache::new();
let path = test_path("file.rs");
cache.cache_grant(path.clone(), PermissionGrant::Session);
let _ = cache.get_permission(&path);
assert_eq!(cache.stats().hits, 1);
let _ = cache.get_permission(&test_path("other.rs"));
assert_eq!(cache.stats().misses, 1);
}
#[test]
fn test_calculates_hit_rate() {
let mut cache = AcpPermissionCache::new();
let path1 = test_path("file1.rs");
let path2 = test_path("file2.rs");
let path1_for_cache = path1.clone();
cache.cache_grant(path1_for_cache, PermissionGrant::Session);
cache.get_permission(&path1);
cache.get_permission(&path1);
cache.get_permission(&path1);
cache.get_permission(&path2);
let stats = cache.stats();
assert_eq!(stats.hits, 3);
assert_eq!(stats.misses, 1);
assert_eq!(stats.total_requests, 4);
assert!((stats.hit_rate - 0.75).abs() < 0.001);
}
#[test]
fn test_invalidates_path() {
let mut cache = AcpPermissionCache::new();
let path = test_path("file.rs");
cache.cache_grant(path.clone(), PermissionGrant::Session);
assert!(cache.get_permission(&path).is_some());
cache.invalidate(&path);
assert!(cache.get_permission(&path).is_none());
}
#[test]
fn test_clears_all() {
let mut cache = AcpPermissionCache::new();
cache.cache_grant(test_path("file1.rs"), PermissionGrant::Session);
cache.cache_grant(test_path("file2.rs"), PermissionGrant::Session);
cache.get_permission(&test_path("file1.rs"));
cache.clear();
let stats = cache.stats();
assert_eq!(stats.cached_entries, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
}
#[test]
fn test_identifies_denied_paths() {
let mut cache = AcpPermissionCache::new();
let denied_path = test_path("secret.txt");
let allowed_path = test_path("public.txt");
let denied_for_cache = denied_path.clone();
let allowed_for_cache = allowed_path.clone();
cache.cache_grant(denied_for_cache, PermissionGrant::Denied);
cache.cache_grant(allowed_for_cache, PermissionGrant::Session);
assert!(cache.is_denied(&denied_path));
assert!(!cache.is_denied(&allowed_path));
assert!(!cache.is_denied(&test_path("unknown.txt")));
}
#[test]
fn test_can_use_cached_for_session_grants() {
let mut cache = AcpPermissionCache::new();
let once_path = test_path("once.rs");
let session_path = test_path("session.rs");
let denied_path = test_path("denied.rs");
let temp_denied_path = test_path("temp_denied.rs");
cache.cache_grant(once_path.clone(), PermissionGrant::Once);
cache.cache_grant(session_path.clone(), PermissionGrant::Session);
cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
assert!(!cache.can_use_cached(&once_path));
assert!(!cache.can_use_cached(&temp_denied_path));
assert!(cache.can_use_cached(&session_path));
assert!(cache.can_use_cached(&denied_path));
}
#[test]
fn test_multiple_paths() {
let mut cache = AcpPermissionCache::new();
for i in 0..5 {
cache.cache_grant(
test_path(&format!("file{}.rs", i)),
PermissionGrant::Session,
);
}
assert_eq!(cache.stats().cached_entries, 5);
for i in 0..5 {
let grant = cache.get_permission(&test_path(&format!("file{}.rs", i)));
assert_eq!(grant, Some(PermissionGrant::Session));
}
assert_eq!(cache.stats().hits, 5);
}
#[test]
fn test_distinguishes_denied_from_temporary_denial() {
let mut cache = AcpPermissionCache::new();
let denied_path = test_path("denied.rs");
let temp_denied_path = test_path("temp_denied.rs");
cache.cache_grant(denied_path.clone(), PermissionGrant::Denied);
cache.cache_grant(temp_denied_path.clone(), PermissionGrant::TemporaryDenial);
assert!(cache.is_denied(&denied_path));
assert!(!cache.is_denied(&temp_denied_path));
assert!(!cache.is_temporarily_denied(&denied_path));
assert!(cache.is_temporarily_denied(&temp_denied_path));
}
#[test]
fn test_clear_temporary_denials_preserves_policy_denials() {
let mut cache = AcpPermissionCache::new();
let policy_denied = test_path("policy_denied.rs");
let temp_denied = test_path("temp_denied.rs");
let allowed = test_path("allowed.rs");
cache.cache_grant(policy_denied.clone(), PermissionGrant::Denied);
cache.cache_grant(temp_denied.clone(), PermissionGrant::TemporaryDenial);
cache.cache_grant(allowed.clone(), PermissionGrant::Session);
cache.clear_temporary_denials();
assert!(cache.is_denied(&policy_denied));
assert_eq!(
cache.get_permission(&allowed),
Some(PermissionGrant::Session)
);
assert!(!cache.is_temporarily_denied(&temp_denied));
assert_eq!(cache.get_permission(&temp_denied), None);
}
}