use crate::Error;
use crate::hpack::static_table::DYNAMIC_BASE;
pub const MAX_ENTRIES: usize = 64;
pub const MAX_ENTRY_LEN: usize = 128;
const ENTRY_OVERHEAD: usize = 32;
struct Entry {
name: heapless::String<MAX_ENTRY_LEN>,
value: heapless::String<MAX_ENTRY_LEN>,
}
impl Entry {
fn size(&self) -> usize {
self.name.len() + self.value.len() + ENTRY_OVERHEAD
}
}
pub struct DynamicTable {
entries: heapless::Deque<Entry, MAX_ENTRIES>,
size: usize,
capacity: usize,
}
impl DynamicTable {
pub fn new(capacity: usize) -> Self {
Self {
entries: heapless::Deque::new(),
size: 0,
capacity,
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn size(&self) -> usize {
self.size
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn set_capacity(&mut self, capacity: usize) {
self.capacity = capacity;
self.evict_to_fit(0);
}
pub fn get(&self, index: usize) -> Option<(&str, &str)> {
let offset = index.checked_sub(DYNAMIC_BASE)?;
self.entries
.iter()
.nth(offset)
.map(|e| (e.name.as_str(), e.value.as_str()))
}
pub fn insert(&mut self, name: &str, value: &str) {
let size = name.len() + value.len() + ENTRY_OVERHEAD;
self.evict_to_fit(size);
if size > self.capacity || name.len() > MAX_ENTRY_LEN || value.len() > MAX_ENTRY_LEN {
return;
}
if self.entries.is_full() {
self.evict_oldest();
}
let (Ok(name), Ok(value)) = (
heapless::String::try_from(name),
heapless::String::try_from(value),
) else {
return;
};
let entry = Entry { name, value };
self.size += entry.size();
let _ = self.entries.push_front(entry);
}
fn evict_to_fit(&mut self, incoming: usize) {
while self.size + incoming > self.capacity && !self.entries.is_empty() {
self.evict_oldest();
}
}
fn evict_oldest(&mut self) {
if let Some(entry) = self.entries.pop_back() {
self.size -= entry.size();
}
}
}
pub fn lookup(table: &DynamicTable, index: usize) -> Result<(&str, &str), Error> {
if index < DYNAMIC_BASE {
return crate::hpack::static_table::get(index).ok_or(Error::Hpack);
}
table.get(index).ok_or(Error::Hpack)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_newest_entry_is_index_62() {
let mut table = DynamicTable::new(4096);
table.insert("first", "1");
assert_eq!(table.get(62), Some(("first", "1")));
table.insert("second", "2");
assert_eq!(table.get(62), Some(("second", "2")));
assert_eq!(table.get(63), Some(("first", "1")));
assert_eq!(table.get(64), None);
}
#[test]
fn entry_size_includes_the_thirty_two_byte_overhead() {
let mut table = DynamicTable::new(4096);
table.insert("ab", "cd");
assert_eq!(table.size(), 2 + 2 + 32);
}
#[test]
fn a_full_table_evicts_the_oldest_first() {
let mut table = DynamicTable::new((1 + 1 + 32) * 2);
table.insert("a", "1");
table.insert("b", "2");
assert_eq!(table.len(), 2);
table.insert("c", "3");
assert_eq!(table.len(), 2);
assert_eq!(table.get(62), Some(("c", "3")));
assert_eq!(table.get(63), Some(("b", "2")));
assert_eq!(table.get(64), None, "the oldest entry must be gone");
}
#[test]
fn an_entry_larger_than_the_table_empties_it_and_is_dropped() {
let mut table = DynamicTable::new(64);
table.insert("a", "1");
assert_eq!(table.len(), 1);
let mut huge = heapless::String::<128>::new();
for _ in 0..100 {
huge.push('x').unwrap();
}
table.insert("enormous", &huge);
assert!(table.is_empty());
assert_eq!(table.get(62), None);
}
#[test]
fn shrinking_the_capacity_evicts_immediately() {
let mut table = DynamicTable::new(4096);
table.insert("a", "1");
table.insert("b", "2");
table.set_capacity(34);
assert_eq!(table.len(), 1);
assert_eq!(table.get(62), Some(("b", "2")));
table.set_capacity(0);
assert!(table.is_empty());
}
#[test]
fn lookup_spans_both_tables_and_refuses_what_it_cannot_resolve() {
let mut table = DynamicTable::new(4096);
table.insert("custom", "value");
assert_eq!(lookup(&table, 2).unwrap(), (":method", "GET"));
assert_eq!(lookup(&table, 62).unwrap(), ("custom", "value"));
assert_eq!(lookup(&table, 63), Err(Error::Hpack));
assert_eq!(lookup(&table, 0), Err(Error::Hpack));
}
#[test]
fn the_entry_count_cap_holds_even_when_the_byte_budget_would_allow_more() {
let mut table = DynamicTable::new(1_000_000);
for i in 0..MAX_ENTRIES + 10 {
let mut value = heapless::String::<8>::new();
let _ = core::fmt::Write::write_fmt(&mut value, format_args!("{i}"));
table.insert("k", &value);
}
assert_eq!(table.len(), MAX_ENTRIES);
}
}