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