use std::collections::HashMap;
use std::net::IpAddr;
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
struct CacheEntry {
hostname: String,
inserted_at: Instant,
}
#[derive(Debug)]
pub struct RdnsCache {
cache: Arc<RwLock<HashMap<IpAddr, CacheEntry>>>,
ttl: Duration,
}
impl RdnsCache {
pub fn new(ttl: Duration) -> Self {
Self {
cache: Arc::new(RwLock::new(HashMap::new())),
ttl,
}
}
pub fn with_default_ttl() -> Self {
Self::new(Duration::from_secs(3600))
}
pub fn get(&self, ip: &IpAddr) -> Option<String> {
let mut cache = self.cache.write().expect("rwlock poisoned");
if let Some(entry) = cache.get(ip) {
if entry.inserted_at.elapsed() < self.ttl {
return Some(entry.hostname.clone());
} else {
cache.remove(ip);
}
}
None
}
pub fn insert(&self, ip: IpAddr, hostname: String) {
let mut cache = self.cache.write().expect("rwlock poisoned");
cache.insert(
ip,
CacheEntry {
hostname,
inserted_at: Instant::now(),
},
);
}
pub fn len(&self) -> usize {
let cache = self.cache.read().expect("rwlock poisoned");
cache.len()
}
pub fn is_empty(&self) -> bool {
let cache = self.cache.read().expect("rwlock poisoned");
cache.is_empty()
}
pub fn clear(&self) {
let mut cache = self.cache.write().expect("rwlock poisoned");
cache.clear();
}
pub fn evict_expired(&self) {
let mut cache = self.cache.write().expect("rwlock poisoned");
let now = Instant::now();
cache.retain(|_, entry| now.duration_since(entry.inserted_at) < self.ttl);
}
}
impl Default for RdnsCache {
fn default() -> Self {
Self::with_default_ttl()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;
use std::thread;
#[test]
fn test_rdns_cache() {
let cache = RdnsCache::new(Duration::from_secs(60));
assert!(cache.is_empty(), "Cache should start empty");
assert_eq!(cache.len(), 0, "Initial cache size should be 0");
let ip1 = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
let hostname1 = "dns.google".to_string();
cache.insert(ip1, hostname1.clone());
assert_eq!(cache.len(), 1, "Cache should have 1 entry after insert");
assert!(!cache.is_empty(), "Cache should not be empty after insert");
let retrieved = cache.get(&ip1);
assert!(retrieved.is_some(), "Should find inserted entry");
assert_eq!(
retrieved.unwrap(),
hostname1,
"Should retrieve correct hostname"
);
for _ in 0..10 {
let result = cache.get(&ip1);
assert_eq!(
result.unwrap(),
hostname1,
"Cache should return consistent data"
);
}
let ip2 = IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1));
let hostname2 = "one.one.one.one".to_string();
cache.insert(ip2, hostname2.clone());
assert_eq!(cache.len(), 2, "Cache should have 2 entries");
assert_eq!(cache.get(&ip1).unwrap(), hostname1);
assert_eq!(cache.get(&ip2).unwrap(), hostname2);
let ip3 = IpAddr::V4(Ipv4Addr::new(4, 4, 4, 4));
assert!(
cache.get(&ip3).is_none(),
"Should return None for missing entry"
);
let new_hostname1 = "dns.google.com".to_string();
cache.insert(ip1, new_hostname1.clone());
assert_eq!(
cache.get(&ip1).unwrap(),
new_hostname1,
"Should return updated hostname"
);
assert_eq!(cache.len(), 2, "Size shouldn't change on update");
let ipv6 = IpAddr::V6("2001:4860:4860::8888".parse().unwrap());
let hostname_v6 = "dns.google.ipv6".to_string();
cache.insert(ipv6, hostname_v6.clone());
assert_eq!(cache.len(), 3, "Should handle IPv6 addresses");
assert_eq!(cache.get(&ipv6).unwrap(), hostname_v6);
cache.clear();
assert!(cache.is_empty(), "Cache should be empty after clear");
assert_eq!(cache.len(), 0, "Size should be 0 after clear");
assert!(cache.get(&ip1).is_none(), "Should return None after clear");
assert!(cache.get(&ip2).is_none(), "Should return None after clear");
}
#[test]
fn test_cache_expiration() {
let cache = RdnsCache::new(Duration::from_millis(50));
let ip = IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8));
cache.insert(ip, "dns.google".to_string());
assert!(cache.get(&ip).is_some());
thread::sleep(Duration::from_millis(60));
assert!(cache.get(&ip).is_none());
assert_eq!(cache.len(), 0);
}
#[test]
fn test_evict_expired() {
let cache = RdnsCache::new(Duration::from_millis(50));
cache.insert(
IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)),
"dns1.google".to_string(),
);
cache.insert(
IpAddr::V4(Ipv4Addr::new(8, 8, 4, 4)),
"dns2.google".to_string(),
);
assert_eq!(cache.len(), 2);
thread::sleep(Duration::from_millis(60));
cache.evict_expired();
assert_eq!(cache.len(), 0);
}
}