Skip to main content

zerodds_hpack/
encoder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! HPACK Encoder — RFC 7541 §6.
5//!
6//! Vier Header-Field-Repraesentationen:
7//!
8//! * `Indexed Header Field` (§6.1) — Bit 7 = 1.
9//! * `Literal Header Field with Incremental Indexing` (§6.2.1) — Bit
10//!   7..6 = 01.
11//! * `Literal Header Field without Indexing` (§6.2.2) — Bit 7..4 = 0000.
12//! * `Literal Header Field never Indexed` (§6.2.3) — Bit 7..4 = 0001.
13
14use alloc::vec::Vec;
15
16use crate::integer::encode_integer;
17use crate::string::encode_string;
18use crate::table::{HeaderField, Table};
19
20/// Encoder-Fehler.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum EncoderError {
23    /// Reserviert fuer kuenftige Erweiterungen — aktuell unused.
24    Reserved,
25}
26
27/// HPACK-Encoder mit eigener Dynamic-Table.
28#[derive(Debug, Clone, PartialEq, Eq, Default)]
29pub struct Encoder {
30    table: Table,
31    /// Wenn `true`, werden String-Literals Huffman-komprimiert (Spec
32    /// §5.2 erlaubt beides).
33    pub use_huffman: bool,
34}
35
36impl Encoder {
37    /// Konstruktor mit Default-Max-Size 4096.
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Konstruktor mit konfigurierter Max-Size.
44    #[must_use]
45    pub fn with_max_size(max: usize) -> Self {
46        Self {
47            table: Table::new(max),
48            use_huffman: false,
49        }
50    }
51
52    /// Reference auf die Dynamic-Table.
53    #[must_use]
54    pub fn table(&self) -> &Table {
55        &self.table
56    }
57
58    /// Mut-Reference (Caller passt Max-Size an).
59    pub fn table_mut(&mut self) -> &mut Table {
60        &mut self.table
61    }
62
63    /// Encode eine Header-Liste in einen Output-Buffer.
64    ///
65    /// Strategie:
66    /// * Voll-Match → `Indexed Header Field` (Spec §6.1).
67    /// * Name-Only-Match → `Literal with Incremental Indexing,
68    ///   indexed name`.
69    /// * Kein Match → `Literal with Incremental Indexing, new name`.
70    #[must_use]
71    pub fn encode(&mut self, headers: &[HeaderField]) -> Vec<u8> {
72        let mut out = Vec::new();
73        for h in headers {
74            match self.table.find(&h.name, &h.value) {
75                Some((index, true)) => {
76                    // §6.1: 1xxx_xxxx — 7-Bit-Index.
77                    let buf = encode_integer(index as u64, 7, 0x80);
78                    out.extend_from_slice(&buf);
79                }
80                Some((index, false)) => {
81                    // §6.2.1: 01xx_xxxx — 6-Bit-Index, dann Value-String.
82                    let buf = encode_integer(index as u64, 6, 0x40);
83                    out.extend_from_slice(&buf);
84                    out.extend_from_slice(&encode_string(&h.value, self.use_huffman));
85                    self.table.add(h.clone());
86                }
87                None => {
88                    // §6.2.1: 0100_0000 + Name-Literal + Value-Literal.
89                    out.push(0x40);
90                    out.extend_from_slice(&encode_string(&h.name, self.use_huffman));
91                    out.extend_from_slice(&encode_string(&h.value, self.use_huffman));
92                    self.table.add(h.clone());
93                }
94            }
95        }
96        out
97    }
98}
99
100#[cfg(test)]
101#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
102mod tests {
103    use super::*;
104
105    fn hf(n: &str, v: &str) -> HeaderField {
106        HeaderField {
107            name: n.into(),
108            value: v.into(),
109        }
110    }
111
112    #[test]
113    fn full_static_match_is_single_byte() {
114        let mut e = Encoder::new();
115        let buf = e.encode(&[hf(":method", "GET")]);
116        assert_eq!(buf, alloc::vec![0x82]); // index 2 with high-bit set
117    }
118
119    #[test]
120    fn name_only_static_emits_literal_value() {
121        let mut e = Encoder::new();
122        let buf = e.encode(&[hf(":method", "PATCH")]);
123        // 0x42 (01_000010) = literal incremental, indexed name=2 (or 3)
124        assert!(buf[0] & 0xc0 == 0x40);
125        // Should add to dynamic table.
126        assert_eq!(e.table().len(), 1);
127    }
128
129    #[test]
130    fn unknown_header_emits_literal_name_and_value() {
131        let mut e = Encoder::new();
132        let buf = e.encode(&[hf("custom", "value")]);
133        assert_eq!(buf[0], 0x40); // literal incremental, new name
134        assert_eq!(e.table().len(), 1);
135    }
136
137    #[test]
138    fn second_encode_uses_dynamic_table_match() {
139        let mut e = Encoder::new();
140        let _ = e.encode(&[hf("custom", "value")]);
141        let buf = e.encode(&[hf("custom", "value")]);
142        // Should be Indexed → high bit set.
143        assert_eq!(buf[0] & 0x80, 0x80);
144    }
145
146    #[test]
147    fn huffman_flag_compresses_literal_strings() {
148        let mut e = Encoder::with_max_size(4096);
149        e.use_huffman = true;
150        let buf = e.encode(&[hf("custom", "value")]);
151        // Length-prefix bytes have H-flag set on at least name + value.
152        // Literal-name byte at offset 1.
153        assert!(buf[1] & 0x80 == 0x80, "name string should have H-flag");
154    }
155
156    #[test]
157    fn multiple_headers_encoded_sequentially() {
158        let mut e = Encoder::new();
159        let buf = e.encode(&[hf(":method", "GET"), hf(":scheme", "https")]);
160        assert_eq!(buf, alloc::vec![0x82, 0x87]); // indices 2 and 7
161    }
162
163    #[test]
164    fn encoder_default_uses_no_huffman() {
165        let e = Encoder::new();
166        assert!(!e.use_huffman);
167    }
168}