use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Default)]
pub struct NonceStore {
inner: Mutex<HashMap<String, Instant>>,
}
impl NonceStore {
pub fn new() -> Self {
Self::default()
}
pub fn register(&self, nonce: impl Into<String>, ttl: Duration) {
let mut map = self.lock();
let now = Instant::now();
map.retain(|_, expiry| *expiry > now);
map.insert(nonce.into(), now + ttl);
}
pub fn consume(&self, nonce: &str) -> bool {
let mut map = self.lock();
match map.remove(nonce) {
Some(expiry) => expiry > Instant::now(),
None => false,
}
}
pub fn len(&self) -> usize {
let mut map = self.lock();
let now = Instant::now();
map.retain(|_, expiry| *expiry > now);
map.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Instant>> {
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn consume_succeeds_once_then_rejects_replay() {
let store = NonceStore::new();
store.register("abc", Duration::from_secs(300));
assert!(store.consume("abc"), "first use accepted");
assert!(!store.consume("abc"), "replay rejected");
}
#[test]
fn unknown_nonce_rejected() {
let store = NonceStore::new();
assert!(!store.consume("never-issued"));
}
#[test]
fn expired_nonce_rejected() {
let store = NonceStore::new();
store.register("stale", Duration::ZERO);
assert!(!store.consume("stale"));
}
#[test]
fn len_counts_live_and_evicts_expired() {
let store = NonceStore::new();
store.register("live", Duration::from_secs(300));
store.register("dead", Duration::ZERO);
assert_eq!(store.len(), 1);
assert!(!store.is_empty());
assert!(store.consume("live"));
assert!(store.is_empty());
}
}