use std::hash::{DefaultHasher, Hasher};
#[derive(Debug)]
pub(in crate::headers) struct RecentPairs {
hashes: Box<[u32]>,
cursor: usize,
}
impl RecentPairs {
pub(in crate::headers) fn with_size(size: usize) -> Self {
let size = size.max(1);
Self {
hashes: vec![0; size].into_boxed_slice(),
cursor: 0,
}
}
pub(in crate::headers) fn hash(name: &[u8], value: &[u8]) -> u32 {
let mut h = DefaultHasher::new();
h.write(name);
h.write_u8(0xff);
h.write(value);
#[allow(clippy::cast_possible_truncation)]
let truncated = h.finish() as u32;
truncated
}
pub(in crate::headers) fn seen(&self, hash: u32) -> bool {
self.hashes.contains(&hash)
}
pub(in crate::headers) fn remember(&mut self, hash: u32) {
self.hashes[self.cursor] = hash;
self.cursor = (self.cursor + 1) % self.hashes.len();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn check(p: &mut RecentPairs, name: &[u8], value: &[u8]) -> bool {
let h = RecentPairs::hash(name, value);
let seen = p.seen(h);
p.remember(h);
seen
}
#[test]
fn first_sighting_is_unseen_second_is_seen() {
let mut p = RecentPairs::with_size(12);
assert!(!check(&mut p, b"x-a", b"1"));
assert!(check(&mut p, b"x-a", b"1"));
}
#[test]
fn different_values_under_same_name_are_independent() {
let mut p = RecentPairs::with_size(12);
assert!(!check(&mut p, b"x-a", b"1"));
assert!(!check(&mut p, b"x-a", b"2"));
assert!(check(&mut p, b"x-a", b"1"));
assert!(check(&mut p, b"x-a", b"2"));
}
#[test]
fn nameval_separator_prevents_concat_collision() {
let mut p = RecentPairs::with_size(12);
assert!(!check(&mut p, b"x-ab", b"c"));
assert!(!check(&mut p, b"x-a", b"bc"));
}
#[test]
fn seen_does_not_mutate_the_ring() {
let mut p = RecentPairs::with_size(12);
let h1 = RecentPairs::hash(b"x-a", b"1");
p.remember(h1);
let h2 = RecentPairs::hash(b"x-b", b"1");
let _ = p.seen(h2);
let _ = p.seen(h2);
let _ = p.seen(h2);
assert!(p.seen(h1));
assert!(!p.seen(h2));
}
#[test]
fn ring_evicts_oldest_after_full_cycle() {
let ring_size = 12;
let mut p = RecentPairs::with_size(ring_size);
assert!(!check(&mut p, b"x-target", b"v"));
for i in 0..ring_size {
let name = format!("pad-{i}");
assert!(!check(&mut p, name.as_bytes(), b"v"));
}
assert!(!check(&mut p, b"x-target", b"v"));
}
#[test]
fn with_size_clamps_zero_to_one() {
let mut p = RecentPairs::with_size(0);
assert_eq!(p.hashes.len(), 1);
let h = RecentPairs::hash(b"x", b"y");
p.remember(h);
assert!(p.seen(h));
}
}