micro_h2/hpack/
dynamic.rs1use crate::Error;
24use crate::hpack::static_table::DYNAMIC_BASE;
25
26pub const MAX_ENTRIES: usize = 64;
33
34pub const MAX_ENTRY_LEN: usize = 128;
41
42const ENTRY_OVERHEAD: usize = 32;
47
48struct Entry {
49 name: heapless::String<MAX_ENTRY_LEN>,
50 value: heapless::String<MAX_ENTRY_LEN>,
51}
52
53impl Entry {
54 fn size(&self) -> usize {
55 self.name.len() + self.value.len() + ENTRY_OVERHEAD
56 }
57}
58
59pub struct DynamicTable {
61 entries: heapless::Deque<Entry, MAX_ENTRIES>,
63 size: usize,
64 capacity: usize,
65}
66
67impl DynamicTable {
68 pub fn new(capacity: usize) -> Self {
69 Self {
70 entries: heapless::Deque::new(),
71 size: 0,
72 capacity,
73 }
74 }
75
76 pub fn len(&self) -> usize {
77 self.entries.len()
78 }
79
80 pub fn is_empty(&self) -> bool {
81 self.entries.is_empty()
82 }
83
84 pub fn size(&self) -> usize {
85 self.size
86 }
87
88 pub fn capacity(&self) -> usize {
89 self.capacity
90 }
91
92 pub fn set_capacity(&mut self, capacity: usize) {
97 self.capacity = capacity;
98 self.evict_to_fit(0);
99 }
100
101 pub fn get(&self, index: usize) -> Option<(&str, &str)> {
103 let offset = index.checked_sub(DYNAMIC_BASE)?;
104 self.entries
105 .iter()
106 .nth(offset)
107 .map(|e| (e.name.as_str(), e.value.as_str()))
108 }
109
110 pub fn insert(&mut self, name: &str, value: &str) {
115 let size = name.len() + value.len() + ENTRY_OVERHEAD;
116 self.evict_to_fit(size);
117
118 if size > self.capacity || name.len() > MAX_ENTRY_LEN || value.len() > MAX_ENTRY_LEN {
119 return;
120 }
121 if self.entries.is_full() {
127 self.evict_oldest();
128 }
129 let (Ok(name), Ok(value)) = (
130 heapless::String::try_from(name),
131 heapless::String::try_from(value),
132 ) else {
133 return;
134 };
135 let entry = Entry { name, value };
136 self.size += entry.size();
137 let _ = self.entries.push_front(entry);
138 }
139
140 fn evict_to_fit(&mut self, incoming: usize) {
141 while self.size + incoming > self.capacity && !self.entries.is_empty() {
142 self.evict_oldest();
143 }
144 }
145
146 fn evict_oldest(&mut self) {
147 if let Some(entry) = self.entries.pop_back() {
148 self.size -= entry.size();
149 }
150 }
151}
152
153pub fn lookup(table: &DynamicTable, index: usize) -> Result<(&str, &str), Error> {
155 if index < DYNAMIC_BASE {
156 return crate::hpack::static_table::get(index).ok_or(Error::Hpack);
157 }
158 table.get(index).ok_or(Error::Hpack)
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn the_newest_entry_is_index_62() {
167 let mut table = DynamicTable::new(4096);
168 table.insert("first", "1");
169 assert_eq!(table.get(62), Some(("first", "1")));
170
171 table.insert("second", "2");
174 assert_eq!(table.get(62), Some(("second", "2")));
175 assert_eq!(table.get(63), Some(("first", "1")));
176 assert_eq!(table.get(64), None);
177 }
178
179 #[test]
180 fn entry_size_includes_the_thirty_two_byte_overhead() {
181 let mut table = DynamicTable::new(4096);
184 table.insert("ab", "cd");
185 assert_eq!(table.size(), 2 + 2 + 32);
186 }
187
188 #[test]
189 fn a_full_table_evicts_the_oldest_first() {
190 let mut table = DynamicTable::new((1 + 1 + 32) * 2);
192 table.insert("a", "1");
193 table.insert("b", "2");
194 assert_eq!(table.len(), 2);
195
196 table.insert("c", "3");
197 assert_eq!(table.len(), 2);
198 assert_eq!(table.get(62), Some(("c", "3")));
199 assert_eq!(table.get(63), Some(("b", "2")));
200 assert_eq!(table.get(64), None, "the oldest entry must be gone");
201 }
202
203 #[test]
204 fn an_entry_larger_than_the_table_empties_it_and_is_dropped() {
205 let mut table = DynamicTable::new(64);
208 table.insert("a", "1");
209 assert_eq!(table.len(), 1);
210
211 let mut huge = heapless::String::<128>::new();
212 for _ in 0..100 {
213 huge.push('x').unwrap();
214 }
215 table.insert("enormous", &huge);
216 assert!(table.is_empty());
217 assert_eq!(table.get(62), None);
218 }
219
220 #[test]
221 fn shrinking_the_capacity_evicts_immediately() {
222 let mut table = DynamicTable::new(4096);
223 table.insert("a", "1");
224 table.insert("b", "2");
225 table.set_capacity(34);
226 assert_eq!(table.len(), 1);
227 assert_eq!(table.get(62), Some(("b", "2")));
228 table.set_capacity(0);
229 assert!(table.is_empty());
230 }
231
232 #[test]
233 fn lookup_spans_both_tables_and_refuses_what_it_cannot_resolve() {
234 let mut table = DynamicTable::new(4096);
235 table.insert("custom", "value");
236 assert_eq!(lookup(&table, 2).unwrap(), (":method", "GET"));
237 assert_eq!(lookup(&table, 62).unwrap(), ("custom", "value"));
238 assert_eq!(lookup(&table, 63), Err(Error::Hpack));
241 assert_eq!(lookup(&table, 0), Err(Error::Hpack));
242 }
243
244 #[test]
245 fn the_entry_count_cap_holds_even_when_the_byte_budget_would_allow_more() {
246 let mut table = DynamicTable::new(1_000_000);
249 for i in 0..MAX_ENTRIES + 10 {
250 let mut value = heapless::String::<8>::new();
251 let _ = core::fmt::Write::write_fmt(&mut value, format_args!("{i}"));
252 table.insert("k", &value);
253 }
254 assert_eq!(table.len(), MAX_ENTRIES);
255 }
256}