Skip to main content

sozu_lib/protocol/mux/hpack/
encoder.rs

1//! HPACK encoder (RFC 7541 §6) writing into a buffer the caller owns.
2//!
3//! [`Encoder::encode_header_into`] applies the representation policy sozu has
4//! always sent — the one `loona-hpack` 0.4.3 applied, reproduced here as
5//! behavior so that the bytes on the wire do not change:
6//!
7//! - name and value found in a table: Indexed Header Field (§6.1);
8//! - only the name found: Literal without Indexing, indexed name (§6.2.2),
9//!   naming the *last* entry holding that name when the static table is
10//!   scanned, then the dynamic table from its newest entry — `:method: PUT`
11//!   names index 3 (`:method: POST`), `:status: 201` names index 14;
12//! - neither found: Literal with Incremental Indexing, new name (§6.2.1), and
13//!   the field enters the dynamic table.
14//!
15//! Strings are sent without Huffman coding. The dynamic table only ever holds
16//! fields whose name is in no table, so the peer's decoder sees at most one
17//! entry per name.
18
19use super::{
20    encode_integer, huffman,
21    table::{DEFAULT_MAX_SIZE, DYNAMIC_TABLE_START, DynamicTable, STATIC_TABLE},
22};
23
24/// The representation [`Encoder::encode_field`] emits. Sozu itself only sends
25/// [`Representation::Proxy`]; the others reproduce RFC 7541 Appendix C and let
26/// a test peer or a fuzz target produce every representation a decoder meets.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Representation {
29    /// The policy of [`Encoder::encode_header_into`].
30    Proxy,
31    /// §6.2.1, or §6.1 when name and value are found.
32    IncrementalIndexing,
33    /// §6.2.2, never §6.1.
34    WithoutIndexing,
35    /// §6.2.3, never §6.1.
36    NeverIndexed,
37}
38
39/// What a table search found.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41enum Found {
42    Field(usize),
43    /// The first and the last index holding the name. [`Representation::Proxy`]
44    /// names the last one, as `loona-hpack` did; the others name the first,
45    /// as RFC 7541 Appendix C does.
46    Name {
47        first: usize,
48        last: usize,
49    },
50    Nothing,
51}
52
53/// The table-size changes the peer's decoder has not been told about yet.
54///
55/// RFC 7541 §4.2: when the maximum size changes more than once between two
56/// blocks, the next block opens with the smallest size reached, then the final
57/// size. The smallest one is what decides which entries this encoder evicted;
58/// the final one is what both tables hold from then on.
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60struct PendingSizeUpdate {
61    /// The smallest maximum size set since the last size update was encoded.
62    smallest: usize,
63    /// The maximum size set last.
64    last: usize,
65}
66
67/// The connection-level HPACK encoder of one direction.
68#[derive(Debug)]
69pub struct Encoder {
70    table: DynamicTable,
71    /// Set by [`Encoder::change_max_table_size`], emptied by
72    /// [`Encoder::encode_size_updates_into`].
73    pending_size_update: Option<PendingSizeUpdate>,
74}
75
76impl Default for Encoder {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl Encoder {
83    /// An encoder with the default 4096-octet table. Allocates nothing.
84    pub const fn new() -> Self {
85        Encoder {
86            table: DynamicTable::new(DEFAULT_MAX_SIZE),
87            pending_size_update: None,
88        }
89    }
90
91    /// Sets the maximum size of the table, at most the peer's
92    /// `SETTINGS_HEADER_TABLE_SIZE`, and evicts until it fits (§4.3). The
93    /// caller signals the change to the peer with a size update (§6.3) at the
94    /// start of its next block; [`Self::change_max_table_size`] records it for
95    /// [`Self::encode_size_updates_into`] instead.
96    pub fn set_max_table_size(&mut self, max_size: usize) {
97        self.table.set_max_size(max_size);
98    }
99
100    /// Sets the maximum size like [`Self::set_max_table_size`] and records the
101    /// change for the size updates that open the next block (§4.2): the
102    /// smallest size set since the last block, and the last one.
103    pub fn change_max_table_size(&mut self, max_size: usize) {
104        self.table.set_max_size(max_size);
105        let smallest = self
106            .pending_size_update
107            .map_or(max_size, |pending| pending.smallest.min(max_size));
108        self.pending_size_update = Some(PendingSizeUpdate {
109            smallest,
110            last: max_size,
111        });
112        debug_assert!(
113            smallest <= max_size,
114            "the smallest size reached never exceeds the last one"
115        );
116        debug_assert!(
117            self.pending_size_update
118                .is_some_and(|pending| pending.last == max_size),
119            "the change is recorded as the last one"
120        );
121    }
122
123    /// Appends the size updates (§6.3) that open a block after the maximum
124    /// size changed to `last_size`, and forgets the changes recorded by
125    /// [`Self::change_max_table_size`].
126    ///
127    /// When the size went below `last_size` since the last block, the
128    /// smallest size goes first (§4.2): the peer's decoder must evict what
129    /// this encoder evicted, or the two tables no longer hold the same
130    /// entries. `last_size` follows, alone when nothing smaller was set.
131    /// Returns the number of updates appended, 1 or 2 — the most a block may
132    /// open with.
133    pub fn encode_size_updates_into(&mut self, last_size: usize, out: &mut Vec<u8>) -> usize {
134        let pending = self.pending_size_update.take();
135        debug_assert!(
136            pending.is_none_or(|pending| pending.last == last_size),
137            "the size the caller signals is the last one recorded"
138        );
139        let start = out.len();
140        let mut updates = 0;
141        if let Some(pending) = pending
142            && pending.smallest < last_size
143        {
144            encode_integer(pending.smallest, 5, 0x20, out);
145            updates += 1;
146        }
147        encode_integer(last_size, 5, 0x20, out);
148        updates += 1;
149        debug_assert!(
150            (1..=2).contains(&updates),
151            "a block opens with at most two size updates (§4.2)"
152        );
153        debug_assert!(
154            out[start] & 0xe0 == 0x20 && self.pending_size_update.is_none(),
155            "the appended bytes open with a size update, and nothing stays recorded"
156        );
157        updates
158    }
159
160    /// Empties the dynamic table and records the size updates that make the
161    /// peer's decoder empty its own: `0`, then the current maximum size. RFC
162    /// 7541 §4.2 provides exactly this: "This mechanism can be used to
163    /// completely clear entries from the dynamic table by setting a maximum
164    /// size of 0, which can subsequently be restored." Returns that maximum
165    /// size, which the caller signals through
166    /// [`Self::encode_size_updates_into`] at the start of its next block.
167    ///
168    /// The table changes when a block is ENCODED, not when it reaches the
169    /// peer, so this is how a caller that dropped a block it encoded brings
170    /// the two tables back together (sozu-proxy/sozu#1627). Entries are
171    /// numbered from the newest (§2.3.3): every insertion the peer missed
172    /// moves the older entries down one index in this table only, and a later
173    /// block naming one of them makes the peer read another field, with no
174    /// error. After the reset both tables are empty, whatever the peer missed,
175    /// at the cost of the entries the next blocks would have reused.
176    ///
177    /// Sound only when no block encoded before this call reaches the peer
178    /// after the block that carries the updates: that later block would name
179    /// entries of the table this empties.
180    pub fn reset_table(&mut self) -> usize {
181        let max_size = self.table.max_size();
182        self.change_max_table_size(0);
183        self.change_max_table_size(max_size);
184        debug_assert!(
185            self.table.len() == 0 && self.table.max_size() == max_size,
186            "the table is empty and keeps its maximum size"
187        );
188        debug_assert_eq!(
189            self.pending_size_update,
190            Some(PendingSizeUpdate {
191                smallest: 0,
192                last: max_size,
193            }),
194            "the next block opens with 0, then the maximum size"
195        );
196        max_size
197    }
198
199    #[cfg(test)]
200    pub(super) fn table(&self) -> &DynamicTable {
201        &self.table
202    }
203
204    /// Appends the representation of one field to `out`.
205    pub fn encode_header_into(&mut self, (name, value): (&[u8], &[u8]), out: &mut Vec<u8>) {
206        self.encode_field(name, value, Representation::Proxy, false, out);
207    }
208
209    /// Appends the representation of every field to `out`, in order.
210    pub fn encode_into<'a, I>(&mut self, headers: I, out: &mut Vec<u8>)
211    where
212        I: IntoIterator<Item = (&'a [u8], &'a [u8])>,
213    {
214        for header in headers {
215            self.encode_header_into(header, out);
216        }
217    }
218
219    /// Encodes `headers` into a new block.
220    #[cfg(test)]
221    pub fn encode<'a, I>(&mut self, headers: I) -> Vec<u8>
222    where
223        I: IntoIterator<Item = (&'a [u8], &'a [u8])>,
224    {
225        let mut out = Vec::new();
226        self.encode_into(headers, &mut out);
227        out
228    }
229
230    /// Appends one field in the requested representation, the strings Huffman
231    /// coded when `huffman`.
232    pub fn encode_field(
233        &mut self,
234        name: &[u8],
235        value: &[u8],
236        representation: Representation,
237        huffman: bool,
238        out: &mut Vec<u8>,
239    ) {
240        let found = self.find(name, value);
241        match (representation, found) {
242            (Representation::Proxy | Representation::IncrementalIndexing, Found::Field(index)) => {
243                encode_integer(index, 7, 0x80, out);
244            }
245            (Representation::Proxy, Found::Name { last, .. }) => {
246                encode_integer(last, 4, 0x00, out);
247                encode_string(value, huffman, out);
248            }
249            (Representation::Proxy | Representation::IncrementalIndexing, Found::Nothing) => {
250                out.push(0x40);
251                encode_string(name, huffman, out);
252                encode_string(value, huffman, out);
253                self.table.insert(name, value);
254            }
255            (Representation::IncrementalIndexing, Found::Name { first, .. }) => {
256                encode_integer(first, 6, 0x40, out);
257                encode_string(value, huffman, out);
258                self.table.insert(name, value);
259            }
260            (Representation::WithoutIndexing | Representation::NeverIndexed, found) => {
261                let flags = if representation == Representation::NeverIndexed {
262                    0x10
263                } else {
264                    0x00
265                };
266                match found {
267                    Found::Field(index) | Found::Name { first: index, .. } => {
268                        encode_integer(index, 4, flags, out);
269                    }
270                    Found::Nothing => {
271                        out.push(flags);
272                        encode_string(name, huffman, out);
273                    }
274                }
275                encode_string(value, huffman, out);
276            }
277        }
278    }
279
280    /// Searches the static table, then the dynamic table from its newest
281    /// entry: the first index holding both name and value, else the first and
282    /// the last index holding the name.
283    fn find(&self, name: &[u8], value: &[u8]) -> Found {
284        let entries = DYNAMIC_TABLE_START + self.table.len();
285        let mut found = Found::Nothing;
286        let tables = STATIC_TABLE
287            .iter()
288            .copied()
289            .chain(self.table.iter())
290            .enumerate();
291        for (position, (entry_name, entry_value)) in tables {
292            if entry_name != name {
293                continue;
294            }
295            let index = position + 1;
296            if entry_value == value {
297                return Found::Field(index);
298            }
299            found = match found {
300                Found::Name { first, .. } => Found::Name { first, last: index },
301                _ => Found::Name {
302                    first: index,
303                    last: index,
304                },
305            };
306        }
307        debug_assert!(
308            match found {
309                Found::Field(index) => (1..entries).contains(&index),
310                Found::Name { first, last } => 0 < first && first <= last && last < entries,
311                Found::Nothing => true,
312            },
313            "a found index addresses an entry of one of the two tables"
314        );
315        found
316    }
317}
318
319/// Appends a string literal (§5.2).
320fn encode_string(string: &[u8], huffman: bool, out: &mut Vec<u8>) {
321    if huffman {
322        encode_integer(huffman::encoded_len(string), 7, 0x80, out);
323        huffman::encode(string, out);
324    } else {
325        encode_integer(string.len(), 7, 0x00, out);
326        out.extend_from_slice(string);
327    }
328}