Skip to main content

micro_h2/hpack/
dynamic.rs

1//! The HPACK dynamic table.
2//!
3//! # Why this cannot be skipped
4//!
5//! It is tempting to ignore: a client could decline to *use* the dynamic table
6//! in the headers it sends, and never add an entry of its own. But the table is
7//! shared state built from the peer's stream, and the peer decides what goes in
8//! it. Every "literal with incremental indexing" the server sends inserts an
9//! entry, and every later reference is an index into a table that only exists if
10//! we kept it.
11//!
12//! Skipping an insertion does not produce a missing header — it shifts every
13//! subsequent index by one, so headers decode as *other headers*, plausibly and
14//! silently, for the rest of the connection. That is why decoding an unknown
15//! index is an error here rather than a best-effort guess.
16//!
17//! # Shape
18//!
19//! A ring of fixed capacity. Entry 62 is the newest, and inserting evicts from
20//! the far end — which is the opposite of what a naive `Vec` push does, and the
21//! reason indices are computed rather than stored.
22
23use crate::Error;
24use crate::hpack::static_table::DYNAMIC_BASE;
25
26/// How many entries the table can hold.
27///
28/// The protocol bounds the table by *bytes*, not entries, so this is a second,
29/// independent cap. It exists because there is no allocator: `SETTINGS` can
30/// raise the byte budget beyond what a fixed array holds, and running out of
31/// slots must be a bounded eviction rather than a panic.
32pub const MAX_ENTRIES: usize = 64;
33
34/// The longest header name or value the table will store.
35///
36/// A longer one is not an error — it simply is not remembered, exactly as if the
37/// encoder had sent it without indexing. Entries are only ever a compression
38/// hint, so forgetting one costs bytes and nothing else, while refusing the
39/// connection over a long cookie would cost the connection.
40pub const MAX_ENTRY_LEN: usize = 128;
41
42/// RFC 7541 section 4.1: an entry's size is its name plus its value plus 32
43/// bytes of assumed overhead. The constant is part of the wire protocol — both
44/// ends must compute the same size or they evict at different moments and the
45/// tables diverge.
46const 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
59/// The decoder's view of the table the peer is building.
60pub struct DynamicTable {
61    /// Newest first, so index 62 is `entries[0]`.
62    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    /// Apply a dynamic table size update from the peer.
93    ///
94    /// Shrinking evicts immediately; the peer has already done the same, so the
95    /// two tables stay in step.
96    pub fn set_capacity(&mut self, capacity: usize) {
97        self.capacity = capacity;
98        self.evict_to_fit(0);
99    }
100
101    /// Look up a 1-based HPACK index, which for the dynamic table starts at 62.
102    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    /// Insert a header the peer marked for indexing.
111    ///
112    /// Never fails: an entry too large for the table evicts everything and is
113    /// then dropped, which is exactly what RFC 7541 section 4.4 requires.
114    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        // The length cap above is ours rather than the protocol's, so an
122        // over-long header is simply not remembered. That is safe because an
123        // entry we decline is one we could never have resolved an index into
124        // anyway — and if the peer does index it, `lookup` fails loudly instead
125        // of returning the wrong header.
126        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
153/// Resolve an index against the static table first, then the dynamic one.
154pub 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        // Inserting pushes the previous entry *up* an index, which is the whole
172        // reason indices are computed rather than stored.
173        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        // Both ends must compute this identically or they evict at different
182        // moments and every index after that diverges.
183        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        // Capacity for exactly two of these.
191        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        // RFC 7541 section 4.4. Getting this wrong leaves a phantom entry and
206        // shifts every later index.
207        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        // An index nobody defined must be an error. Guessing would decode later
239        // headers as different headers, plausibly and silently.
240        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        // The protocol bounds by bytes; with no allocator we also bound by
247        // slots, and running out must evict rather than panic.
248        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}