#![allow(dead_code)]
pub mod backend;
pub mod entry;
pub mod filesystem;
pub mod key;
pub mod memory;
#[cfg(feature = "redis")]
pub mod redis;
pub mod rug_pull;
pub mod stats;
pub use backend::{Cache, CacheBackend, CacheConfig};
pub use entry::CacheEntry;
pub use filesystem::FilesystemCache;
pub use key::{CacheCategory, CacheKey};
pub use memory::MemoryCache;
#[cfg(feature = "redis")]
pub use redis::RedisCache;
#[allow(unused_imports)]
pub use rug_pull::{detect_rug_pull, RugPullDetection, RugPullSeverity, ToolHashRecord};
#[allow(unused_imports)]
pub use stats::{CacheStats, CategoryStats};
use anyhow::Result;
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
use std::time::Duration;
pub struct CacheManager {
backend: Arc<dyn Cache>,
config: CacheConfig,
}
impl CacheManager {
pub async fn new(config: CacheConfig) -> Result<Self> {
let backend: Arc<dyn Cache> = match &config.backend {
CacheBackend::Filesystem { path } => {
Arc::new(FilesystemCache::with_path(path.clone(), config.clone()).await?)
}
CacheBackend::Memory => Arc::new(MemoryCache::new(config.clone())),
#[cfg(feature = "redis")]
CacheBackend::Redis { url, .. } => {
Arc::new(RedisCache::new(url, config.clone()).await?)
}
};
Ok(Self { backend, config })
}
pub fn memory() -> Self {
Self {
backend: Arc::new(MemoryCache::new(CacheConfig::memory())),
config: CacheConfig::memory(),
}
}
pub fn is_enabled(&self) -> bool {
self.config.enabled
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
pub async fn get<T: DeserializeOwned>(&self, key: &CacheKey) -> Result<Option<T>> {
if !self.config.enabled {
return Ok(None);
}
if let Some(entry) = self.backend.get(key).await? {
let value = entry.to_value()?;
Ok(Some(value))
} else {
Ok(None)
}
}
pub async fn set<T: Serialize>(&self, key: &CacheKey, value: &T) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let ttl = self.config.ttl_for_category(key.category);
let entry = CacheEntry::from_value(value, ttl)?;
self.backend.set(key, entry).await
}
pub async fn set_with_ttl<T: Serialize>(
&self,
key: &CacheKey,
value: &T,
ttl: Duration,
) -> Result<()> {
if !self.config.enabled {
return Ok(());
}
let entry = CacheEntry::from_value(value, ttl)?;
self.backend.set(key, entry).await
}
pub async fn delete(&self, key: &CacheKey) -> Result<()> {
self.backend.delete(key).await
}
pub async fn exists(&self, key: &CacheKey) -> Result<bool> {
if !self.config.enabled {
return Ok(false);
}
self.backend.exists(key).await
}
pub async fn get_schema<T: DeserializeOwned>(&self, server_hash: &str) -> Result<Option<T>> {
let key = CacheKey::schema(server_hash);
self.get(&key).await
}
pub async fn set_schema<T: Serialize>(&self, server_hash: &str, schema: &T) -> Result<()> {
let key = CacheKey::schema(server_hash);
self.set(&key, schema).await
}
pub async fn get_scan_result<T: DeserializeOwned>(
&self,
server_hash: &str,
ruleset_hash: &str,
) -> Result<Option<T>> {
let key = CacheKey::scan_result(server_hash, ruleset_hash);
self.get(&key).await
}
pub async fn set_scan_result<T: Serialize>(
&self,
server_hash: &str,
ruleset_hash: &str,
result: &T,
) -> Result<()> {
let key = CacheKey::scan_result(server_hash, ruleset_hash);
self.set(&key, result).await
}
pub async fn get_validation<T: DeserializeOwned>(
&self,
server_hash: &str,
protocol_version: &str,
) -> Result<Option<T>> {
let key = CacheKey::validation(server_hash, protocol_version);
self.get(&key).await
}
pub async fn set_validation<T: Serialize>(
&self,
server_hash: &str,
protocol_version: &str,
result: &T,
) -> Result<()> {
let key = CacheKey::validation(server_hash, protocol_version);
self.set(&key, result).await
}
pub async fn get_corpus<T: DeserializeOwned>(&self, server_id: &str) -> Result<Option<T>> {
let key = CacheKey::corpus(server_id);
self.get(&key).await
}
pub async fn set_corpus<T: Serialize>(&self, server_id: &str, corpus: &T) -> Result<()> {
let key = CacheKey::corpus(server_id);
self.set(&key, corpus).await
}
pub async fn get_tool_hash<T: DeserializeOwned>(&self, server_id: &str) -> Result<Option<T>> {
let key = CacheKey::tool_hash(server_id);
self.get(&key).await
}
pub async fn set_tool_hash<T: Serialize>(&self, server_id: &str, hash: &T) -> Result<()> {
let key = CacheKey::tool_hash(server_id);
self.set(&key, hash).await
}
pub async fn clear(&self, category: Option<CacheCategory>) -> Result<u64> {
self.backend.clear(category).await
}
pub async fn prune_expired(&self) -> Result<u64> {
self.backend.prune_expired().await
}
pub async fn stats(&self) -> Result<CacheStats> {
self.backend.stats().await
}
pub async fn keys(&self, category: Option<CacheCategory>) -> Result<Vec<CacheKey>> {
self.backend.keys(category).await
}
}
pub fn hash_server(server: &str, args: &[String]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
server.hash(&mut hasher);
for arg in args {
arg.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
pub fn hash_ruleset(rules: &[String]) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
for rule in rules {
rule.hash(&mut hasher);
}
format!("{:016x}", hasher.finish())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn cache_manager_memory() {
let cache = CacheManager::memory();
assert!(cache.is_enabled());
let key = CacheKey::schema("test");
let data = vec!["tool1", "tool2"];
cache.set(&key, &data).await.unwrap();
let retrieved: Option<Vec<String>> = cache.get(&key).await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap(), data);
}
#[tokio::test]
async fn cache_manager_schema_helpers() {
let cache = CacheManager::memory();
let tools = vec!["read_file", "write_file"];
cache.set_schema("server1", &tools).await.unwrap();
let retrieved: Option<Vec<String>> = cache.get_schema("server1").await.unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap(), tools);
}
#[tokio::test]
async fn cache_manager_disabled() {
let config = CacheConfig::disabled();
let cache = CacheManager::new(config).await.unwrap();
assert!(!cache.is_enabled());
let key = CacheKey::schema("test");
cache.set(&key, &"data").await.unwrap();
let retrieved: Option<String> = cache.get(&key).await.unwrap();
assert!(retrieved.is_none());
}
#[test]
fn hash_server_consistency() {
let hash1 = hash_server("server", &["arg1".to_string(), "arg2".to_string()]);
let hash2 = hash_server("server", &["arg1".to_string(), "arg2".to_string()]);
assert_eq!(hash1, hash2);
let hash3 = hash_server("server", &["arg1".to_string()]);
assert_ne!(hash1, hash3);
}
#[test]
fn hash_ruleset_consistency() {
let hash1 = hash_ruleset(&["rule1".to_string(), "rule2".to_string()]);
let hash2 = hash_ruleset(&["rule1".to_string(), "rule2".to_string()]);
assert_eq!(hash1, hash2);
}
#[tokio::test]
async fn cache_manager_new_with_memory_backend() {
let config = CacheConfig::memory();
let cache = CacheManager::new(config).await.unwrap();
assert!(cache.is_enabled());
assert!(matches!(cache.config().backend, CacheBackend::Memory));
}
#[tokio::test]
async fn cache_manager_config_accessor() {
let cache = CacheManager::memory();
let config = cache.config();
assert!(config.enabled);
assert_eq!(config.schema_ttl_secs, 3600);
}
#[tokio::test]
async fn cache_manager_get_nonexistent() {
let cache = CacheManager::memory();
let key = CacheKey::schema("nonexistent");
let result: Option<String> = cache.get(&key).await.unwrap();
assert!(result.is_none());
}
#[tokio::test]
async fn cache_manager_set_with_custom_ttl() {
let cache = CacheManager::memory();
let key = CacheKey::schema("test");
let data = "test data";
cache
.set_with_ttl(&key, &data, Duration::from_secs(7200))
.await
.unwrap();
let retrieved: Option<String> = cache.get(&key).await.unwrap();
assert_eq!(retrieved, Some(data.to_string()));
}
#[tokio::test]
async fn cache_manager_delete() {
let cache = CacheManager::memory();
let key = CacheKey::schema("test");
let data = "test data";
cache.set(&key, &data).await.unwrap();
assert!(cache.exists(&key).await.unwrap());
cache.delete(&key).await.unwrap();
assert!(!cache.exists(&key).await.unwrap());
}
#[tokio::test]
async fn cache_manager_exists() {
let cache = CacheManager::memory();
let key = CacheKey::schema("test");
assert!(!cache.exists(&key).await.unwrap());
cache.set(&key, &"data").await.unwrap();
assert!(cache.exists(&key).await.unwrap());
}
#[tokio::test]
async fn cache_manager_exists_when_disabled() {
let config = CacheConfig::disabled();
let cache = CacheManager::new(config).await.unwrap();
let key = CacheKey::schema("test");
assert!(!cache.exists(&key).await.unwrap());
}
#[tokio::test]
async fn cache_manager_scan_result_helpers() {
let cache = CacheManager::memory();
let result = vec!["finding1".to_string(), "finding2".to_string()];
cache
.set_scan_result("server1", "rules1", &result)
.await
.unwrap();
let retrieved: Option<Vec<String>> =
cache.get_scan_result("server1", "rules1").await.unwrap();
assert_eq!(retrieved, Some(result));
}
#[tokio::test]
async fn cache_manager_validation_helpers() {
let cache = CacheManager::memory();
let result = vec!["violation1".to_string(), "violation2".to_string()];
cache
.set_validation("server1", "2024-11", &result)
.await
.unwrap();
let retrieved: Option<Vec<String>> =
cache.get_validation("server1", "2024-11").await.unwrap();
assert_eq!(retrieved, Some(result));
}
#[tokio::test]
async fn cache_manager_corpus_helpers() {
let cache = CacheManager::memory();
let corpus = vec!["input1".to_string(), "input2".to_string()];
cache.set_corpus("server1", &corpus).await.unwrap();
let retrieved: Option<Vec<String>> = cache.get_corpus("server1").await.unwrap();
assert_eq!(retrieved, Some(corpus));
}
#[tokio::test]
async fn cache_manager_tool_hash_helpers() {
let cache = CacheManager::memory();
let hash = "abc123";
cache.set_tool_hash("server1", &hash).await.unwrap();
let retrieved: Option<String> = cache.get_tool_hash("server1").await.unwrap();
assert_eq!(retrieved, Some(hash.to_string()));
}
#[tokio::test]
async fn cache_manager_clear_all() {
let cache = CacheManager::memory();
cache.set_schema("server1", &"data1").await.unwrap();
cache.set_schema("server2", &"data2").await.unwrap();
cache
.set_validation("server3", "1.0", &"data3")
.await
.unwrap();
let cleared = cache.clear(None).await.unwrap();
assert_eq!(cleared, 3);
assert!(!cache.exists(&CacheKey::schema("server1")).await.unwrap());
}
#[tokio::test]
async fn cache_manager_clear_by_category() {
let cache = CacheManager::memory();
cache.set_schema("server1", &"data1").await.unwrap();
cache.set_schema("server2", &"data2").await.unwrap();
cache
.set_validation("server3", "1.0", &"data3")
.await
.unwrap();
let cleared = cache.clear(Some(CacheCategory::Schema)).await.unwrap();
assert_eq!(cleared, 2);
assert!(!cache.exists(&CacheKey::schema("server1")).await.unwrap());
assert!(cache
.exists(&CacheKey::validation("server3", "1.0"))
.await
.unwrap());
}
#[tokio::test]
async fn cache_manager_prune_expired() {
let cache = CacheManager::memory();
let key = CacheKey::schema("test");
cache
.set_with_ttl(&key, &"data", Duration::from_secs(0))
.await
.unwrap();
tokio::time::sleep(Duration::from_millis(10)).await;
let pruned = cache.prune_expired().await.unwrap();
assert_eq!(pruned, 1);
}
#[tokio::test]
async fn cache_manager_stats() {
let cache = CacheManager::memory();
cache.set_schema("server1", &"data1").await.unwrap();
cache.set_schema("server2", &"data2").await.unwrap();
let stats = cache.stats().await.unwrap();
assert_eq!(stats.total_entries, 2);
}
#[tokio::test]
async fn cache_manager_keys_all() {
let cache = CacheManager::memory();
cache.set_schema("server1", &"data1").await.unwrap();
cache
.set_validation("server2", "1.0", &"data2")
.await
.unwrap();
let keys = cache.keys(None).await.unwrap();
assert_eq!(keys.len(), 2);
}
#[tokio::test]
async fn cache_manager_keys_by_category() {
let cache = CacheManager::memory();
cache.set_schema("server1", &"data1").await.unwrap();
cache.set_schema("server2", &"data2").await.unwrap();
cache
.set_validation("server3", "1.0", &"data3")
.await
.unwrap();
let keys = cache.keys(Some(CacheCategory::Schema)).await.unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.iter().all(|k| k.category == CacheCategory::Schema));
}
#[test]
fn hash_server_different_args() {
let hash1 = hash_server("server", &["arg1".to_string()]);
let hash2 = hash_server("server", &["arg2".to_string()]);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_server_different_servers() {
let hash1 = hash_server("server1", &["arg".to_string()]);
let hash2 = hash_server("server2", &["arg".to_string()]);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_server_empty_args() {
let hash1 = hash_server("server", &[]);
let hash2 = hash_server("server", &[]);
assert_eq!(hash1, hash2);
}
#[test]
fn hash_server_format() {
let hash = hash_server("server", &["arg".to_string()]);
assert_eq!(hash.len(), 16);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn hash_ruleset_empty() {
let hash1 = hash_ruleset(&[]);
let hash2 = hash_ruleset(&[]);
assert_eq!(hash1, hash2);
}
#[test]
fn hash_ruleset_different_order() {
let hash1 = hash_ruleset(&["rule1".to_string(), "rule2".to_string()]);
let hash2 = hash_ruleset(&["rule2".to_string(), "rule1".to_string()]);
assert_ne!(hash1, hash2);
}
#[test]
fn hash_ruleset_format() {
let hash = hash_ruleset(&["rule1".to_string()]);
assert_eq!(hash.len(), 16);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
}
}