Skip to main content

vibeio_http/h2/hpack/
table.rs

1//! HPACK header tables (RFC 7541 Sections 2.3 and 4).
2
3use bytes::Bytes;
4use std::collections::VecDeque;
5
6/// The static table (RFC 7541 Appendix A): 61 immutable entries.
7/// Index 1 is `:authority`, index 61 is `www-authenticate`.
8const STATIC_TABLE: [(&[u8], &[u8]); 61] = [
9    (b":authority", b""),
10    (b":method", b"GET"),
11    (b":method", b"POST"),
12    (b":path", b"/"),
13    (b":path", b"/index.html"),
14    (b":scheme", b"http"),
15    (b":scheme", b"https"),
16    (b":status", b"200"),
17    (b":status", b"204"),
18    (b":status", b"206"),
19    (b":status", b"304"),
20    (b":status", b"400"),
21    (b":status", b"404"),
22    (b":status", b"500"),
23    (b"accept-charset", b""),
24    (b"accept-encoding", b"gzip, deflate"),
25    (b"accept-language", b""),
26    (b"accept-ranges", b""),
27    (b"accept", b""),
28    (b"access-control-allow-origin", b""),
29    (b"age", b""),
30    (b"allow", b""),
31    (b"authorization", b""),
32    (b"cache-control", b""),
33    (b"content-disposition", b""),
34    (b"content-encoding", b""),
35    (b"content-language", b""),
36    (b"content-length", b""),
37    (b"content-location", b""),
38    (b"content-range", b""),
39    (b"content-type", b""),
40    (b"cookie", b""),
41    (b"date", b""),
42    (b"etag", b""),
43    (b"expect", b""),
44    (b"expires", b""),
45    (b"from", b""),
46    (b"host", b""),
47    (b"if-match", b""),
48    (b"if-modified-since", b""),
49    (b"if-none-match", b""),
50    (b"if-range", b""),
51    (b"if-unmodified-since", b""),
52    (b"last-modified", b""),
53    (b"link", b""),
54    (b"location", b""),
55    (b"max-forwards", b""),
56    (b"proxy-authenticate", b""),
57    (b"proxy-authorization", b""),
58    (b"range", b""),
59    (b"referer", b""),
60    (b"refresh", b""),
61    (b"retry-after", b""),
62    (b"server", b""),
63    (b"set-cookie", b""),
64    (b"strict-transport-security", b""),
65    (b"transfer-encoding", b""),
66    (b"user-agent", b""),
67    (b"vary", b""),
68    (b"via", b""),
69    (b"www-authenticate", b""),
70];
71
72/// Number of entries in the static table.
73pub(crate) const STATIC_LEN: usize = 61;
74
75/// The entry-size overhead (RFC 7541 Section 4.1).
76const ENTRY_OVERHEAD: usize = 32;
77
78/// A header field name/value pair stored in or fetched from a table.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Header {
81    name: Bytes,
82    value: Bytes,
83    /// Cached RFC 7541 Section 4.1 size (overhead + name + value), so
84    /// eviction and accounting never recompute it.
85    size: usize,
86}
87
88impl Header {
89    /// Creates a header field from raw name/value bytes. Names are used
90    /// verbatim (no case normalization), which allows HTTP/2 pseudo
91    /// headers (`:method`, `:status`, ...).
92    #[inline]
93    pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
94        let name = name.into();
95        let value = value.into();
96        let size = ENTRY_OVERHEAD + name.len() + value.len();
97        Header { name, value, size }
98    }
99
100    /// Size in octets as defined in RFC 7541 Section 4.1: the sum of the
101    /// name and value lengths (without Huffman encoding) plus 32.
102    #[inline]
103    pub(crate) fn size(&self) -> usize {
104        self.size
105    }
106
107    #[inline]
108    pub fn name(&self) -> &[u8] {
109        &self.name
110    }
111
112    #[inline]
113    pub fn value(&self) -> &[u8] {
114        &self.value
115    }
116}
117
118/// The HPACK header table: the immutable static table followed by a
119/// dynamically sized FIFO (RFC 7541 Section 4).
120#[derive(Debug)]
121pub(crate) struct Table {
122    /// Dynamic entries, newest at the front. The entry at index 62 in the
123    /// combined addressing scheme is `entries[0]`.
124    entries: VecDeque<Header>,
125    /// Current combined size of the dynamic entries.
126    size: usize,
127    /// Maximum combined size the dynamic table may grow to.
128    max_size: usize,
129}
130
131impl Table {
132    #[inline]
133    pub(crate) fn new() -> Self {
134        Table::with_max_size(DEFAULT_MAX_SIZE)
135    }
136
137    /// The default dynamic-table size when no SETTINGS_HEADER_TABLE_SIZE
138    /// has been exchanged (RFC 7541 Section 4.2).
139    #[inline]
140    pub(crate) fn with_max_size(max_size: usize) -> Self {
141        Table {
142            entries: VecDeque::new(),
143            size: 0,
144            max_size,
145        }
146    }
147
148    /// Returns the entry at 1-based `index`: 1..=61 addresses the static
149    /// table, 62.. the dynamic table (newest first).
150    #[inline]
151    pub(crate) fn get(&self, index: usize) -> Option<Header> {
152        if index == 0 {
153            return None;
154        }
155        if index <= STATIC_LEN {
156            let (name, value) = STATIC_TABLE[index - 1];
157            // `from_static` is zero-copy.
158            return Some(Header::new(
159                Bytes::from_static(name),
160                Bytes::from_static(value),
161            ));
162        }
163        self.entries.get(index - STATIC_LEN - 1).cloned()
164    }
165
166    /// Number of dynamic entries.
167    #[cfg(test)]
168    #[inline]
169    pub(crate) fn dynamic_len(&self) -> usize {
170        self.entries.len()
171    }
172
173    /// 1-based index of the exact `(name, value)` entry if present:
174    /// the static table is searched first, then the dynamic table
175    /// (newest first).
176    #[inline]
177    pub(crate) fn find(&self, name: &[u8], value: &[u8]) -> Option<usize> {
178        for (i, (n, v)) in STATIC_TABLE.iter().enumerate() {
179            if *n == name && *v == value {
180                return Some(i + 1);
181            }
182        }
183        for (i, entry) in self.entries.iter().enumerate() {
184            if entry.name() == name && entry.value() == value {
185                return Some(STATIC_LEN + i + 1);
186            }
187        }
188        None
189    }
190
191    /// 1-based index of an entry with the given `name` if present: the
192    /// static table is searched first, then the dynamic table (newest
193    /// first).
194    #[inline]
195    pub(crate) fn find_name(&self, name: &[u8]) -> Option<usize> {
196        for (i, (n, _)) in STATIC_TABLE.iter().enumerate() {
197            if *n == name {
198                return Some(i + 1);
199            }
200        }
201        for (i, entry) in self.entries.iter().enumerate() {
202            if entry.name() == name {
203                return Some(STATIC_LEN + i + 1);
204            }
205        }
206        None
207    }
208
209    /// Number of static and dynamic entries combined.
210    #[cfg(test)]
211    #[inline]
212    pub(crate) fn len(&self) -> usize {
213        STATIC_LEN + self.entries.len()
214    }
215
216    /// Current combined size of the dynamic entries.
217    #[cfg(test)]
218    #[inline]
219    pub(crate) fn size(&self) -> usize {
220        self.size
221    }
222
223    #[cfg(test)]
224    #[inline]
225    pub(crate) fn max_size(&self) -> usize {
226        self.max_size
227    }
228
229    /// Changes the maximum table size, evicting entries from the end of
230    /// the dynamic table until its size is within the new limit
231    /// (RFC 7541 Section 4.3).
232    #[inline]
233    pub(crate) fn set_max_size(&mut self, max_size: usize) {
234        self.max_size = max_size;
235        while self.size > self.max_size {
236            match self.entries.pop_back() {
237                Some(entry) => self.size -= entry.size(),
238                None => break,
239            }
240        }
241    }
242
243    /// Adds an entry to the front of the dynamic table, evicting entries
244    /// from the end as needed (RFC 7541 Section 4.4). An entry larger
245    /// than the maximum size empties the table and is not added.
246    #[inline]
247    pub(crate) fn add(&mut self, header: Header) {
248        if header.size() > self.max_size {
249            self.entries.clear();
250            self.size = 0;
251            return;
252        }
253        while self.size + header.size() > self.max_size {
254            match self.entries.pop_back() {
255                Some(entry) => self.size -= entry.size(),
256                None => break,
257            }
258        }
259        self.size += header.size();
260        self.entries.push_front(header);
261    }
262}
263
264impl Default for Table {
265    #[inline]
266    fn default() -> Self {
267        Self::new()
268    }
269}
270
271/// RFC 7541 Section 4.2: the initial maximum dynamic table size is 4096
272/// octets.
273const DEFAULT_MAX_SIZE: usize = 4096;
274
275#[cfg(test)]
276mod tests {
277    use super::*;
278
279    fn header(name: &str, value: &str) -> Header {
280        Header::new(
281            Bytes::copy_from_slice(name.as_bytes()),
282            Bytes::copy_from_slice(value.as_bytes()),
283        )
284    }
285
286    #[test]
287    fn static_table_contents() {
288        assert_eq!(
289            Table::new().get(1),
290            Some(Header::new(
291                Bytes::from_static(b":authority"),
292                Bytes::from_static(b"")
293            ))
294        );
295        assert_eq!(
296            Table::new().get(2),
297            Some(Header::new(
298                Bytes::from_static(b":method"),
299                Bytes::from_static(b"GET")
300            ))
301        );
302        assert_eq!(
303            Table::new().get(16),
304            Some(Header::new(
305                Bytes::from_static(b"accept-encoding"),
306                Bytes::from_static(b"gzip, deflate")
307            ))
308        );
309        assert_eq!(
310            Table::new().get(61),
311            Some(Header::new(
312                Bytes::from_static(b"www-authenticate"),
313                Bytes::from_static(b"")
314            ))
315        );
316        assert_eq!(Table::new().get(0), None);
317    }
318
319    #[test]
320    fn entry_size_math() {
321        // 32 overhead + 4 name + 3 value.
322        assert_eq!(header("test", "abc").size(), 39);
323        assert_eq!(header("", "").size(), 32);
324    }
325
326    #[test]
327    fn add_and_fetch_order() {
328        let mut table = Table::with_max_size(200);
329        table.add(header("a", "1"));
330        table.add(header("b", "2"));
331        table.add(header("c", "3"));
332
333        // Newest entry is at index 62.
334        assert_eq!(table.get(62), Some(header("c", "3")));
335        assert_eq!(table.get(63), Some(header("b", "2")));
336        assert_eq!(table.get(64), Some(header("a", "1")));
337        assert_eq!(table.get(65), None);
338        assert_eq!(table.dynamic_len(), 3);
339        assert_eq!(table.len(), STATIC_LEN + 3);
340        assert_eq!(table.size(), 34 * 3);
341    }
342
343    #[test]
344    fn eviction_from_end() {
345        // Each entry is 34 octets; only two fit in 70.
346        let mut table = Table::with_max_size(70);
347        table.add(header("a", "1"));
348        table.add(header("b", "2"));
349        table.add(header("c", "3"));
350
351        assert_eq!(table.dynamic_len(), 2);
352        assert_eq!(table.get(62), Some(header("c", "3")));
353        assert_eq!(table.get(63), Some(header("b", "2")));
354        assert_eq!(table.get(64), None);
355        assert_eq!(table.size(), 68);
356    }
357
358    #[test]
359    fn entry_larger_than_max_empties_table() {
360        let mut table = Table::with_max_size(100);
361        table.add(header("x", "1"));
362        table.add(header("y", "2"));
363        assert_eq!(table.dynamic_len(), 2);
364
365        // 101 octets > 100 max.
366        let big = header(&"v".repeat(60), &"v".repeat(9));
367        assert_eq!(big.size(), 101);
368        table.add(big);
369
370        assert_eq!(table.dynamic_len(), 0);
371        assert_eq!(table.size(), 0);
372    }
373
374    #[test]
375    fn entry_exactly_max_size_is_added() {
376        let mut table = Table::with_max_size(101);
377        let big = header(&"v".repeat(60), &"v".repeat(9));
378        assert_eq!(big.size(), 101);
379        table.add(big);
380        assert_eq!(table.dynamic_len(), 1);
381    }
382
383    #[test]
384    fn size_update_evicts() {
385        let mut table = Table::with_max_size(200);
386        table.add(header("a", "1"));
387        table.add(header("b", "2"));
388        table.add(header("c", "3"));
389
390        table.set_max_size(70);
391        assert_eq!(table.dynamic_len(), 2);
392        assert_eq!(table.get(62), Some(header("c", "3")));
393        assert_eq!(table.size(), 68);
394
395        table.set_max_size(0);
396        assert_eq!(table.dynamic_len(), 0);
397        assert_eq!(table.size(), 0);
398
399        // Growing back to the original size keeps the table empty; new
400        // entries populate it again.
401        table.set_max_size(200);
402        assert_eq!(table.dynamic_len(), 0);
403        table.add(header("d", "4"));
404        assert_eq!(table.get(62), Some(header("d", "4")));
405    }
406
407    #[test]
408    fn max_size_tracks_requests() {
409        let mut table = Table::with_max_size(128);
410        assert_eq!(table.max_size(), 128);
411        table.set_max_size(256);
412        assert_eq!(table.max_size(), 256);
413    }
414
415    #[test]
416    fn default_max_size() {
417        assert_eq!(Table::new().max_size(), DEFAULT_MAX_SIZE);
418        assert_eq!(Table::default().max_size(), DEFAULT_MAX_SIZE);
419    }
420
421    #[test]
422    fn grows_and_reuses_slots() {
423        // The FIFO must keep working after many evictions (the ring
424        // reuses its backing slots).
425        let mut table = Table::with_max_size(200);
426        for i in 0..100 {
427            let entry = header(&format!("k{i}"), "v");
428            table.add(entry.clone());
429            assert!(table.size() <= table.max_size(), "iteration {i}");
430            assert_eq!(table.get(62), Some(entry), "iteration {i}");
431        }
432        assert_eq!(table.get(62), Some(header("k99", "v")));
433        assert_eq!(
434            table.size(),
435            table.dynamic_len() * table.get(62).unwrap().size()
436        );
437    }
438}