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    let mask = (1u64 << prefix_bits) - 1;
50
51    if value < mask {
52        *out.get_mut(len).ok_or(Error::BufferTooSmall)? = flags | value as u8;
53        return Ok(len + 1);
54    }
55
56    *out.get_mut(len).ok_or(Error::BufferTooSmall)? = flags | mask as u8;
57    len += 1;
58    let mut remaining = value - mask;
59    while remaining >= 0x80 {
60        *out.get_mut(len).ok_or(Error::BufferTooSmall)? = (remaining as u8 & 0x7f) | 0x80;
61        len += 1;
62        remaining >>= 7;
63    }
64    *out.get_mut(len).ok_or(Error::BufferTooSmall)? = remaining as u8;
65    Ok(len + 1)
66}
67
68/// A length-prefixed literal string. The high bit of the length byte is zero,
69/// which is what says "not Huffman-coded".
70fn encode_string(text: &str, out: &mut [u8], len: usize) -> Result<usize, Error> {
71    let mut len = encode_integer(text.len() as u64, 7, 0x00, out, len)?;
72    let end = len + text.len();
73    out.get_mut(len..end)
74        .ok_or(Error::BufferTooSmall)?
75        .copy_from_slice(text.as_bytes());
76    len = end;
77    Ok(len)
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::hpack::Decoder;
84
85    #[test]
86    fn encodes_integers_as_the_rfc_section_5_1_examples_do() {
87        let mut out = [0u8; 8];
88        assert_eq!(encode_integer(10, 5, 0x00, &mut out, 0).unwrap(), 1);
89        assert_eq!(out[0], 0x0a);
90
91        let n = encode_integer(1337, 5, 0x00, &mut out, 0).unwrap();
92        assert_eq!(&out[..n], &[0x1f, 0x9a, 0x0a]);
93
94        let n = encode_integer(42, 8, 0x00, &mut out, 0).unwrap();
95        assert_eq!(&out[..n], &[0x2a]);
96    }
97
98    #[test]
99    fn a_header_matching_the_static_table_exactly_costs_one_byte() {
100        let mut out = [0u8; 64];
101        let n = encode_header(":method", "GET", &mut out, 0).unwrap();
102        assert_eq!(&out[..n], &[0x82]);
103
104        let n = encode_header(":scheme", "http", &mut out, 0).unwrap();
105        assert_eq!(&out[..n], &[0x86]);
106    }
107
108    /// The real test: whatever this encoder produces, the decoder — which was
109    /// itself checked against the RFC's own byte sequences — must read back
110    /// unchanged.
111    #[test]
112    fn everything_encoded_here_round_trips_through_the_decoder() {
113        let headers = [
114            (":method", "POST"),
115            (":path", "/machine/register"),
116            (":scheme", "http"),
117            (":authority", "127.0.0.1:8080"),
118            ("content-type", "application/json"),
119            ("content-length", "1234"),
120            ("x-tailscale-something", "a value with spaces and ünïcode"),
121        ];
122
123        let mut block = [0u8; 512];
124        let mut len = 0;
125        for (name, value) in headers {
126            len = encode_header(name, value, &mut block, len).unwrap();
127        }
128
129        let mut decoder = Decoder::new(4096);
130        let mut seen = 0;
131        decoder
132            .decode(&block[..len], |name, value| {
133                let (expected_name, expected_value) = headers[seen];
134                assert_eq!(name, expected_name);
135                assert_eq!(value, expected_value);
136                seen += 1;
137            })
138            .unwrap();
139        assert_eq!(seen, headers.len());
140    }
141
142    #[test]
143    fn nothing_we_emit_enters_the_peers_dynamic_table() {
144        // The property that makes an encoder-side table unnecessary. If any of
145        // these used incremental indexing, the peer's table would grow and our
146        // indices would have to track it.
147        let mut block = [0u8; 256];
148        let mut len = 0;
149        len = encode_header(":method", "POST", &mut block, len).unwrap();
150        len = encode_header("content-type", "application/json", &mut block, len).unwrap();
151        len = encode_header("custom", "value", &mut block, len).unwrap();
152
153        let mut decoder = Decoder::new(4096);
154        decoder.decode(&block[..len], |_, _| {}).unwrap();
155        assert!(decoder.table().is_empty());
156    }
157
158    #[test]
159    fn a_buffer_too_small_is_an_error_not_a_truncated_block() {
160        let mut out = [0u8; 4];
161        assert_eq!(
162            encode_header("content-type", "application/json", &mut out, 0),
163            Err(Error::BufferTooSmall)
164        );
165    }
166}