Skip to main content

zerodds_hpack/
table.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! HPACK Static + Dynamic Table — RFC 7541 §2.3.
5//!
6//! Static-Table: 61 fixed Eintraege (Spec Appendix A).
7//! Dynamic-Table: FIFO mit `header_table_size`-Limit (Caller via
8//! SETTINGS_HEADER_TABLE_SIZE konfiguriert).
9
10use alloc::collections::VecDeque;
11use alloc::string::String;
12
13/// Static-Table-Entry.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct StaticTableEntry {
16    /// Header-Name.
17    pub name: &'static str,
18    /// Default-Value (kann leer sein).
19    pub value: &'static str,
20}
21
22/// RFC 7541 Appendix A — 61 Static-Table-Eintraege (Indices 1..=61).
23pub const STATIC_TABLE: [StaticTableEntry; 61] = [
24    StaticTableEntry {
25        name: ":authority",
26        value: "",
27    },
28    StaticTableEntry {
29        name: ":method",
30        value: "GET",
31    },
32    StaticTableEntry {
33        name: ":method",
34        value: "POST",
35    },
36    StaticTableEntry {
37        name: ":path",
38        value: "/",
39    },
40    StaticTableEntry {
41        name: ":path",
42        value: "/index.html",
43    },
44    StaticTableEntry {
45        name: ":scheme",
46        value: "http",
47    },
48    StaticTableEntry {
49        name: ":scheme",
50        value: "https",
51    },
52    StaticTableEntry {
53        name: ":status",
54        value: "200",
55    },
56    StaticTableEntry {
57        name: ":status",
58        value: "204",
59    },
60    StaticTableEntry {
61        name: ":status",
62        value: "206",
63    },
64    StaticTableEntry {
65        name: ":status",
66        value: "304",
67    },
68    StaticTableEntry {
69        name: ":status",
70        value: "400",
71    },
72    StaticTableEntry {
73        name: ":status",
74        value: "404",
75    },
76    StaticTableEntry {
77        name: ":status",
78        value: "500",
79    },
80    StaticTableEntry {
81        name: "accept-charset",
82        value: "",
83    },
84    StaticTableEntry {
85        name: "accept-encoding",
86        value: "gzip, deflate",
87    },
88    StaticTableEntry {
89        name: "accept-language",
90        value: "",
91    },
92    StaticTableEntry {
93        name: "accept-ranges",
94        value: "",
95    },
96    StaticTableEntry {
97        name: "accept",
98        value: "",
99    },
100    StaticTableEntry {
101        name: "access-control-allow-origin",
102        value: "",
103    },
104    StaticTableEntry {
105        name: "age",
106        value: "",
107    },
108    StaticTableEntry {
109        name: "allow",
110        value: "",
111    },
112    StaticTableEntry {
113        name: "authorization",
114        value: "",
115    },
116    StaticTableEntry {
117        name: "cache-control",
118        value: "",
119    },
120    StaticTableEntry {
121        name: "content-disposition",
122        value: "",
123    },
124    StaticTableEntry {
125        name: "content-encoding",
126        value: "",
127    },
128    StaticTableEntry {
129        name: "content-language",
130        value: "",
131    },
132    StaticTableEntry {
133        name: "content-length",
134        value: "",
135    },
136    StaticTableEntry {
137        name: "content-location",
138        value: "",
139    },
140    StaticTableEntry {
141        name: "content-range",
142        value: "",
143    },
144    StaticTableEntry {
145        name: "content-type",
146        value: "",
147    },
148    StaticTableEntry {
149        name: "cookie",
150        value: "",
151    },
152    StaticTableEntry {
153        name: "date",
154        value: "",
155    },
156    StaticTableEntry {
157        name: "etag",
158        value: "",
159    },
160    StaticTableEntry {
161        name: "expect",
162        value: "",
163    },
164    StaticTableEntry {
165        name: "expires",
166        value: "",
167    },
168    StaticTableEntry {
169        name: "from",
170        value: "",
171    },
172    StaticTableEntry {
173        name: "host",
174        value: "",
175    },
176    StaticTableEntry {
177        name: "if-match",
178        value: "",
179    },
180    StaticTableEntry {
181        name: "if-modified-since",
182        value: "",
183    },
184    StaticTableEntry {
185        name: "if-none-match",
186        value: "",
187    },
188    StaticTableEntry {
189        name: "if-range",
190        value: "",
191    },
192    StaticTableEntry {
193        name: "if-unmodified-since",
194        value: "",
195    },
196    StaticTableEntry {
197        name: "last-modified",
198        value: "",
199    },
200    StaticTableEntry {
201        name: "link",
202        value: "",
203    },
204    StaticTableEntry {
205        name: "location",
206        value: "",
207    },
208    StaticTableEntry {
209        name: "max-forwards",
210        value: "",
211    },
212    StaticTableEntry {
213        name: "proxy-authenticate",
214        value: "",
215    },
216    StaticTableEntry {
217        name: "proxy-authorization",
218        value: "",
219    },
220    StaticTableEntry {
221        name: "range",
222        value: "",
223    },
224    StaticTableEntry {
225        name: "referer",
226        value: "",
227    },
228    StaticTableEntry {
229        name: "refresh",
230        value: "",
231    },
232    StaticTableEntry {
233        name: "retry-after",
234        value: "",
235    },
236    StaticTableEntry {
237        name: "server",
238        value: "",
239    },
240    StaticTableEntry {
241        name: "set-cookie",
242        value: "",
243    },
244    StaticTableEntry {
245        name: "strict-transport-security",
246        value: "",
247    },
248    StaticTableEntry {
249        name: "transfer-encoding",
250        value: "",
251    },
252    StaticTableEntry {
253        name: "user-agent",
254        value: "",
255    },
256    StaticTableEntry {
257        name: "vary",
258        value: "",
259    },
260    StaticTableEntry {
261        name: "via",
262        value: "",
263    },
264    StaticTableEntry {
265        name: "www-authenticate",
266        value: "",
267    },
268];
269
270/// Header-Field (Owned) — wird in der Dynamic-Table gespeichert.
271#[derive(Debug, Clone, PartialEq, Eq)]
272pub struct HeaderField {
273    /// Name.
274    pub name: String,
275    /// Value.
276    pub value: String,
277}
278
279impl HeaderField {
280    /// Spec §4.1 — Estimated-Size (32 + name.len + value.len).
281    #[must_use]
282    pub fn size(&self) -> usize {
283        32 + self.name.len() + self.value.len()
284    }
285}
286
287/// Combined-Lookup — Index in Static + Dynamic. RFC 7541 §2.3.
288///
289/// Static = 1..=61, Dynamic = 62..=N.
290#[derive(Debug, Clone, PartialEq, Eq)]
291pub struct Table {
292    dynamic: VecDeque<HeaderField>,
293    /// Aktuelle Gesamtgroesse der Dynamic-Table (Spec §4.1).
294    size: usize,
295    /// Max-Size laut SETTINGS_HEADER_TABLE_SIZE.
296    max_size: usize,
297}
298
299impl Default for Table {
300    fn default() -> Self {
301        Self {
302            dynamic: VecDeque::new(),
303            size: 0,
304            max_size: 4096, // Spec §6.5.2 default
305        }
306    }
307}
308
309impl Table {
310    /// Konstruktor mit `max_size`.
311    #[must_use]
312    pub fn new(max_size: usize) -> Self {
313        Self {
314            dynamic: VecDeque::new(),
315            size: 0,
316            max_size,
317        }
318    }
319
320    /// Fuegt einen neuen Eintrag in die Dynamic-Table (vorne).
321    /// Spec §4.4.
322    pub fn add(&mut self, field: HeaderField) {
323        let new_size = field.size();
324        if new_size > self.max_size {
325            // Spec §4.4: ueberlanger Eintrag => Tabelle leeren.
326            self.dynamic.clear();
327            self.size = 0;
328            return;
329        }
330        // Evict from back until fits.
331        while self.size + new_size > self.max_size {
332            if let Some(removed) = self.dynamic.pop_back() {
333                self.size -= removed.size();
334            } else {
335                break;
336            }
337        }
338        self.size += new_size;
339        self.dynamic.push_front(field);
340    }
341
342    /// Lookup ueber kombinierten Index.
343    #[must_use]
344    pub fn get(&self, index: usize) -> Option<HeaderField> {
345        if index == 0 {
346            return None;
347        }
348        if index <= STATIC_TABLE.len() {
349            let e = STATIC_TABLE[index - 1];
350            return Some(HeaderField {
351                name: alloc::string::ToString::to_string(e.name),
352                value: alloc::string::ToString::to_string(e.value),
353            });
354        }
355        let dyn_index = index - STATIC_TABLE.len() - 1;
356        self.dynamic.get(dyn_index).cloned()
357    }
358
359    /// Sucht einen Header-Field-Match. Liefert `(index, full_match)`.
360    /// `full_match=true`: Name + Value passen.
361    /// `full_match=false`: nur Name passt.
362    #[must_use]
363    pub fn find(&self, name: &str, value: &str) -> Option<(usize, bool)> {
364        let mut name_only: Option<usize> = None;
365        // Static
366        for (i, e) in STATIC_TABLE.iter().enumerate() {
367            if e.name == name {
368                if e.value == value {
369                    return Some((i + 1, true));
370                }
371                name_only.get_or_insert(i + 1);
372            }
373        }
374        // Dynamic
375        for (i, h) in self.dynamic.iter().enumerate() {
376            let idx = STATIC_TABLE.len() + 1 + i;
377            if h.name == name {
378                if h.value == value {
379                    return Some((idx, true));
380                }
381                name_only.get_or_insert(idx);
382            }
383        }
384        name_only.map(|i| (i, false))
385    }
386
387    /// Aktuelle Dynamic-Table-Size (Spec §4.1).
388    #[must_use]
389    pub fn size(&self) -> usize {
390        self.size
391    }
392
393    /// Max-Size.
394    #[must_use]
395    pub fn max_size(&self) -> usize {
396        self.max_size
397    }
398
399    /// Setzt eine neue Max-Size — bei Verkleinerung werden Eintraege
400    /// von hinten verworfen.
401    pub fn set_max_size(&mut self, new_max: usize) {
402        self.max_size = new_max;
403        while self.size > self.max_size {
404            if let Some(removed) = self.dynamic.pop_back() {
405                self.size -= removed.size();
406            } else {
407                break;
408            }
409        }
410    }
411
412    /// Anzahl Dynamic-Table-Eintraege.
413    #[must_use]
414    pub fn len(&self) -> usize {
415        self.dynamic.len()
416    }
417
418    /// `true` wenn Dynamic-Table leer.
419    #[must_use]
420    pub fn is_empty(&self) -> bool {
421        self.dynamic.is_empty()
422    }
423}
424
425#[cfg(test)]
426#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
427mod tests {
428    use super::*;
429    use alloc::string::ToString;
430
431    fn hf(n: &str, v: &str) -> HeaderField {
432        HeaderField {
433            name: n.into(),
434            value: v.into(),
435        }
436    }
437
438    #[test]
439    fn static_table_has_61_entries() {
440        assert_eq!(STATIC_TABLE.len(), 61);
441        assert_eq!(STATIC_TABLE[0].name, ":authority");
442        assert_eq!(STATIC_TABLE[60].name, "www-authenticate");
443    }
444
445    #[test]
446    fn lookup_static_index_1() {
447        let t = Table::new(4096);
448        let h = t.get(1).unwrap();
449        assert_eq!(h.name, ":authority");
450        assert_eq!(h.value, "");
451    }
452
453    #[test]
454    fn lookup_static_index_2_get() {
455        let t = Table::new(4096);
456        let h = t.get(2).unwrap();
457        assert_eq!(h.name, ":method");
458        assert_eq!(h.value, "GET");
459    }
460
461    #[test]
462    fn dynamic_add_and_lookup() {
463        let mut t = Table::new(4096);
464        t.add(hf("custom", "value"));
465        assert_eq!(t.len(), 1);
466        let h = t.get(62).unwrap();
467        assert_eq!(h.name, "custom");
468        assert_eq!(h.value, "value");
469    }
470
471    #[test]
472    fn dynamic_evicts_oldest_on_overflow() {
473        let mut t = Table::new(64); // small
474        t.add(hf("a", "1")); // size = 32 + 1 + 1 = 34
475        t.add(hf("b", "2")); // would push out 'a'
476        assert_eq!(t.len(), 1);
477        assert_eq!(t.get(62).unwrap().name, "b");
478    }
479
480    #[test]
481    fn entry_too_large_clears_table() {
482        let mut t = Table::new(50);
483        t.add(hf("a", "1"));
484        let huge = hf("very-long-name-that-exceeds-max", "value");
485        t.add(huge);
486        assert!(t.is_empty(), "table should be cleared per Spec §4.4");
487    }
488
489    #[test]
490    fn find_static_full_match() {
491        let t = Table::new(4096);
492        let r = t.find(":method", "GET");
493        assert_eq!(r, Some((2, true)));
494    }
495
496    #[test]
497    fn find_static_name_only() {
498        let t = Table::new(4096);
499        let r = t.find(":method", "PATCH");
500        assert!(r.unwrap().0 == 2 || r.unwrap().0 == 3);
501        assert!(!r.unwrap().1);
502    }
503
504    #[test]
505    fn find_dynamic() {
506        let mut t = Table::new(4096);
507        t.add(hf("custom", "value"));
508        let r = t.find("custom", "value");
509        assert_eq!(r, Some((62, true)));
510    }
511
512    #[test]
513    fn set_max_size_shrinks_evicting() {
514        let mut t = Table::new(4096);
515        t.add(hf("a", "1"));
516        t.add(hf("b", "2"));
517        t.set_max_size(34);
518        assert!(t.len() <= 1);
519    }
520
521    #[test]
522    fn header_field_size_is_32_plus_name_plus_value() {
523        let h = hf("abc", "12345");
524        assert_eq!(h.size(), 32 + 3 + 5);
525    }
526
527    #[test]
528    fn lookup_zero_index_returns_none() {
529        let t = Table::new(4096);
530        assert!(t.get(0).is_none());
531    }
532
533    #[test]
534    fn find_for_name_string() {
535        let t = Table::new(4096);
536        let r = t.find(":method", "GET");
537        let h = t.get(r.unwrap().0).unwrap();
538        assert_eq!(h.value.to_string(), "GET");
539    }
540}