Skip to main content

zincio_http/h2/hpack/
table.rs

1//! HPACK header tables (RFC 7541 Sections 2.3 and 4).
2
3use bytes::Bytes;
4use rustc_hash::FxHashMap;
5use std::collections::VecDeque;
6
7/// Perfect-hash lookup of the static table by exact `(name, value)`. Built once
8/// at compile time, so `find` never scans the 61-entry table (RFC 7541
9/// Appendix A) on the hot path.
10static STATIC_EXACT: phf::Map<(&[u8], &[u8]), usize> = phf::phf_map! {
11    (b":authority", b"") => 1,
12    (b":method", b"GET") => 2,
13    (b":method", b"POST") => 3,
14    (b":path", b"/") => 4,
15    (b":path", b"/index.html") => 5,
16    (b":scheme", b"http") => 6,
17    (b":scheme", b"https") => 7,
18    (b":status", b"200") => 8,
19    (b":status", b"204") => 9,
20    (b":status", b"206") => 10,
21    (b":status", b"304") => 11,
22    (b":status", b"400") => 12,
23    (b":status", b"404") => 13,
24    (b":status", b"500") => 14,
25    (b"accept-charset", b"") => 15,
26    (b"accept-encoding", b"gzip, deflate") => 16,
27    (b"accept-language", b"") => 17,
28    (b"accept-ranges", b"") => 18,
29    (b"accept", b"") => 19,
30    (b"access-control-allow-origin", b"") => 20,
31    (b"age", b"") => 21,
32    (b"allow", b"") => 22,
33    (b"authorization", b"") => 23,
34    (b"cache-control", b"") => 24,
35    (b"content-disposition", b"") => 25,
36    (b"content-encoding", b"") => 26,
37    (b"content-language", b"") => 27,
38    (b"content-length", b"") => 28,
39    (b"content-location", b"") => 29,
40    (b"content-range", b"") => 30,
41    (b"content-type", b"") => 31,
42    (b"cookie", b"") => 32,
43    (b"date", b"") => 33,
44    (b"etag", b"") => 34,
45    (b"expect", b"") => 35,
46    (b"expires", b"") => 36,
47    (b"from", b"") => 37,
48    (b"host", b"") => 38,
49    (b"if-match", b"") => 39,
50    (b"if-modified-since", b"") => 40,
51    (b"if-none-match", b"") => 41,
52    (b"if-range", b"") => 42,
53    (b"if-unmodified-since", b"") => 43,
54    (b"last-modified", b"") => 44,
55    (b"link", b"") => 45,
56    (b"location", b"") => 46,
57    (b"max-forwards", b"") => 47,
58    (b"proxy-authenticate", b"") => 48,
59    (b"proxy-authorization", b"") => 49,
60    (b"range", b"") => 50,
61    (b"referer", b"") => 51,
62    (b"refresh", b"") => 52,
63    (b"retry-after", b"") => 53,
64    (b"server", b"") => 54,
65    (b"set-cookie", b"") => 55,
66    (b"strict-transport-security", b"") => 56,
67    (b"transfer-encoding", b"") => 57,
68    (b"user-agent", b"") => 58,
69    (b"vary", b"") => 59,
70    (b"via", b"") => 60,
71    (b"www-authenticate", b"") => 61,
72};
73
74/// Perfect-hash lookup of the static table by name alone, mapping each name to
75/// its lowest-indexed entry (matching the original left-to-right scan).
76static STATIC_NAME: phf::Map<&[u8], usize> = phf::phf_map! {
77    b":authority" => 1,
78    b":method" => 2,
79    b":path" => 4,
80    b":scheme" => 6,
81    b":status" => 8,
82    b"accept-charset" => 15,
83    b"accept-encoding" => 16,
84    b"accept-language" => 17,
85    b"accept-ranges" => 18,
86    b"accept" => 19,
87    b"access-control-allow-origin" => 20,
88    b"age" => 21,
89    b"allow" => 22,
90    b"authorization" => 23,
91    b"cache-control" => 24,
92    b"content-disposition" => 25,
93    b"content-encoding" => 26,
94    b"content-language" => 27,
95    b"content-length" => 28,
96    b"content-location" => 29,
97    b"content-range" => 30,
98    b"content-type" => 31,
99    b"cookie" => 32,
100    b"date" => 33,
101    b"etag" => 34,
102    b"expect" => 35,
103    b"expires" => 36,
104    b"from" => 37,
105    b"host" => 38,
106    b"if-match" => 39,
107    b"if-modified-since" => 40,
108    b"if-none-match" => 41,
109    b"if-range" => 42,
110    b"if-unmodified-since" => 43,
111    b"last-modified" => 44,
112    b"link" => 45,
113    b"location" => 46,
114    b"max-forwards" => 47,
115    b"proxy-authenticate" => 48,
116    b"proxy-authorization" => 49,
117    b"range" => 50,
118    b"referer" => 51,
119    b"refresh" => 52,
120    b"retry-after" => 53,
121    b"server" => 54,
122    b"set-cookie" => 55,
123    b"strict-transport-security" => 56,
124    b"transfer-encoding" => 57,
125    b"user-agent" => 58,
126    b"vary" => 59,
127    b"via" => 60,
128    b"www-authenticate" => 61,
129};
130
131/// Composite `(name, value)` key for the dynamic exact-match map. `Bytes` are
132/// refcount-bumped clones of the stored header, so lookups never allocate.
133#[derive(Clone, Debug, Hash, PartialEq, Eq)]
134struct NameValue(Bytes, Bytes);
135
136/// The static table (RFC 7541 Appendix A): 61 immutable entries.
137/// Index 1 is `:authority`, index 61 is `www-authenticate`.
138const STATIC_TABLE: [(&[u8], &[u8]); 61] = [
139    (b":authority", b""),
140    (b":method", b"GET"),
141    (b":method", b"POST"),
142    (b":path", b"/"),
143    (b":path", b"/index.html"),
144    (b":scheme", b"http"),
145    (b":scheme", b"https"),
146    (b":status", b"200"),
147    (b":status", b"204"),
148    (b":status", b"206"),
149    (b":status", b"304"),
150    (b":status", b"400"),
151    (b":status", b"404"),
152    (b":status", b"500"),
153    (b"accept-charset", b""),
154    (b"accept-encoding", b"gzip, deflate"),
155    (b"accept-language", b""),
156    (b"accept-ranges", b""),
157    (b"accept", b""),
158    (b"access-control-allow-origin", b""),
159    (b"age", b""),
160    (b"allow", b""),
161    (b"authorization", b""),
162    (b"cache-control", b""),
163    (b"content-disposition", b""),
164    (b"content-encoding", b""),
165    (b"content-language", b""),
166    (b"content-length", b""),
167    (b"content-location", b""),
168    (b"content-range", b""),
169    (b"content-type", b""),
170    (b"cookie", b""),
171    (b"date", b""),
172    (b"etag", b""),
173    (b"expect", b""),
174    (b"expires", b""),
175    (b"from", b""),
176    (b"host", b""),
177    (b"if-match", b""),
178    (b"if-modified-since", b""),
179    (b"if-none-match", b""),
180    (b"if-range", b""),
181    (b"if-unmodified-since", b""),
182    (b"last-modified", b""),
183    (b"link", b""),
184    (b"location", b""),
185    (b"max-forwards", b""),
186    (b"proxy-authenticate", b""),
187    (b"proxy-authorization", b""),
188    (b"range", b""),
189    (b"referer", b""),
190    (b"refresh", b""),
191    (b"retry-after", b""),
192    (b"server", b""),
193    (b"set-cookie", b""),
194    (b"strict-transport-security", b""),
195    (b"transfer-encoding", b""),
196    (b"user-agent", b""),
197    (b"vary", b""),
198    (b"via", b""),
199    (b"www-authenticate", b""),
200];
201
202/// Number of entries in the static table.
203pub(crate) const STATIC_LEN: usize = 61;
204
205/// Below this many dynamic entries, `find`/`find_name` use a linear scan
206/// (cheap early exits for common headers); at or above it they use the
207/// perfect-hash static maps plus the hash-map dynamic index. Tiny tables
208/// favour the scan; large tables favour the hash map.
209const HYBRID_THRESHOLD: usize = 32;
210
211/// The entry-size overhead (RFC 7541 Section 4.1).
212const ENTRY_OVERHEAD: usize = 32;
213
214/// A header field name/value pair stored in or fetched from a table.
215#[derive(Debug, Clone, PartialEq, Eq)]
216pub struct Header {
217    name: Bytes,
218    value: Bytes,
219    /// Cached RFC 7541 Section 4.1 size (overhead + name + value), so
220    /// eviction and accounting never recompute it.
221    size: usize,
222}
223
224impl Header {
225    /// Creates a header field from raw name/value bytes. Names are used
226    /// verbatim (no case normalization), which allows HTTP/2 pseudo
227    /// headers (`:method`, `:status`, ...).
228    #[inline]
229    pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
230        let name = name.into();
231        let value = value.into();
232        let size = ENTRY_OVERHEAD + name.len() + value.len();
233        Header { name, value, size }
234    }
235
236    /// Size in octets as defined in RFC 7541 Section 4.1: the sum of the
237    /// name and value lengths (without Huffman encoding) plus 32.
238    #[inline]
239    pub(crate) fn size(&self) -> usize {
240        self.size
241    }
242
243    #[inline]
244    pub fn name(&self) -> &[u8] {
245        &self.name
246    }
247
248    #[inline]
249    pub fn value(&self) -> &[u8] {
250        &self.value
251    }
252
253    /// Owned `Bytes` view of the name (a refcount bump, no copy).
254    #[inline]
255    pub(crate) fn name_bytes(&self) -> &Bytes {
256        &self.name
257    }
258
259    /// Owned `Bytes` view of the value (a refcount bump, no copy).
260    #[inline]
261    pub(crate) fn value_bytes(&self) -> &Bytes {
262        &self.value
263    }
264}
265
266/// The HPACK header table: the immutable static table followed by a
267/// dynamically sized FIFO (RFC 7541 Section 4).
268#[derive(Debug)]
269pub(crate) struct Table {
270    /// Dynamic entries, newest at the front. The entry at index 62 in the
271    /// combined addressing scheme is `entries[0]`.
272    entries: VecDeque<Header>,
273    /// Current combined size of the dynamic entries.
274    size: usize,
275    /// Maximum combined size the dynamic table may grow to.
276    max_size: usize,
277    /// Exact `(name, value)` -> combined index for the dynamic entries. Always
278    /// holds the newest index of each pair (RFC 7541, newest first).
279    exact: FxHashMap<NameValue, usize>,
280    /// Name -> combined index for the dynamic entries, holding the newest
281    /// index of each name.
282    name: FxHashMap<Bytes, usize>,
283    /// When false (decoder side), the lookup maps are never built or
284    /// consulted: the decoder resolves by index and never calls `find`, so
285    /// maintaining them would be pure overhead.
286    maintain_maps: bool,
287}
288
289impl Table {
290    #[inline]
291    pub(crate) fn new() -> Self {
292        Table::with_max_size(DEFAULT_MAX_SIZE)
293    }
294
295    /// The default dynamic-table size when no SETTINGS_HEADER_TABLE_SIZE
296    /// has been exchanged (RFC 7541 Section 4.2).
297    #[inline]
298    pub(crate) fn with_max_size(max_size: usize) -> Self {
299        Table {
300            entries: VecDeque::new(),
301            size: 0,
302            max_size,
303            exact: FxHashMap::default(),
304            name: FxHashMap::default(),
305            maintain_maps: true,
306        }
307    }
308
309    /// Like [`Table::with_max_size`] but without lookup-map maintenance,
310    /// for the decoder which resolves entries by index and never queries
311    /// the maps.
312    #[inline]
313    pub(crate) fn with_max_size_no_maps(max_size: usize) -> Self {
314        Table {
315            entries: VecDeque::new(),
316            size: 0,
317            max_size,
318            exact: FxHashMap::default(),
319            name: FxHashMap::default(),
320            maintain_maps: false,
321        }
322    }
323
324    /// Returns the entry at 1-based `index`: 1..=61 addresses the static
325    /// table, 62.. the dynamic table (newest first).
326    #[inline]
327    pub(crate) fn get(&self, index: usize) -> Option<Header> {
328        if index == 0 {
329            return None;
330        }
331        if index <= STATIC_LEN {
332            let (name, value) = STATIC_TABLE[index - 1];
333            // `from_static` is zero-copy.
334            return Some(Header::new(
335                Bytes::from_static(name),
336                Bytes::from_static(value),
337            ));
338        }
339        self.entries.get(index - STATIC_LEN - 1).cloned()
340    }
341
342    /// Number of dynamic entries.
343    #[cfg(test)]
344    #[inline]
345    pub(crate) fn dynamic_len(&self) -> usize {
346        self.entries.len()
347    }
348
349    /// 1-based index of the exact `(name, value)` entry if present:
350    /// the static table is searched first, then the dynamic table
351    /// (newest first).
352    #[inline]
353    pub(crate) fn find(&self, name: &Bytes, value: &Bytes) -> Option<usize> {
354        if self.maintain_maps && self.entries.len() > HYBRID_THRESHOLD {
355            if let Some(&index) = STATIC_EXACT.get(&(name.as_ref(), value.as_ref())) {
356                return Some(index);
357            }
358            return self
359                .exact
360                .get(&NameValue(name.clone(), value.clone()))
361                .copied();
362        }
363        // Linear fallback: static table first (lowest index), then dynamic
364        // (newest first). Matches the original scan for tiny tables.
365        let name_ref = name.as_ref();
366        let value_ref = value.as_ref();
367        for (i, entry) in STATIC_TABLE.iter().enumerate() {
368            if entry.0 == name_ref && entry.1 == value_ref {
369                return Some(i + 1);
370            }
371        }
372        for (i, header) in self.entries.iter().enumerate() {
373            if header.name() == name_ref && header.value() == value_ref {
374                return Some(STATIC_LEN + 1 + i);
375            }
376        }
377        None
378    }
379
380    /// 1-based index of an entry with the given `name` if present: the
381    /// static table is searched first, then the dynamic table (newest
382    /// first).
383    #[inline]
384    pub(crate) fn find_name(&self, name: &Bytes) -> Option<usize> {
385        if self.maintain_maps && self.entries.len() > HYBRID_THRESHOLD {
386            if let Some(&index) = STATIC_NAME.get(name.as_ref()) {
387                return Some(index);
388            }
389            return self.name.get(name.as_ref()).copied();
390        }
391        let name_ref = name.as_ref();
392        for (i, entry) in STATIC_TABLE.iter().enumerate() {
393            if entry.0 == name_ref {
394                return Some(i + 1);
395            }
396        }
397        for (i, header) in self.entries.iter().enumerate() {
398            if header.name() == name_ref {
399                return Some(STATIC_LEN + 1 + i);
400            }
401        }
402        None
403    }
404
405    /// Number of static and dynamic entries combined.
406    #[cfg(test)]
407    #[inline]
408    pub(crate) fn len(&self) -> usize {
409        STATIC_LEN + self.entries.len()
410    }
411
412    /// Current combined size of the dynamic entries.
413    #[cfg(test)]
414    #[inline]
415    pub(crate) fn size(&self) -> usize {
416        self.size
417    }
418
419    #[cfg(test)]
420    #[inline]
421    pub(crate) fn max_size(&self) -> usize {
422        self.max_size
423    }
424
425    /// Changes the maximum table size, evicting entries from the end of
426    /// the dynamic table until its size is within the new limit
427    /// (RFC 7541 Section 4.3).
428    #[inline]
429    pub(crate) fn set_max_size(&mut self, max_size: usize) {
430        self.max_size = max_size;
431        while self.size > self.max_size {
432            match self.entries.pop_back() {
433                Some(entry) => {
434                    self.size -= entry.size();
435                    let combined = STATIC_LEN + self.entries.len() + 1;
436                    self.evict_entry(&entry, combined);
437                }
438                None => break,
439            }
440        }
441    }
442
443    /// Drops `entry` from the lookup maps if it is still the stored newest
444    /// match for its key. Back-eviction only ever removes the oldest entry,
445    /// so a map value equals `combined` only when `entry` was the sole
446    /// occurrence of its key in the table.
447    #[inline]
448    fn evict_entry(&mut self, entry: &Header, combined: usize) {
449        if !self.maintain_maps {
450            return;
451        }
452        if self.name.get(entry.name()).copied() == Some(combined) {
453            self.name.remove(entry.name());
454        }
455        let key = NameValue(entry.name_bytes().clone(), entry.value_bytes().clone());
456        if self.exact.get(&key).copied() == Some(combined) {
457            self.exact.remove(&key);
458        }
459    }
460
461    /// Adds an entry to the front of the dynamic table, evicting entries
462    /// from the end as needed (RFC 7541 Section 4.4). An entry larger
463    /// than the maximum size empties the table and is not added.
464    #[inline]
465    pub(crate) fn add(&mut self, header: Header) {
466        if header.size() > self.max_size {
467            self.entries.clear();
468            self.size = 0;
469            if self.maintain_maps {
470                self.exact.clear();
471                self.name.clear();
472            }
473            return;
474        }
475        while self.size + header.size() > self.max_size {
476            match self.entries.pop_back() {
477                Some(entry) => {
478                    self.size -= entry.size();
479                    let combined = STATIC_LEN + self.entries.len() + 1;
480                    self.evict_entry(&entry, combined);
481                }
482                None => break,
483            }
484        }
485        if self.maintain_maps {
486            // A front insertion shifts every existing dynamic entry's combined
487            // index up by one; bump the stored indices to match.
488            for v in self.exact.values_mut() {
489                *v += 1;
490            }
491            for v in self.name.values_mut() {
492                *v += 1;
493            }
494        }
495
496        let combined = STATIC_LEN + 1;
497        self.size += header.size();
498        if self.maintain_maps {
499            let name = header.name_bytes().clone();
500            let value = header.value_bytes().clone();
501            self.entries.push_front(header);
502            self.exact
503                .insert(NameValue(name.clone(), value.clone()), combined);
504            self.name.insert(name, combined);
505        } else {
506            self.entries.push_front(header);
507        }
508    }
509}
510
511impl Default for Table {
512    #[inline]
513    fn default() -> Self {
514        Self::new()
515    }
516}
517
518/// RFC 7541 Section 4.2: the initial maximum dynamic table size is 4096
519/// octets.
520const DEFAULT_MAX_SIZE: usize = 4096;
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    fn header(name: &str, value: &str) -> Header {
527        Header::new(
528            Bytes::copy_from_slice(name.as_bytes()),
529            Bytes::copy_from_slice(value.as_bytes()),
530        )
531    }
532
533    /// The hash-map backed `find`/`find_name` must stay correct across the
534    /// front-insert index shift and back-eviction, including for duplicate
535    /// names and values.
536    #[test]
537    fn find_tracks_eviction_and_duplicates() {
538        let mut table = Table::with_max_size(200);
539        table.add(header("a", "1")); // combined 62
540        table.add(header("b", "2")); // combined 63
541        table.add(header("a", "3")); // combined 62 (newest), `a`/`1` shifts to 64
542
543        assert_eq!(table.find_name(header("a", "").name_bytes()), Some(62));
544        assert_eq!(
545            table.find(
546                header("a", "3").name_bytes(),
547                header("a", "3").value_bytes()
548            ),
549            Some(62)
550        );
551        assert_eq!(
552            table.find(
553                header("a", "1").name_bytes(),
554                header("a", "1").value_bytes()
555            ),
556            Some(64)
557        );
558
559        // Evict the oldest entry (`a`/`1` at combined 64) by shrinking; the
560        // newer `a`/`3` must remain the stored name match.
561        table.set_max_size(70); // three 34-octet entries = 102 > 70 -> one evicted
562        assert_eq!(
563            table.find(
564                header("a", "1").name_bytes(),
565                header("a", "1").value_bytes()
566            ),
567            None
568        );
569        assert_eq!(table.find_name(header("a", "").name_bytes()), Some(62));
570        assert_eq!(
571            table.find(
572                header("a", "3").name_bytes(),
573                header("a", "3").value_bytes()
574            ),
575            Some(62)
576        );
577        assert_eq!(table.find_name(header("b", "").name_bytes()), Some(63));
578    }
579
580    #[test]
581    fn static_table_contents() {
582        assert_eq!(
583            Table::new().get(1),
584            Some(Header::new(
585                Bytes::from_static(b":authority"),
586                Bytes::from_static(b"")
587            ))
588        );
589        assert_eq!(
590            Table::new().get(2),
591            Some(Header::new(
592                Bytes::from_static(b":method"),
593                Bytes::from_static(b"GET")
594            ))
595        );
596        assert_eq!(
597            Table::new().get(16),
598            Some(Header::new(
599                Bytes::from_static(b"accept-encoding"),
600                Bytes::from_static(b"gzip, deflate")
601            ))
602        );
603        assert_eq!(
604            Table::new().get(61),
605            Some(Header::new(
606                Bytes::from_static(b"www-authenticate"),
607                Bytes::from_static(b"")
608            ))
609        );
610        assert_eq!(Table::new().get(0), None);
611    }
612
613    #[test]
614    fn entry_size_math() {
615        // 32 overhead + 4 name + 3 value.
616        assert_eq!(header("test", "abc").size(), 39);
617        assert_eq!(header("", "").size(), 32);
618    }
619
620    #[test]
621    fn add_and_fetch_order() {
622        let mut table = Table::with_max_size(200);
623        table.add(header("a", "1"));
624        table.add(header("b", "2"));
625        table.add(header("c", "3"));
626
627        // Newest entry is at index 62.
628        assert_eq!(table.get(62), Some(header("c", "3")));
629        assert_eq!(table.get(63), Some(header("b", "2")));
630        assert_eq!(table.get(64), Some(header("a", "1")));
631        assert_eq!(table.get(65), None);
632        assert_eq!(table.dynamic_len(), 3);
633        assert_eq!(table.len(), STATIC_LEN + 3);
634        assert_eq!(table.size(), 34 * 3);
635    }
636
637    #[test]
638    fn eviction_from_end() {
639        // Each entry is 34 octets; only two fit in 70.
640        let mut table = Table::with_max_size(70);
641        table.add(header("a", "1"));
642        table.add(header("b", "2"));
643        table.add(header("c", "3"));
644
645        assert_eq!(table.dynamic_len(), 2);
646        assert_eq!(table.get(62), Some(header("c", "3")));
647        assert_eq!(table.get(63), Some(header("b", "2")));
648        assert_eq!(table.get(64), None);
649        assert_eq!(table.size(), 68);
650    }
651
652    #[test]
653    fn entry_larger_than_max_empties_table() {
654        let mut table = Table::with_max_size(100);
655        table.add(header("x", "1"));
656        table.add(header("y", "2"));
657        assert_eq!(table.dynamic_len(), 2);
658
659        // 101 octets > 100 max.
660        let big = header(&"v".repeat(60), &"v".repeat(9));
661        assert_eq!(big.size(), 101);
662        table.add(big);
663
664        assert_eq!(table.dynamic_len(), 0);
665        assert_eq!(table.size(), 0);
666    }
667
668    #[test]
669    fn entry_exactly_max_size_is_added() {
670        let mut table = Table::with_max_size(101);
671        let big = header(&"v".repeat(60), &"v".repeat(9));
672        assert_eq!(big.size(), 101);
673        table.add(big);
674        assert_eq!(table.dynamic_len(), 1);
675    }
676
677    #[test]
678    fn size_update_evicts() {
679        let mut table = Table::with_max_size(200);
680        table.add(header("a", "1"));
681        table.add(header("b", "2"));
682        table.add(header("c", "3"));
683
684        table.set_max_size(70);
685        assert_eq!(table.dynamic_len(), 2);
686        assert_eq!(table.get(62), Some(header("c", "3")));
687        assert_eq!(table.size(), 68);
688
689        table.set_max_size(0);
690        assert_eq!(table.dynamic_len(), 0);
691        assert_eq!(table.size(), 0);
692
693        // Growing back to the original size keeps the table empty; new
694        // entries populate it again.
695        table.set_max_size(200);
696        assert_eq!(table.dynamic_len(), 0);
697        table.add(header("d", "4"));
698        assert_eq!(table.get(62), Some(header("d", "4")));
699    }
700
701    #[test]
702    fn max_size_tracks_requests() {
703        let mut table = Table::with_max_size(128);
704        assert_eq!(table.max_size(), 128);
705        table.set_max_size(256);
706        assert_eq!(table.max_size(), 256);
707    }
708
709    #[test]
710    fn default_max_size() {
711        assert_eq!(Table::new().max_size(), DEFAULT_MAX_SIZE);
712        assert_eq!(Table::default().max_size(), DEFAULT_MAX_SIZE);
713    }
714
715    #[test]
716    fn grows_and_reuses_slots() {
717        // The FIFO must keep working after many evictions (the ring
718        // reuses its backing slots).
719        let mut table = Table::with_max_size(200);
720        for i in 0..100 {
721            let entry = header(&format!("k{i}"), "v");
722            table.add(entry.clone());
723            assert!(table.size() <= table.max_size(), "iteration {i}");
724            assert_eq!(table.get(62), Some(entry), "iteration {i}");
725        }
726        assert_eq!(table.get(62), Some(header("k99", "v")));
727        assert_eq!(
728            table.size(),
729            table.dynamic_len() * table.get(62).unwrap().size()
730        );
731    }
732}