Skip to main content

simoom_hpack/
encoder.rs

1//! The header block encoder (RFC 7541 §6, §7.1).
2
3use crate::table::{static_name, DynTable, Name, STATIC_TABLE};
4use crate::{bytes_eq, encode_integer, encode_string, field_size, DEFAULT_TABLE_SIZE};
5
6/// Fields whose values must never enter a compression context (RFC 7541 §7.1.3): they are
7/// sent as never-indexed literals, so intermediaries keep them out of their tables too.
8pub fn is_sensitive(name: &[u8]) -> bool {
9    matches!(name, b"authorization" | b"proxy-authorization" | b"cookie" | b"set-cookie")
10}
11
12/// Static indices (bits of a mask) of the sensitive names: `authorization` (23), `cookie` (32),
13/// `proxy-authorization` (49), `set-cookie` (55). They are all in the static table.
14const SENSITIVE: u64 = 1 << 23 | 1 << 32 | 1 << 49 | 1 << 55;
15
16/// Static indices of names whose values rarely repeat: `age` (21), `content-length` (28),
17/// `content-range` (30), `etag` (34), `if-match` (39), `if-modified-since` (40),
18/// `if-none-match` (41), `last-modified` (44), `location` (46).
19const RARE: u64 = 1 << 21 | 1 << 28 | 1 << 30 | 1 << 34 | 1 << 39 | 1 << 40 | 1 << 41 | 1 << 44 | 1 << 46;
20
21/// Fields whose values rarely repeat on a connection: indexing them would only churn the table.
22/// `idx` is the name's first static index (0: not in the table).
23fn rarely_repeats(idx: usize, name: &[u8], value: &[u8]) -> bool {
24    match idx {
25        4 => value.len() > 64 || value.contains(&b'?'), // :path
26        0 => matches!(name, b"x-request-id" | b"traceparent" | b"tracestate"),
27        i => RARE >> i & 1 != 0,
28    }
29}
30
31/// A stateful header block encoder.
32///
33/// Exact static or dynamic matches are sent as indexed fields; other fields are literals with
34/// incremental indexing, except sensitive fields (never indexed), fields whose values rarely
35/// repeat, and fields larger than half the table (without indexing). Strings are Huffman coded
36/// when that is shorter.
37#[derive(Debug, Clone)]
38pub struct Encoder {
39    table: DynTable,
40    /// The largest table we are willing to keep, whatever the peer allows.
41    limit: usize,
42    /// Smallest size the table passed through since the last block, and whether an update is due.
43    pending_min: usize,
44    pending: bool,
45    /// Use Huffman coding when it is shorter (default true).
46    pub huffman: bool,
47    /// Add literals to the dynamic table (default true).
48    pub indexing: bool,
49}
50
51impl Default for Encoder {
52    fn default() -> Self {
53        Encoder::new(DEFAULT_TABLE_SIZE)
54    }
55}
56
57impl Encoder {
58    /// An encoder that keeps at most `limit` bytes of dynamic table. The peer's table starts at
59    /// 4096 (the protocol default); a smaller limit is announced in the first block.
60    pub fn new(limit: usize) -> Self {
61        let size = limit.min(DEFAULT_TABLE_SIZE);
62        Encoder {
63            table: DynTable::new(size),
64            limit,
65            pending_min: size,
66            pending: size != DEFAULT_TABLE_SIZE,
67            huffman: true,
68            indexing: true,
69        }
70    }
71
72    /// Applies the peer's SETTINGS_HEADER_TABLE_SIZE. The encoder uses the smaller of that and
73    /// its own limit, and announces the change at the start of the next block.
74    pub fn set_peer_max_table_size(&mut self, peer: usize) {
75        let size = peer.min(self.limit);
76        if size != self.table.max() {
77            self.pending_min = self.pending_min.min(size);
78            self.pending = true;
79            self.table.set_max(size);
80        }
81    }
82
83    /// Current dynamic table size in octets.
84    pub fn table_size(&self) -> usize {
85        self.table.size()
86    }
87
88    /// Number of dynamic table entries.
89    pub fn table_len(&self) -> usize {
90        self.table.len()
91    }
92
93    /// Starts a header block: emits any pending dynamic table size updates (RFC 7541 §4.2).
94    pub fn begin_block(&mut self, out: &mut Vec<u8>) {
95        if self.pending {
96            let max = self.table.max();
97            if self.pending_min < max {
98                encode_integer(self.pending_min, 5, 0x20, out);
99            }
100            encode_integer(max, 5, 0x20, out);
101            self.pending = false;
102            self.pending_min = max;
103        }
104    }
105
106    /// Encodes a whole header block.
107    pub fn encode(&mut self, fields: &[(&[u8], &[u8])], out: &mut Vec<u8>) {
108        self.begin_block(out);
109        for (n, v) in fields {
110            self.field(n, v, out);
111        }
112    }
113
114    /// Encodes one field of the current block (after [`Encoder::begin_block`]).
115    pub fn field(&mut self, name: &[u8], value: &[u8], out: &mut Vec<u8>) {
116        let mut name_idx = 0usize;
117        if let Some((first, count)) = static_name(name) {
118            name_idx = first;
119            for i in first..first + count {
120                if bytes_eq(STATIC_TABLE[i - 1].1, value) {
121                    encode_integer(i, 7, 0x80, out);
122                    return;
123                }
124            }
125        }
126        let static_idx = name_idx;
127        // the same as `is_sensitive(name)`, from the static index
128        let sensitive = SENSITIVE >> static_idx & 1 != 0;
129        let (full, by_name) = self.table.find(name, value);
130        if let (Some(i), false) = (full, sensitive) {
131            encode_integer(STATIC_TABLE.len() + 1 + i, 7, 0x80, out);
132            return;
133        }
134        if let (0, Some(i)) = (name_idx, by_name) {
135            name_idx = STATIC_TABLE.len() + 1 + i;
136        }
137        let index = self.indexing
138            && !sensitive
139            && !rarely_repeats(static_idx, name, value)
140            && field_size(name, value) <= self.table.max() / 2;
141        let (prefix, first) = if sensitive {
142            (4, 0x10)
143        } else if index {
144            (6, 0x40)
145        } else {
146            (4, 0x00)
147        };
148        encode_integer(name_idx, prefix, first, out);
149        if name_idx == 0 {
150            encode_string(name, self.huffman, out);
151        }
152        encode_string(value, self.huffman, out);
153        if index {
154            self.table.insert(Name::Bytes(name), value);
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    /// The index masks agree with the names they stand for.
164    #[test]
165    fn masks_match_the_names() {
166        let rare: [&[u8]; 9] = [b"content-length", b"content-range", b"etag", b"if-none-match", b"if-modified-since", b"if-match", b"last-modified", b"age", b"location"];
167        for (i, (n, _)) in STATIC_TABLE.iter().enumerate() {
168            let first = static_name(n).unwrap().0;
169            if first != i + 1 {
170                continue;
171            }
172            assert_eq!(SENSITIVE >> first & 1 != 0, is_sensitive(n), "{}", String::from_utf8_lossy(n));
173            if first != 4 {
174                assert_eq!(rarely_repeats(first, n, b"x"), rare.contains(n), "{}", String::from_utf8_lossy(n));
175            }
176        }
177        assert!(rarely_repeats(4, b":path", b"/a?b=1"));
178        assert!(!rarely_repeats(4, b":path", b"/a"));
179        for n in [&b"x-request-id"[..], b"traceparent", b"tracestate"] {
180            assert!(rarely_repeats(0, n, b"1"));
181        }
182        assert!(!rarely_repeats(0, b"x-other", b"1"));
183    }
184}