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/// Fixed number of entries. This represents every entry that fits the
27/// connection's advertised 256-byte table (32 bytes of overhead per entry).
28pub const MAX_ENTRIES: usize = 8;
29
30/// A single field can use all 256 advertised bytes minus entry overhead.
31pub const MAX_ENTRY_LEN: usize = 224;
32
33/// RFC 7541 section 4.1: an entry's size is its name plus its value plus 32
34/// bytes of assumed overhead. The constant is part of the wire protocol — both
35/// ends must compute the same size or they evict at different moments and the
36/// tables diverge.
37const 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
50/// The decoder's view of the table the peer is building.
51pub struct DynamicTable {
52    /// Newest first, so index 62 is `entries[0]`.
53    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    /// Apply a dynamic table size update from the peer.
86    ///
87    /// Shrinking evicts immediately; the peer has already done the same, so the
88    /// two tables stay in step.
89    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    /// 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        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    /// Insert an indexed header, evicting only as required by the peer's byte
114    /// capacity. A capacity above our fixed storage is supported only while its
115    /// entries fit. Exceeding storage invalidates dynamic lookups until an
116    /// explicit table clear; silently evicting would shift the peer's indices.
117    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
156/// Resolve an index against the static table first, then the dynamic one.
157pub 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        // Inserting pushes the previous entry *up* an index, which is the whole
175        // reason indices are computed rather than stored.
176        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        // Both ends must compute this identically or they evict at different
185        // moments and every index after that diverges.
186        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        // Capacity for exactly two of these.
194        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        // RFC 7541 section 4.4. Getting this wrong leaves a phantom entry and
209        // shifts every later index.
210        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        // An index nobody defined must be an error. Guessing would decode later
242        // headers as different headers, plausibly and silently.
243        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        // Exceeding fixed storage must fail closed, without inventing peer
250        // evictions that change the meaning of a dynamic index.
251        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}