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