use parking_lot::RwLock;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
struct CacheEntry {
expires_at: Instant,
}
#[derive(Clone)]
pub struct CredentialCache {
cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
ttl: Duration,
}
impl CredentialCache {
pub fn new(ttl: Duration) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
fn cache_key(username: &str, password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(username.as_bytes());
hasher.update(b":");
hasher.update(password.as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn is_cached(&self, username: &str, password: &str) -> bool {
let key = Self::cache_key(username, password);
let cache = self.cache.read();
if let Some(entry) = cache.get(&key)
&& entry.expires_at > Instant::now()
{
return true;
}
false
}
pub fn cache_success(&self, username: &str, password: &str) {
let key = Self::cache_key(username, password);
let entry = CacheEntry {
expires_at: Instant::now() + self.ttl,
};
let mut cache = self.cache.write();
cache.insert(key, entry);
if cache.len() > 1000 {
self.cleanup_expired_internal(&mut cache);
}
}
fn cleanup_expired_internal(&self, cache: &mut HashMap<String, CacheEntry>) {
let now = Instant::now();
cache.retain(|_, entry| entry.expires_at > now);
}
pub fn clear(&self) {
self.cache.write().clear();
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.cache.read().len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.cache.read().is_empty()
}
}
impl std::fmt::Debug for CredentialCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CredentialCache")
.field("ttl", &self.ttl)
.field("entries", &self.cache.read().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
#[test]
fn test_cache_hit() {
let cache = CredentialCache::new(Duration::from_secs(60));
assert!(!cache.is_cached("user", "password"));
cache.cache_success("user", "password");
assert!(cache.is_cached("user", "password"));
assert!(!cache.is_cached("user", "wrong_password"));
assert!(!cache.is_cached("other_user", "password"));
}
#[test]
fn test_cache_expiry() {
let cache = CredentialCache::new(Duration::from_millis(50));
cache.cache_success("user", "password");
assert!(cache.is_cached("user", "password"));
sleep(Duration::from_millis(100));
assert!(!cache.is_cached("user", "password"));
}
#[test]
fn test_cache_key_uniqueness() {
let key1 = CredentialCache::cache_key("user", "pass");
let key2 = CredentialCache::cache_key("user", "pass2");
let key3 = CredentialCache::cache_key("user2", "pass");
assert_ne!(key1, key2);
assert_ne!(key1, key3);
assert_ne!(key2, key3);
}
}