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
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/// The decoder's view of the table the peer is building.
52#[derive(Clone)]
53pub struct DynamicTable {
54    /// Newest first, so index 62 is `entries[0]`.
55    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    /// Apply a dynamic table size update from the peer.
88    ///
89    /// Shrinking evicts immediately; the peer has already done the same, so the
90    /// two tables stay in step.
91    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    /// Look up a 1-based HPACK index, which for the dynamic table starts at 62.
104    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    /// Insert an indexed header, evicting only as required by the peer's byte
116    /// capacity. A capacity above our fixed storage is supported only while its
117    /// entries fit. Exceeding storage invalidates dynamic lookups until an
118    /// explicit table clear; silently evicting would shift the peer's indices.
119    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
158/// Resolve an index against the static table first, then the dynamic one.
159pub 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        // Inserting pushes the previous entry *up* an index, which is the whole
177        // reason indices are computed rather than stored.
178        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        // Both ends must compute this identically or they evict at different
187        // moments and every index after that diverges.
188        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        // Capacity for exactly two of these.
196        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        // RFC 7541 section 4.4. Getting this wrong leaves a phantom entry and
211        // shifts every later index.
212        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        // An index nobody defined must be an error. Guessing would decode later
244        // headers as different headers, plausibly and silently.
245        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        // Exceeding fixed storage must fail closed, without inventing peer
252        // evictions that change the meaning of a dynamic index.
253        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}