use std::collections::VecDeque;
use super::super::error::H2ParseError;
use super::static_table::STATIC_TABLE_MAX;
pub const MAX_TABLE_SIZE: usize = 64 * 1024;
pub const ENTRY_OVERHEAD: usize = 32;
#[derive(Debug)]
pub struct DynamicTable {
entries: VecDeque<(String, String)>,
size_octets: usize,
max_size: usize,
}
impl DynamicTable {
pub fn new(max_size: usize) -> Result<Self, H2ParseError> {
if max_size > MAX_TABLE_SIZE {
return Err(H2ParseError::HpackTableSizeOversized {
requested: max_size,
max: MAX_TABLE_SIZE,
});
}
Ok(Self {
entries: VecDeque::new(),
size_octets: 0,
max_size,
})
}
pub fn entry_size(name: &str, value: &str) -> usize {
name.len() + value.len() + ENTRY_OVERHEAD
}
pub fn lookup(&self, idx: u64) -> Option<(&str, &str)> {
if idx == 0 {
return None;
}
if idx <= STATIC_TABLE_MAX {
return super::static_table::lookup_static(idx).map(|(n, v)| (*n, *v));
}
let dyn_idx = (idx - STATIC_TABLE_MAX - 1) as usize;
self.entries
.get(dyn_idx)
.map(|(n, v)| (n.as_str(), v.as_str()))
}
pub fn insert(&mut self, name: String, value: String) {
let new_size = Self::entry_size(&name, &value);
if new_size > self.max_size {
self.entries.clear();
self.size_octets = 0;
return;
}
while self.size_octets + new_size > self.max_size {
if let Some((n, v)) = self.entries.pop_back() {
self.size_octets = self.size_octets.saturating_sub(Self::entry_size(&n, &v));
} else {
break;
}
}
self.size_octets += new_size;
self.entries.push_front((name, value));
}
pub fn update_max_size(&mut self, new_max: usize) -> Result<(), H2ParseError> {
if new_max > MAX_TABLE_SIZE {
return Err(H2ParseError::HpackTableSizeOversized {
requested: new_max,
max: MAX_TABLE_SIZE,
});
}
self.max_size = new_max;
while self.size_octets > new_max {
if let Some((n, v)) = self.entries.pop_back() {
self.size_octets = self.size_octets.saturating_sub(Self::entry_size(&n, &v));
} else {
break;
}
}
Ok(())
}
pub fn entry_count(&self) -> usize {
self.entries.len()
}
pub fn size_octets(&self) -> usize {
self.size_octets
}
#[cfg(test)]
pub fn max_size(&self) -> usize {
self.max_size
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn static_index_1_returns_authority_empty() {
let t = DynamicTable::new(4096).unwrap();
let (name, value) = t.lookup(1).unwrap();
assert_eq!(name, ":authority");
assert_eq!(value, "");
}
#[test]
fn dynamic_index_62_returns_first_inserted_entry() {
let mut t = DynamicTable::new(4096).unwrap();
t.insert(":authority".into(), "api.example.com".into());
let (name, value) = t.lookup(62).unwrap();
assert_eq!(name, ":authority");
assert_eq!(value, "api.example.com");
assert_eq!(t.entry_count(), 1);
assert_eq!(
t.size_octets(),
":authority".len() + "api.example.com".len() + 32
);
}
#[test]
fn insert_evicts_oldest_when_oversized() {
let mut t = DynamicTable::new(60).unwrap();
t.insert("a".into(), "x".into()); t.insert("b".into(), "y".into()); assert_eq!(t.entry_count(), 1);
let (name, value) = t.lookup(62).unwrap();
assert_eq!(name, "b");
assert_eq!(value, "y");
}
#[test]
fn update_max_size_zero_clears_table() {
let mut t = DynamicTable::new(4096).unwrap();
t.insert("a".into(), "b".into());
assert_eq!(t.entry_count(), 1);
t.update_max_size(0).unwrap();
assert_eq!(t.entry_count(), 0);
assert_eq!(t.size_octets(), 0);
assert_eq!(t.max_size(), 0);
}
#[test]
fn entry_size_includes_32_octet_overhead() {
assert_eq!(DynamicTable::entry_size("a", "b"), 1 + 1 + 32);
assert_eq!(DynamicTable::entry_size("", ""), 32);
assert_eq!(
DynamicTable::entry_size(":authority", "api.example.com"),
":authority".len() + "api.example.com".len() + 32
);
}
#[test]
fn oversized_max_size_rejected() {
let err = DynamicTable::new(MAX_TABLE_SIZE + 1).unwrap_err();
assert!(matches!(err, H2ParseError::HpackTableSizeOversized { .. }));
let mut t = DynamicTable::new(4096).unwrap();
let err = t.update_max_size(MAX_TABLE_SIZE + 1).unwrap_err();
assert!(matches!(err, H2ParseError::HpackTableSizeOversized { .. }));
}
#[test]
fn lookup_out_of_range_returns_none() {
let t = DynamicTable::new(4096).unwrap();
assert!(t.lookup(0).is_none());
assert!(t.lookup(62).is_none()); assert!(t.lookup(u64::MAX).is_none());
}
#[test]
fn insert_oversized_single_entry_clears_table_per_rfc_7541_4_4() {
let mut t = DynamicTable::new(60).unwrap();
t.insert("a".into(), "b".into());
assert_eq!(t.entry_count(), 1);
let big_value: String = std::iter::repeat_n('x', 100).collect();
t.insert("name".into(), big_value);
assert_eq!(t.entry_count(), 0);
assert_eq!(t.size_octets(), 0);
}
#[test]
fn lookup_static_pseudo_headers_intact() {
let t = DynamicTable::new(4096).unwrap();
assert_eq!(t.lookup(2).unwrap(), (":method", "GET"));
assert_eq!(t.lookup(7).unwrap(), (":scheme", "https"));
assert_eq!(t.lookup(38).unwrap(), ("host", ""));
}
}