sozu_lib/protocol/mux/hpack/mod.rs
1//! HPACK, the header compression of HTTP/2 (RFC 7541), with no I/O.
2//!
3//! The decoder reads a complete header block from a `&[u8]` and hands each
4//! field to a callback; the encoder appends to a `Vec<u8>` the caller owns.
5//! Neither reads a socket, a clock or anything but its arguments and its own
6//! tables, so both are driven byte-in, byte-out like the rest of the H2 core.
7//!
8//! # Allocations
9//!
10//! The hot path allocates nothing once a connection is warm:
11//!
12//! - Huffman decoding reads a state machine computed at compile time
13//! ([`huffman`]) and writes into the decoder's scratch buffer;
14//! - a string sent without Huffman coding is borrowed from the block;
15//! - a field found in a table is borrowed from the table;
16//! - the dynamic table keeps all its entries in one byte buffer plus one ring
17//! of offsets, both grown to a high-water mark bounded by the table size.
18//!
19//! The only allocations are the growth of those three buffers — the scratch
20//! buffer, the table bytes and the offset ring — up to their high-water mark.
21//!
22//! # Guards on untrusted input
23//!
24//! Every malformed block is a [`DecoderError`], never a panic:
25//!
26//! - an integer longer than five octets ([`DecoderError::IntegerOverflow`],
27//! §5.1), or a block ending inside an integer or a string
28//! ([`DecoderError::Truncated`]);
29//! - index 0 or an index past both tables ([`DecoderError::InvalidIndex`],
30//! §2.3.3);
31//! - a size update above the advertised `SETTINGS_HEADER_TABLE_SIZE`
32//! ([`DecoderError::TableSizeTooLarge`], §6.3), after a field
33//! ([`DecoderError::SizeUpdateNotAtStart`], §4.2), more than two of them
34//! ([`DecoderError::TooManySizeUpdates`]) or ending the block
35//! ([`DecoderError::SizeUpdateAtEnd`]);
36//! - EOS inside a Huffman string, padding that is not all ones, or longer than
37//! seven bits ([`HuffmanError`], §5.2).
38//!
39//! The decoded header-list size (RFC 9113 §6.5.2) is the caller's to enforce:
40//! `decode_headers_with_budget` (`lib/src/protocol/mux/pkawa.rs`) counts every
41//! field the callback receives. The decoder still decodes the whole block past
42//! that budget, because the dynamic table must stay in step with the peer's
43//! encoder.
44//!
45//! # Provenance
46//!
47//! Written from RFC 7541. `table::STATIC_TABLE` is Appendix A and
48//! `huffman::CODES` is Appendix B, both transcribed from the RFC text; the
49//! code table was also checked, octet by octet, against HAProxy's `ht` table
50//! (`src/hpack-huff.c`). No code comes from `loona-hpack` or `hpack-rs`, which
51//! this module replaces; the encoder only reproduces the representation policy
52//! `loona-hpack` applied, so that the bytes sozu sends do not change.
53
54mod decoder;
55mod encoder;
56pub mod huffman;
57mod table;
58
59#[cfg(test)]
60mod tests;
61
62pub use decoder::Decoder;
63pub use encoder::{Encoder, Representation};
64pub use huffman::HuffmanError;
65
66/// Why a header block could not be decoded. Every variant is a connection
67/// error of type COMPRESSION_ERROR (RFC 9113 §4.3).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
69pub enum DecoderError {
70 /// The block ends inside an integer or a string.
71 #[error("header block truncated")]
72 Truncated,
73 /// An integer takes more than five octets (RFC 7541 §5.1).
74 #[error("integer encoded on more than five octets")]
75 IntegerOverflow,
76 /// Index 0, or an index past the static and dynamic tables (§2.3.3).
77 #[error("index outside the static and dynamic tables")]
78 InvalidIndex,
79 /// A Huffman string is invalid (§5.2).
80 #[error("invalid Huffman string: {0}")]
81 Huffman(HuffmanError),
82 /// A size update exceeds the advertised `SETTINGS_HEADER_TABLE_SIZE`
83 /// (§6.3).
84 #[error("dynamic table size update above SETTINGS_HEADER_TABLE_SIZE")]
85 TableSizeTooLarge,
86 /// A size update follows a field in the same block (§4.2).
87 #[error("dynamic table size update after a header field")]
88 SizeUpdateNotAtStart,
89 /// More than two size updates open the block (§4.2).
90 #[error("more than two dynamic table size updates")]
91 TooManySizeUpdates,
92 /// The block ends with a size update.
93 #[error("dynamic table size update at the end of a header block")]
94 SizeUpdateAtEnd,
95}
96
97/// Octets an integer may take: its prefix octet and four continuation octets,
98/// enough for 2^28 plus the prefix maximum, far above any length, index or
99/// table size a block can carry.
100const MAX_INTEGER_OCTETS: usize = 5;
101
102/// Decodes the integer (§5.1) whose `prefix_bits`-bit prefix starts `input`,
103/// and returns it with the number of octets it takes.
104fn decode_integer(input: &[u8], prefix_bits: u8) -> Result<(usize, usize), DecoderError> {
105 debug_assert!(
106 (1..=8).contains(&prefix_bits),
107 "an HPACK prefix is 1 to 8 bits"
108 );
109 let Some(&first) = input.first() else {
110 return Err(DecoderError::Truncated);
111 };
112 let mask = ((1u16 << prefix_bits) - 1) as u8;
113 let mut value = usize::from(first & mask);
114 if value < usize::from(mask) {
115 return Ok((value, 1));
116 }
117 for (position, &octet) in input.iter().enumerate().skip(1) {
118 if position >= MAX_INTEGER_OCTETS {
119 return Err(DecoderError::IntegerOverflow);
120 }
121 // At most four continuation octets of 7 bits: the sum stays below
122 // 2^28 + 255 and cannot overflow a 32-bit `usize`.
123 value += usize::from(octet & 0x7f) << (7 * (position - 1));
124 if octet & 0x80 == 0 {
125 debug_assert!(position < MAX_INTEGER_OCTETS, "the octet bound holds");
126 return Ok((value, position + 1));
127 }
128 }
129 Err(DecoderError::Truncated)
130}
131
132/// Appends `value` as an integer with a `prefix_bits`-bit prefix (§5.1), the
133/// high bits of the first octet set to `flags`.
134pub fn encode_integer(value: usize, prefix_bits: u8, flags: u8, out: &mut Vec<u8>) {
135 debug_assert!(
136 (1..=8).contains(&prefix_bits),
137 "an HPACK prefix is 1 to 8 bits"
138 );
139 let mask = ((1u16 << prefix_bits) - 1) as u8;
140 debug_assert!(flags & mask == 0, "flags do not overlap the prefix");
141 if value < usize::from(mask) {
142 out.push(flags | value as u8);
143 return;
144 }
145 out.push(flags | mask);
146 let mut rest = value - usize::from(mask);
147 while rest >= 0x80 {
148 out.push((rest & 0x7f) as u8 | 0x80);
149 rest >>= 7;
150 }
151 out.push(rest as u8);
152}