#![allow(dead_code)]
use std::collections::HashMap;
use std::time::{Duration, Instant};
const DEFAULT_TTL_SECS: u64 = 3600;
const DEFAULT_MAX_ENTRIES: usize = 100_000;
#[derive(Debug, Clone)]
pub struct SignatureEntry {
pub signature: Vec<u8>,
pub file_size: u64,
created_at: Instant,
last_accessed: Instant,
access_count: u64,
ttl: Duration,
}
impl SignatureEntry {
pub fn new(signature: Vec<u8>, file_size: u64, ttl: Duration) -> Self {
let now = Instant::now();
Self {
signature,
file_size,
created_at: now,
last_accessed: now,
access_count: 0,
ttl,
}
}
pub fn is_expired(&self) -> bool {
self.created_at.elapsed() > self.ttl
}
pub fn age(&self) -> Duration {
self.created_at.elapsed()
}
pub fn idle_time(&self) -> Duration {
self.last_accessed.elapsed()
}
pub fn access_count(&self) -> u64 {
self.access_count
}
pub fn record_access(&mut self) {
self.last_accessed = Instant::now();
self.access_count += 1;
}
pub fn signature(&self) -> &[u8] {
&self.signature
}
}
#[derive(Debug, Clone)]
pub struct StoreConfig {
pub default_ttl: Duration,
pub max_entries: usize,
pub lru_eviction: bool,
}
impl Default for StoreConfig {
fn default() -> Self {
Self {
default_ttl: Duration::from_secs(DEFAULT_TTL_SECS),
max_entries: DEFAULT_MAX_ENTRIES,
lru_eviction: true,
}
}
}
impl StoreConfig {
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.default_ttl = ttl;
self
}
pub fn with_max_entries(mut self, max: usize) -> Self {
self.max_entries = max.max(1);
self
}
pub fn with_lru_eviction(mut self, enabled: bool) -> Self {
self.lru_eviction = enabled;
self
}
}
#[derive(Debug)]
pub struct SignatureStore {
entries: HashMap<String, SignatureEntry>,
config: StoreConfig,
total_inserts: u64,
total_lookups: u64,
total_hits: u64,
total_evictions: u64,
}
impl SignatureStore {
pub fn new() -> Self {
Self::with_config(StoreConfig::default())
}
pub fn with_config(config: StoreConfig) -> Self {
Self {
entries: HashMap::new(),
config,
total_inserts: 0,
total_lookups: 0,
total_hits: 0,
total_evictions: 0,
}
}
pub fn insert(&mut self, key: String, signature: Vec<u8>, file_size: u64) {
if self.entries.len() >= self.config.max_entries {
self.evict_one();
}
let entry = SignatureEntry::new(signature, file_size, self.config.default_ttl);
self.entries.insert(key, entry);
self.total_inserts += 1;
}
pub fn insert_with_ttl(
&mut self,
key: String,
signature: Vec<u8>,
file_size: u64,
ttl: Duration,
) {
if self.entries.len() >= self.config.max_entries {
self.evict_one();
}
let entry = SignatureEntry::new(signature, file_size, ttl);
self.entries.insert(key, entry);
self.total_inserts += 1;
}
pub fn get(&mut self, key: &str) -> Option<&[u8]> {
self.total_lookups += 1;
if self.entries.get(key).map_or(false, |e| e.is_expired()) {
self.entries.remove(key);
return None;
}
if let Some(entry) = self.entries.get_mut(key) {
entry.record_access();
self.total_hits += 1;
Some(entry.signature())
} else {
None
}
}
pub fn contains(&self, key: &str) -> bool {
self.entries.get(key).map_or(false, |e| !e.is_expired())
}
pub fn remove(&mut self, key: &str) -> bool {
self.entries.remove(key).is_some()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn purge_expired(&mut self) -> usize {
let before = self.entries.len();
self.entries.retain(|_, entry| !entry.is_expired());
let removed = before - self.entries.len();
self.total_evictions += removed as u64;
removed
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn stats(&self) -> StoreStats {
let total_signature_bytes: u64 = self
.entries
.values()
.map(|e| e.signature.len() as u64)
.sum();
StoreStats {
entries: self.entries.len(),
total_inserts: self.total_inserts,
total_lookups: self.total_lookups,
total_hits: self.total_hits,
total_evictions: self.total_evictions,
total_signature_bytes,
}
}
fn evict_one(&mut self) {
if self.entries.is_empty() {
return;
}
let expired_key = self
.entries
.iter()
.find(|(_, e)| e.is_expired())
.map(|(k, _)| k.clone());
if let Some(key) = expired_key {
self.entries.remove(&key);
self.total_evictions += 1;
return;
}
if self.config.lru_eviction {
let lru_key = self
.entries
.iter()
.min_by_key(|(_, e)| e.last_accessed)
.map(|(k, _)| k.clone());
if let Some(key) = lru_key {
self.entries.remove(&key);
self.total_evictions += 1;
}
} else {
let oldest_key = self
.entries
.iter()
.min_by_key(|(_, e)| e.created_at)
.map(|(k, _)| k.clone());
if let Some(key) = oldest_key {
self.entries.remove(&key);
self.total_evictions += 1;
}
}
}
pub fn find_matching(&self, signature: &[u8]) -> Vec<&str> {
self.entries
.iter()
.filter(|(_, e)| !e.is_expired() && e.signature == signature)
.map(|(k, _)| k.as_str())
.collect()
}
}
impl Default for SignatureStore {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct StoreStats {
pub entries: usize,
pub total_inserts: u64,
pub total_lookups: u64,
pub total_hits: u64,
pub total_evictions: u64,
pub total_signature_bytes: u64,
}
impl StoreStats {
#[allow(clippy::cast_precision_loss)]
pub fn hit_rate(&self) -> f64 {
if self.total_lookups == 0 {
return 0.0;
}
self.total_hits as f64 / self.total_lookups as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signature_entry_new() {
let entry = SignatureEntry::new(vec![1, 2, 3], 1024, Duration::from_secs(60));
assert_eq!(entry.signature(), &[1, 2, 3]);
assert_eq!(entry.file_size, 1024);
assert_eq!(entry.access_count(), 0);
assert!(!entry.is_expired());
}
#[test]
fn test_signature_entry_access() {
let mut entry = SignatureEntry::new(vec![1], 100, Duration::from_secs(600));
entry.record_access();
entry.record_access();
assert_eq!(entry.access_count(), 2);
}
#[test]
fn test_store_config_default() {
let cfg = StoreConfig::default();
assert_eq!(cfg.default_ttl, Duration::from_secs(DEFAULT_TTL_SECS));
assert_eq!(cfg.max_entries, DEFAULT_MAX_ENTRIES);
assert!(cfg.lru_eviction);
}
#[test]
fn test_store_config_builders() {
let cfg = StoreConfig::default()
.with_ttl(Duration::from_secs(300))
.with_max_entries(500)
.with_lru_eviction(false);
assert_eq!(cfg.default_ttl, Duration::from_secs(300));
assert_eq!(cfg.max_entries, 500);
assert!(!cfg.lru_eviction);
}
#[test]
fn test_store_insert_and_get() {
let mut store = SignatureStore::new();
store.insert("file1".to_string(), vec![0xAB, 0xCD], 2048);
let sig = store.get("file1").expect("operation should succeed");
assert_eq!(sig, &[0xAB, 0xCD]);
}
#[test]
fn test_store_get_nonexistent() {
let mut store = SignatureStore::new();
assert!(store.get("nope").is_none());
}
#[test]
fn test_store_contains() {
let mut store = SignatureStore::new();
store.insert("key1".to_string(), vec![1], 100);
assert!(store.contains("key1"));
assert!(!store.contains("key2"));
}
#[test]
fn test_store_remove() {
let mut store = SignatureStore::new();
store.insert("key1".to_string(), vec![1], 100);
assert!(store.remove("key1"));
assert!(!store.remove("key1"));
assert!(!store.contains("key1"));
}
#[test]
fn test_store_clear() {
let mut store = SignatureStore::new();
store.insert("a".to_string(), vec![1], 100);
store.insert("b".to_string(), vec![2], 200);
store.clear();
assert!(store.is_empty());
assert_eq!(store.len(), 0);
}
#[test]
fn test_store_eviction_on_max_entries() {
let config = StoreConfig::default().with_max_entries(3);
let mut store = SignatureStore::with_config(config);
store.insert("a".to_string(), vec![1], 100);
store.insert("b".to_string(), vec![2], 100);
store.insert("c".to_string(), vec![3], 100);
store.insert("d".to_string(), vec![4], 100);
assert_eq!(store.len(), 3);
assert!(store.contains("d"));
}
#[test]
fn test_store_stats() {
let mut store = SignatureStore::new();
store.insert("a".to_string(), vec![1, 2, 3], 100);
let _ = store.get("a");
let _ = store.get("b");
let stats = store.stats();
assert_eq!(stats.entries, 1);
assert_eq!(stats.total_inserts, 1);
assert_eq!(stats.total_lookups, 2);
assert_eq!(stats.total_hits, 1);
assert_eq!(stats.total_signature_bytes, 3);
}
#[test]
fn test_store_hit_rate() {
let stats = StoreStats {
entries: 10,
total_inserts: 10,
total_lookups: 100,
total_hits: 75,
total_evictions: 0,
total_signature_bytes: 100,
};
assert!((stats.hit_rate() - 0.75).abs() < f64::EPSILON);
}
#[test]
fn test_store_hit_rate_empty() {
let stats = StoreStats {
entries: 0,
total_inserts: 0,
total_lookups: 0,
total_hits: 0,
total_evictions: 0,
total_signature_bytes: 0,
};
assert!((stats.hit_rate() - 0.0).abs() < f64::EPSILON);
}
#[test]
fn test_find_matching() {
let mut store = SignatureStore::new();
store.insert("f1".to_string(), vec![1, 2, 3], 100);
store.insert("f2".to_string(), vec![1, 2, 3], 200);
store.insert("f3".to_string(), vec![4, 5, 6], 300);
let matches = store.find_matching(&[1, 2, 3]);
assert_eq!(matches.len(), 2);
assert!(matches.contains(&"f1"));
assert!(matches.contains(&"f2"));
}
#[test]
fn test_insert_with_custom_ttl() {
let mut store = SignatureStore::new();
store.insert_with_ttl("k".to_string(), vec![9], 50, Duration::from_secs(1));
assert!(store.contains("k"));
}
#[test]
fn test_expired_entry_not_returned() {
let mut store =
SignatureStore::with_config(StoreConfig::default().with_ttl(Duration::from_millis(0)));
store.insert("expired".to_string(), vec![1], 100);
std::thread::sleep(Duration::from_millis(1));
assert!(store.get("expired").is_none());
}
}