Skip to main content

vibeio_http/h2/hpack/
encode.rs

1//! HPACK encoder (RFC 7541 Section 6).
2//!
3//! Encodes a header list into a single header block, maintaining the
4//! dynamic table across blocks. The encoder's table mirrors the peer's
5//! decoder table: it honors the peer's `SETTINGS_HEADER_TABLE_SIZE` via
6//! [`Encoder::queue_size_update`].
7//!
8//! Unlike the API sketch in `CUSTOM_HTTP2_IMPL.md` (`&[(HeaderName,
9//! &[u8])]`), [`Encoder::encode`] takes [`Header`] pairs, because
10//! `http::HeaderName` rejects HTTP/2 pseudo-headers (`:method`, ...)
11//! while they are a first-class part of HPACK header blocks; it also
12//! mirrors the [`Decoder`](super::decode::Decoder) API.
13
14use super::{
15    integer, string,
16    table::{Header, Table},
17};
18
19/// The representation prefixes defined in RFC 7541 Section 6.
20const INDEXED: u8 = 0b1000_0000;
21const LITERAL_WITH_INDEXING: u8 = 0b0100_0000;
22const LITERAL_NEVER_INDEXED: u8 = 0b0001_0000;
23const SIZE_UPDATE: u8 = 0b0010_0000;
24
25/// Header names that must never be added to the indexed table
26/// (RFC 7541 Section 7.1.3).
27const NEVER_INDEXED: [&[u8]; 3] = [b"authorization", b"proxy-authorization", b"cookie"];
28
29/// Encodes HPACK header blocks.
30#[derive(Debug)]
31pub struct Encoder {
32    /// Static + dynamic header table, mirroring the peer decoder.
33    table: Table,
34    /// A size update queued by the protocol layer (SETTINGS) and emitted
35    /// at the start of the next header block.
36    queued_size_update: Option<usize>,
37    /// Whether string literals are Huffman-coded when shorter.
38    use_huffman: bool,
39}
40
41impl Encoder {
42    /// Creates an encoder whose table allows `max_table_size` octets
43    /// (RFC 7541 Section 4.2: 4096 by default). This must match the
44    /// peer's initial `SETTINGS_HEADER_TABLE_SIZE`.
45    #[inline]
46    pub fn new(max_table_size: usize) -> Self {
47        Encoder {
48            table: Table::with_max_size(max_table_size),
49            queued_size_update: None,
50            use_huffman: true,
51        }
52    }
53
54    /// Queues a protocol-level table size update (from SETTINGS) to be
55    /// applied and emitted at the start of the next header block.
56    #[inline]
57    pub fn queue_size_update(&mut self, size: usize) {
58        self.queued_size_update = Some(match self.queued_size_update {
59            Some(current) => current.max(size),
60            None => size,
61        });
62    }
63
64    /// Controls Huffman-coded string literals. Enabled by default, in
65    /// which case a literal is Huffman-coded when that is shorter.
66    #[inline]
67    pub fn set_use_huffman(&mut self, use_huffman: bool) {
68        self.use_huffman = use_huffman;
69    }
70
71    /// The current table, for inspection by in-crate tests.
72    #[cfg(test)]
73    #[inline]
74    pub(crate) fn table(&self) -> &Table {
75        &self.table
76    }
77
78    /// Encodes `headers` as a single header block appended to `out`.
79    ///
80    /// Non-sensitive headers are encoded with incremental indexing
81    /// (RFC 7541 Section 6.2.1) and added to the dynamic table; exact
82    /// matches use the indexed representation (Section 6.1). Sensitive
83    /// headers ([`NEVER_INDEXED`]) are encoded never-indexed (Section
84    /// 6.2.3) and never added to the table.
85    #[inline]
86    pub fn encode(&mut self, headers: &[Header], out: &mut Vec<u8>) {
87        if let Some(size) = self.queued_size_update.take() {
88            self.table.set_max_size(size);
89            integer::encode(out, size as u64, 5, SIZE_UPDATE);
90        }
91
92        for header in headers {
93            let name = header.name();
94            if NEVER_INDEXED.contains(&name) {
95                match self.table.find_name(name) {
96                    Some(index) => integer::encode(out, index as u64, 4, LITERAL_NEVER_INDEXED),
97                    None => {
98                        out.push(LITERAL_NEVER_INDEXED);
99                        self.encode_string(out, name);
100                    }
101                }
102                self.encode_string(out, header.value());
103                continue;
104            }
105
106            match self.table.find(name, header.value()) {
107                Some(index) => {
108                    integer::encode(out, index as u64, 7, INDEXED);
109                }
110                None => {
111                    match self.table.find_name(name) {
112                        Some(index) => integer::encode(out, index as u64, 6, LITERAL_WITH_INDEXING),
113                        None => {
114                            out.push(LITERAL_WITH_INDEXING);
115                            self.encode_string(out, name);
116                        }
117                    }
118                    self.encode_string(out, header.value());
119                    // Store the header by cloning its already-owned `Bytes`
120                    // (a refcount bump) instead of re-copying the name and
121                    // value into fresh heap allocations.
122                    self.table.add(header.clone());
123                }
124            }
125        }
126    }
127
128    #[inline]
129    fn encode_string(&self, out: &mut Vec<u8>, value: &[u8]) {
130        let huffman_len = self
131            .use_huffman
132            .then(|| string::huffman_encoded_len_if_shorter(value))
133            .flatten();
134        string::encode(out, value, huffman_len);
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::h2::hpack::decode::Decoder;
142
143    /// Builds a `Header` list from string tuples.
144    #[inline]
145    fn headers(list: &[(&str, &str)]) -> Vec<Header> {
146        list.iter()
147            .map(|(name, value)| Header::new(name.as_bytes().to_vec(), value.as_bytes().to_vec()))
148            .collect()
149    }
150
151    #[inline]
152    fn encode(encoder: &mut Encoder, list: &[(&str, &str)]) -> Vec<u8> {
153        let list = headers(list);
154        let mut out = Vec::new();
155        encoder.encode(&list, &mut out);
156        out
157    }
158
159    #[inline]
160    fn decode(encoder: &Encoder, wire: &[u8]) -> Vec<(String, String)> {
161        let mut decoder = Decoder::new(encoder.table().max_size());
162        decoder
163            .decode(wire, &mut 0)
164            .unwrap()
165            .into_iter()
166            .map(|h| {
167                (
168                    String::from_utf8(h.name().to_vec()).unwrap(),
169                    String::from_utf8(h.value().to_vec()).unwrap(),
170                )
171            })
172            .collect()
173    }
174
175    #[inline]
176    fn hex_to_bytes(hex: &str) -> Vec<u8> {
177        hex.as_bytes()
178            .chunks_exact(2)
179            .map(|pair| {
180                let hi = (pair[0] as char).to_digit(16).unwrap() as u8;
181                let lo = (pair[1] as char).to_digit(16).unwrap() as u8;
182                (hi << 4) | lo
183            })
184            .collect()
185    }
186
187    /// RFC 7541 C.2.1: literal with incremental indexing, new name.
188    #[test]
189    fn literal_new_name_indexed() {
190        let mut encoder = Encoder::new(4096);
191        encoder.set_use_huffman(false);
192        let out = encode(&mut encoder, &[("custom-key", "custom-header")]);
193        assert_eq!(
194            out,
195            hex_to_bytes("400a637573746f6d2d6b65790d637573746f6d2d686561646572")
196        );
197        assert_eq!(encoder.table().dynamic_len(), 1);
198    }
199
200    /// RFC 7541 C.3: full request walkthrough without Huffman.
201    #[test]
202    fn c3_request_walkthrough() {
203        let mut encoder = Encoder::new(4096);
204        encoder.set_use_huffman(false);
205
206        // C.3.1: first request; :authority added at index 62.
207        assert_eq!(
208            encode(
209                &mut encoder,
210                &[
211                    (":method", "GET"),
212                    (":scheme", "http"),
213                    (":path", "/"),
214                    (":authority", "www.example.com"),
215                ]
216            ),
217            hex_to_bytes("828684410f7777772e6578616d706c652e636f6d")
218        );
219
220        // C.3.2: :authority referenced at dynamic 62; cache-control added.
221        assert_eq!(
222            encode(
223                &mut encoder,
224                &[
225                    (":method", "GET"),
226                    (":scheme", "http"),
227                    (":path", "/"),
228                    (":authority", "www.example.com"),
229                    ("cache-control", "no-cache"),
230                ]
231            ),
232            hex_to_bytes("828684be58086e6f2d6361636865")
233        );
234
235        // C.3.3: cache-control at 62, :authority at 63, new literal name.
236        assert_eq!(
237            encode(
238                &mut encoder,
239                &[
240                    (":method", "GET"),
241                    (":scheme", "https"),
242                    (":path", "/index.html"),
243                    (":authority", "www.example.com"),
244                    ("custom-key", "custom-value"),
245                ]
246            ),
247            hex_to_bytes("828785bf400a637573746f6d2d6b65790c637573746f6d2d76616c7565")
248        );
249    }
250
251    /// RFC 7541 C.4: full request walkthrough with Huffman.
252    #[test]
253    fn c4_request_walkthrough() {
254        let mut encoder = Encoder::new(4096);
255
256        // C.4.1.
257        assert_eq!(
258            encode(
259                &mut encoder,
260                &[
261                    (":method", "GET"),
262                    (":scheme", "http"),
263                    (":path", "/"),
264                    (":authority", "www.example.com"),
265                ]
266            ),
267            hex_to_bytes("828684418cf1e3c2e5f23a6ba0ab90f4ff")
268        );
269
270        // C.4.2.
271        assert_eq!(
272            encode(
273                &mut encoder,
274                &[
275                    (":method", "GET"),
276                    (":scheme", "http"),
277                    (":path", "/"),
278                    (":authority", "www.example.com"),
279                    ("cache-control", "no-cache"),
280                ]
281            ),
282            hex_to_bytes("828684be5886a8eb10649cbf")
283        );
284
285        // C.4.3.
286        assert_eq!(
287            encode(
288                &mut encoder,
289                &[
290                    (":method", "GET"),
291                    (":scheme", "https"),
292                    (":path", "/index.html"),
293                    (":authority", "www.example.com"),
294                    ("custom-key", "custom-value"),
295                ]
296            ),
297            hex_to_bytes("828785bf408825a849e95ba97d7f8925a849e95bb8e8b4bf")
298        );
299    }
300
301    /// RFC 7541 C.5: response walkthrough without Huffman, 256-octet
302    /// table so evictions occur.
303    #[test]
304    fn c5_response_walkthrough() {
305        let mut encoder = Encoder::new(256);
306        encoder.set_use_huffman(false);
307
308        // C.5.1.
309        assert_eq!(
310            encode(
311                &mut encoder,
312                &[
313                    (":status", "302"),
314                    ("cache-control", "private"),
315                    ("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
316                    ("location", "https://www.example.com"),
317                ]
318            ),
319            hex_to_bytes(
320                "4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d"
321            )
322        );
323        assert_eq!(encoder.table().dynamic_len(), 4);
324
325        // C.5.2: :status: 302 evicted to make room.
326        assert_eq!(
327            encode(
328                &mut encoder,
329                &[
330                    (":status", "307"),
331                    ("cache-control", "private"),
332                    ("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
333                    ("location", "https://www.example.com"),
334                ]
335            ),
336            hex_to_bytes("4803333037c1c0bf")
337        );
338        assert_eq!(encoder.table().dynamic_len(), 4);
339
340        // C.5.3: several entries evicted.
341        assert_eq!(
342            encode(
343                &mut encoder,
344                &[
345                    (":status", "200"),
346                    ("cache-control", "private"),
347                    ("date", "Mon, 21 Oct 2013 20:13:22 GMT"),
348                    ("location", "https://www.example.com"),
349                    ("content-encoding", "gzip"),
350                    (
351                        "set-cookie",
352                        "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
353                    ),
354                ]
355            ),
356            hex_to_bytes(
357                "88c1611d4d6f6e2c203231204f637420323031332032303a31333a323220474d54c05a04677a69707738666f6f3d4153444a4b48514b425a584f5157454f50495541585157454f49553b206d61782d6167653d333630303b2076657273696f6e3d31"
358            )
359        );
360        assert_eq!(encoder.table().dynamic_len(), 3);
361    }
362
363    /// RFC 7541 C.6: response walkthrough with Huffman, same table
364    /// dynamics as C.5.
365    #[test]
366    fn c6_response_walkthrough() {
367        let mut encoder = Encoder::new(256);
368
369        // C.6.1.
370        assert_eq!(
371            encode(
372                &mut encoder,
373                &[
374                    (":status", "302"),
375                    ("cache-control", "private"),
376                    ("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
377                    ("location", "https://www.example.com"),
378                ]
379            ),
380            hex_to_bytes(
381                "488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3"
382            )
383        );
384        assert_eq!(encoder.table().dynamic_len(), 4);
385
386        // C.6.2.
387        assert_eq!(
388            encode(
389                &mut encoder,
390                &[
391                    (":status", "307"),
392                    ("cache-control", "private"),
393                    ("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
394                    ("location", "https://www.example.com"),
395                ]
396            ),
397            hex_to_bytes("4883640effc1c0bf")
398        );
399        assert_eq!(encoder.table().dynamic_len(), 4);
400
401        // C.6.3.
402        assert_eq!(
403            encode(
404                &mut encoder,
405                &[
406                    (":status", "200"),
407                    ("cache-control", "private"),
408                    ("date", "Mon, 21 Oct 2013 20:13:22 GMT"),
409                    ("location", "https://www.example.com"),
410                    ("content-encoding", "gzip"),
411                    (
412                        "set-cookie",
413                        "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
414                    ),
415                ]
416            ),
417            hex_to_bytes(
418                "88c16196d07abe941054d444a8200595040b8166e084a62d1bffc05a839bd9ab77ad94e7821dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab270fb5291f9587316065c003ed4ee5b1063d5007"
419            )
420        );
421        assert_eq!(encoder.table().dynamic_len(), 3);
422    }
423
424    /// Non-sensitive headers are indexed even when never-indexed would
425    /// be a valid choice; only [`NEVER_INDEXED`] names are excluded.
426    #[test]
427    fn non_sensitive_is_indexed() {
428        let mut encoder = Encoder::new(4096);
429        encoder.set_use_huffman(false);
430        let out = encode(&mut encoder, &[("password", "secret")]);
431        // Literal with incremental indexing (0x40), new name.
432        assert_eq!(out, hex_to_bytes("400870617373776f726406736563726574"));
433        assert_eq!(encoder.table().dynamic_len(), 1);
434    }
435
436    /// RFC 7541 Section 7.1.3: sensitive headers are encoded never
437    /// indexed and never enter the dynamic table.
438    #[test]
439    fn sensitive_headers_never_indexed() {
440        for name in ["authorization", "proxy-authorization", "cookie"] {
441            let mut encoder = Encoder::new(4096);
442            encoder.set_use_huffman(false);
443            let out = encode(&mut encoder, &[(name, "secret-value")]);
444
445            // Literal never indexed (0x10) with the static-table name
446            // index; all three names are static entries above 15, so the
447            // index is split across a 0x1f first octet + continuation.
448            assert_eq!(out[0], 0x1f, "{name}");
449            let decoded = decode(&encoder, &out);
450            assert_eq!(
451                decoded,
452                vec![(name.to_string(), "secret-value".into())],
453                "{name}"
454            );
455            // Sensitive entries never enter the table.
456            assert_eq!(encoder.table().dynamic_len(), 0, "{name}");
457        }
458    }
459
460    /// The static-table name index for a sensitive header is emitted
461    /// verbatim: authorization is static index 23 (0x1f 0x08).
462    #[test]
463    fn sensitive_uses_static_name_index() {
464        let mut encoder = Encoder::new(4096);
465        encoder.set_use_huffman(false);
466        let out = encode(&mut encoder, &[("authorization", "Bearer xyz")]);
467        assert_eq!(out, hex_to_bytes("1f080a4265617265722078797a"));
468        assert_eq!(encoder.table().dynamic_len(), 0);
469    }
470
471    /// A queued table size update is emitted before the first field and
472    /// applied to the encoder's table (RFC 7541 Section 6.3).
473    #[test]
474    fn size_update_emitted_at_block_start() {
475        let mut encoder = Encoder::new(4096);
476        encoder.set_use_huffman(false);
477        encoder.queue_size_update(0);
478        let out = encode(&mut encoder, &[(":method", "GET")]);
479        assert_eq!(out, hex_to_bytes("2082"));
480        assert_eq!(encoder.table().max_size(), 0);
481
482        // Shrinking evicts entries so indices stay in sync with the peer.
483        let mut encoder = Encoder::new(4096);
484        encoder.set_use_huffman(false);
485        let _ = encode(&mut encoder, &[("custom-key", "custom-value")]);
486        assert_eq!(encoder.table().dynamic_len(), 1);
487        encoder.queue_size_update(32);
488        let _ = encode(&mut encoder, &[(":method", "GET")]);
489        assert_eq!(encoder.table().dynamic_len(), 0);
490    }
491
492    /// Growing the table again restarts indexing at 62.
493    #[test]
494    fn size_update_grow_reindexes() {
495        let mut encoder = Encoder::new(4096);
496        encoder.set_use_huffman(false);
497        encoder.queue_size_update(0);
498        let _ = encode(&mut encoder, &[("a", "1")]);
499        encoder.queue_size_update(4096);
500        let _ = encode(&mut encoder, &[("b", "2")]);
501        assert_eq!(encoder.table().get(62).unwrap().name(), b"b");
502        assert_eq!(encoder.table().get(63), None);
503    }
504
505    /// Round-trip: encoding then decoding reproduces the original list,
506    /// for a range of table sizes and Huffman settings.
507    #[test]
508    fn round_trip() {
509        let lists: [&[(&str, &str)]; 5] = [
510            &[(":method", "GET"), (":scheme", "https")],
511            &[
512                (":status", "302"),
513                ("cache-control", "private"),
514                ("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
515                ("location", "https://www.example.com"),
516                ("content-encoding", "gzip"),
517                ("authorization", "Bearer sekrit"),
518                ("x-empty", ""),
519            ],
520            &[("x-long", &"v".repeat(300))],
521            &[("x-huffman-ok", "custom-value"), ("cookie", "a=b; c=d")],
522            &[("accept-encoding", "gzip, deflate"), ("te", "trailers")],
523        ];
524
525        for (i, list) in lists.iter().enumerate() {
526            for table_size in [0, 16, 256, 4096] {
527                for use_huffman in [false, true] {
528                    let mut encoder = Encoder::new(table_size);
529                    encoder.set_use_huffman(use_huffman);
530                    let wire = encode(&mut encoder, list);
531                    let decoded = decode(&encoder, &wire);
532                    let expected = list
533                        .iter()
534                        .map(|(name, value)| (name.to_string(), value.to_string()))
535                        .collect::<Vec<_>>();
536                    assert_eq!(
537                        decoded, expected,
538                        "list {i}, table {table_size}, huffman {use_huffman}"
539                    );
540                }
541            }
542        }
543    }
544
545    /// Empty values round-trip with Huffman enabled.
546    #[test]
547    fn empty_value_round_trip() {
548        let mut encoder = Encoder::new(4096);
549        let out = encode(&mut encoder, &[("x-empty", "")]);
550        assert_eq!(decode(&encoder, &out), vec![("x-empty".into(), "".into())]);
551    }
552}