//! CCR Store — Compress-Cache-Retrieve: reversible compression with retrieval //! //! When compressing, cache the full original with a SHA256 hash. //! If the model needs more detail, it can request the original via hash lookup. //! This makes compression aggressive but reversible. use anyhow::Result; use sha2::{Sha256, Digest}; use indexmap::IndexMap; use std::sync::Mutex; use time::OffsetDateTime; pub struct CcrStore { /// LRU cache: hash → (original_content, timestamp) cache: Mutex>, max_entries: usize, ttl_secs: u64, } impl CcrStore { /// Create a new CCR store with default limits pub fn new() -> Self { Self::with_limits(1000, 3600) // 1000 entries, 1 hour TTL } /// Create with custom limits pub fn with_limits(max_entries: usize, ttl_secs: u64) -> Self { Self { cache: Mutex::new(IndexMap::new()), max_entries, ttl_secs, } } /// Store original content and return its hash pub fn store(&self, content: &str) -> Result { let hash = self.hash_content(content); let mut cache = self.cache.lock().map_err(|e| anyhow::anyhow!("mutex lock failed: {}", e))?; // Remove oldest entry if at capacity if cache.len() >= self.max_entries { if let Some(oldest_key) = cache.keys().next().cloned() { cache.remove(&oldest_key); } } cache.insert(hash.clone(), (content.to_string(), OffsetDateTime::now_utc())); Ok(hash) } /// Retrieve original content by hash pub fn retrieve(&self, hash: &str) -> Result> { let mut cache = self.cache.lock().map_err(|e| anyhow::anyhow!("mutex lock failed: {}", e))?; if let Some((content, timestamp)) = cache.get(hash) { // Check if expired let duration = OffsetDateTime::now_utc() - *timestamp; if duration.whole_seconds() > self.ttl_secs as i64 { cache.remove(hash); return Ok(None); } // Return copy without moving (to avoid mutable borrow) return Ok(Some(content.clone())); } Ok(None) } /// Compute hash of content fn hash_content(&self, content: &str) -> String { let mut hasher = Sha256::new(); hasher.update(content.as_bytes()); format!("{:x}", hasher.finalize()) } /// Get current cache size pub fn size(&self) -> Result { let cache = self.cache.lock().map_err(|e| anyhow::anyhow!("mutex lock failed: {}", e))?; Ok(cache.len()) } /// Clear expired entries pub fn evict_expired(&self) -> Result { let mut cache = self.cache.lock().map_err(|e| anyhow::anyhow!("mutex lock failed: {}", e))?; let before = cache.len(); cache.retain(|_, (_, timestamp)| { let duration = OffsetDateTime::now_utc() - *timestamp; duration.whole_seconds() <= self.ttl_secs as i64 }); Ok(before - cache.len()) } } impl Default for CcrStore { fn default() -> Self { Self::new() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_ccr_store_and_retrieve() { let store = CcrStore::new(); let content = "Original content to store"; let hash = store.store(content).unwrap(); assert!(!hash.is_empty()); assert_eq!(hash.len(), 64); // SHA256 hex let retrieved = store.retrieve(&hash).unwrap(); assert_eq!(retrieved, Some(content.to_string())); } #[test] fn test_ccr_hash_deterministic() { let store = CcrStore::new(); let content = "Deterministic content"; let hash1 = store.store(content).unwrap(); let hash2 = store.store(content).unwrap(); assert_eq!(hash1, hash2); } #[test] fn test_ccr_different_content_different_hash() { let store = CcrStore::new(); let hash1 = store.store("Content A").unwrap(); let hash2 = store.store("Content B").unwrap(); assert_ne!(hash1, hash2); } #[test] fn test_ccr_retrieve_nonexistent() { let store = CcrStore::new(); let result = store.retrieve("nonexistent_hash").unwrap(); assert_eq!(result, None); } #[test] fn test_ccr_lru_eviction() { let store = CcrStore::with_limits(3, 3600); let h1 = store.store("content1").unwrap(); let h2 = store.store("content2").unwrap(); let h3 = store.store("content3").unwrap(); // Cache at capacity assert_eq!(store.size().unwrap(), 3); // Add new entry should evict oldest (h1) let _h4 = store.store("content4").unwrap(); assert_eq!(store.size().unwrap(), 3); // h1 should be gone assert_eq!(store.retrieve(&h1).unwrap(), None); // h2, h3 should still exist assert!(store.retrieve(&h2).unwrap().is_some()); assert!(store.retrieve(&h3).unwrap().is_some()); } #[test] fn test_ccr_inject_hint() { let store = CcrStore::new(); let content = "Original error message"; let hash = store.store(content).unwrap(); let hint = format!("", hash); assert!(hint.len() > 10); assert!(hint.contains("CCR:")); assert!(hint.contains(&hash)); } #[test] fn test_ccr_large_content() { let store = CcrStore::new(); let large = "x".repeat(100_000); let hash = store.store(&large).unwrap(); let retrieved = store.retrieve(&hash).unwrap(); assert_eq!(retrieved.unwrap().len(), 100_000); } #[test] fn test_ccr_multiple_stores_same_content() { let store = CcrStore::new(); let h1 = store.store("shared").unwrap(); let h2 = store.store("shared").unwrap(); // Same content should produce same hash assert_eq!(h1, h2); // But they should have separate cache entries (LRU) // Last one should be retrievable assert!(store.retrieve(&h2).unwrap().is_some()); } }