Skip to main content

micro_h2/hpack/
encode.rs

1//! Encoding a header block.
2//!
3//! Deliberately the simplest legal encoder. Every header goes out either as a
4//! bare static-table index, when name *and* value match one exactly, or as a
5//! literal without indexing — never Huffman-coded, never entered into a table.
6//!
7//! # Why give up the compression
8//!
9//! Because the alternative is the hardest bug in HPACK. An encoder that indexes
10//! has to keep a table in lockstep with the peer's copy of it, and a divergence
11//! does not fail: it makes the peer read subsequent headers as different
12//! headers. There is nothing to diverge here.
13//!
14//! The cost is bytes on a connection that carries two request shapes, both of
15//! them tiny next to the map responses coming back the other way. That is a good
16//! trade for a class of bug that would be very hard to find.
17
18use crate::Error;
19use crate::hpack::static_table;
20
21/// Append a header to `out`, returning the new length.
22pub fn encode_header(name: &str, value: &str, out: &mut [u8], len: usize) -> Result<usize, Error> {
23    if let Some(index) = static_table::find(name, value) {
24        // Indexed header field: one byte for most of the static table.
25        return encode_integer(index as u64, 7, 0x80, out, len);
26    }
27
28    let mut len = match static_table::find_name(name) {
29        // Literal without indexing, name from the table.
30        Some(index) => encode_integer(index as u64, 4, 0x00, out, len)?,
31        // Literal without indexing, new name.
32        None => {
33            let len = encode_integer(0, 4, 0x00, out, len)?;
34            encode_string(name, out, len)?
35        }
36    };
37    len = encode_string(value, out, len)?;
38    Ok(len)
39}
40
41/// RFC 7541 section 5.1, with `flags` supplying the bits above the prefix.
42pub fn encode_integer(
43    value: u64,
44    prefix_bits: u32,
45    flags: u8,
46    out: &mut [u8],
47    mut len: usize,
48) -> Result<usize, Error> {
49    if !(1..=8).contains(&prefix_bits) {
50        return Err(Error::Hpack);
51    }
52    let mask = (1u64 << prefix_bits) - 1;
53
54    if value < mask {
55        *out.get_mut(len).ok_or(Error::BufferTooSmall)? = flags | value as u8;
56        return Ok(len + 1);
57    }
58
59    *out.get_mut(len).ok_or(Error::BufferTooSmall)? = flags | mask as u8;
60    len += 1;
61    let mut remaining = value - mask;
62    while remaining >= 0x80 {
63        *out.get_mut(len).ok_or(Error::BufferTooSmall)? = (remaining as u8 & 0x7f) | 0x80;
64        len += 1;
65        remaining >>= 7;
66    }
67    *out.get_mut(len).ok_or(Error::BufferTooSmall)? = remaining as u8;
68    Ok(len + 1)
69}
70
71/// A length-prefixed literal string. The high bit of the length byte is zero,
72/// which is what says "not Huffman-coded".
73fn encode_string(text: &str, out: &mut [u8], len: usize) -> Result<usize, Error> {
74    let mut len = encode_integer(text.len() as u64, 7, 0x00, out, len)?;
75    let end = len + text.len();
76    out.get_mut(len..end)
77        .ok_or(Error::BufferTooSmall)?
78        .copy_from_slice(text.as_bytes());
79    len = end;
80    Ok(len)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::hpack::Decoder;
87
88    #[test]
89    fn encodes_integers_as_the_rfc_section_5_1_examples_do() {
90        let mut out = [0u8; 8];
91        assert_eq!(encode_integer(10, 5, 0x00, &mut out, 0).unwrap(), 1);
92        assert_eq!(out[0], 0x0a);
93
94        let n = encode_integer(1337, 5, 0x00, &mut out, 0).unwrap();
95        assert_eq!(&out[..n], &[0x1f, 0x9a, 0x0a]);
96
97        let n = encode_integer(42, 8, 0x00, &mut out, 0).unwrap();
98        assert_eq!(&out[..n], &[0x2a]);
99    }
100
101    #[test]
102    fn a_header_matching_the_static_table_exactly_costs_one_byte() {
103        let mut out = [0u8; 64];
104        let n = encode_header(":method", "GET", &mut out, 0).unwrap();
105        assert_eq!(&out[..n], &[0x82]);
106
107        let n = encode_header(":scheme", "http", &mut out, 0).unwrap();
108        assert_eq!(&out[..n], &[0x86]);
109    }
110
111    /// The real test: whatever this encoder produces, the decoder — which was
112    /// itself checked against the RFC's own byte sequences — must read back
113    /// unchanged.
114    #[test]
115    fn everything_encoded_here_round_trips_through_the_decoder() {
116        let headers = [
117            (":method", "POST"),
118            (":path", "/machine/register"),
119            (":scheme", "http"),
120            (":authority", "127.0.0.1:8080"),
121            ("content-type", "application/json"),
122            ("content-length", "1234"),
123            ("x-tailscale-something", "a value with spaces and ünïcode"),
124        ];
125
126        let mut block = [0u8; 512];
127        let mut len = 0;
128        for (name, value) in headers {
129            len = encode_header(name, value, &mut block, len).unwrap();
130        }
131
132        let mut decoder = Decoder::new(4096);
133        let mut seen = 0;
134        decoder
135            .decode(&block[..len], |name, value| {
136                let (expected_name, expected_value) = headers[seen];
137                assert_eq!(name, expected_name);
138                assert_eq!(value, expected_value);
139                seen += 1;
140            })
141            .unwrap();
142        assert_eq!(seen, headers.len());
143    }
144
145    #[test]
146    fn nothing_we_emit_enters_the_peers_dynamic_table() {
147        // The property that makes an encoder-side table unnecessary. If any of
148        // these used incremental indexing, the peer's table would grow and our
149        // indices would have to track it.
150        let mut block = [0u8; 256];
151        let mut len = 0;
152        len = encode_header(":method", "POST", &mut block, len).unwrap();
153        len = encode_header("content-type", "application/json", &mut block, len).unwrap();
154        len = encode_header("custom", "value", &mut block, len).unwrap();
155
156        let mut decoder = Decoder::new(4096);
157        decoder.decode(&block[..len], |_, _| {}).unwrap();
158        assert!(decoder.table().is_empty());
159    }
160
161    #[test]
162    fn a_buffer_too_small_is_an_error_not_a_truncated_block() {
163        let mut out = [0u8; 4];
164        assert_eq!(
165            encode_header("content-type", "application/json", &mut out, 0),
166            Err(Error::BufferTooSmall)
167        );
168    }
169}