Skip to main content

specter/transport/h2/hpack_impl/
encoder.rs

1//! HPACK encoder (RFC 7541).
2
3use super::dynamic_table::DynamicTable;
4use super::huffman::huffman_encode_if_smaller;
5use super::integer::encode_integer;
6use super::static_table::{find_static_entry, find_static_entry_by_name};
7
8const STATIC_TABLE_SIZE: usize = 61;
9
10/// HPACK encoder.
11pub struct Encoder {
12    dynamic_table: DynamicTable,
13}
14
15impl Encoder {
16    /// Create a new encoder.
17    pub fn new() -> Self {
18        Self {
19            dynamic_table: DynamicTable::new(4096),
20        }
21    }
22
23    /// Set the maximum dynamic table size.
24    pub fn set_max_table_size(&mut self, size: usize) {
25        self.dynamic_table.set_max_size(size);
26    }
27
28    /// Encode a list of headers.
29    ///
30    /// Headers should be provided as a slice of (name, value) byte slices.
31    pub fn encode(&mut self, headers: &[(&[u8], &[u8])]) -> Vec<u8> {
32        let mut output = Vec::new();
33
34        for (name, value) in headers {
35            self.encode_header(name, value, &mut output);
36        }
37
38        output
39    }
40
41    /// Encode a single header field.
42    fn encode_header(&mut self, name: &[u8], value: &[u8], output: &mut Vec<u8>) {
43        // Try to find exact match (name + value) in static table
44        if let Some(static_idx) = find_static_entry(name, value) {
45            // Indexed header field representation (RFC 7541 Section 6.1)
46            // Prefix: 1xxxxxxx (7-bit index)
47            // Always push a new byte for this header
48            output.push(0x80); // Set top bit
49            encode_integer(static_idx, 7, 0x7F, output).unwrap();
50            return;
51        }
52
53        // Try to find exact match in dynamic table
54        if let Some(dynamic_idx) = self.dynamic_table.find(name, value) {
55            let combined_idx = STATIC_TABLE_SIZE + dynamic_idx;
56            // Indexed header field representation
57            // Always push a new byte for this header
58            output.push(0x80);
59            encode_integer(combined_idx, 7, 0x7F, output).unwrap();
60            return;
61        }
62
63        // Try to find name match in static table
64        if let Some(static_name_idx) = find_static_entry_by_name(name) {
65            // Literal header field with incremental indexing (RFC 7541 Section 6.2.1)
66            // Prefix: 01xxxxxx (6-bit index)
67            // Always push a new byte for this header
68            output.push(0x40); // Set top 2 bits to 01
69            encode_integer(static_name_idx, 6, 0x3F, output).unwrap();
70            self.encode_string_literal(value, output);
71
72            // Add to dynamic table
73            self.dynamic_table.add(name.to_vec(), value.to_vec());
74            return;
75        }
76
77        // Try to find name match in dynamic table
78        if let Some(dynamic_name_idx) = self.dynamic_table.find_by_name(name) {
79            let combined_name_idx = STATIC_TABLE_SIZE + dynamic_name_idx;
80            // Literal header field with incremental indexing
81            // Always push a new byte for this header
82            output.push(0x40);
83            encode_integer(combined_name_idx, 6, 0x3F, output).unwrap();
84            self.encode_string_literal(value, output);
85
86            // Add to dynamic table
87            self.dynamic_table.add(name.to_vec(), value.to_vec());
88            return;
89        }
90
91        // New name: literal header field with incremental indexing
92        // Prefix: 01xxxxxx, index = 0 means new name
93        // Always push a new byte for this header
94        output.push(0x40);
95        encode_integer(0, 6, 0x3F, output).unwrap();
96        self.encode_string_literal(name, output);
97        self.encode_string_literal(value, output);
98
99        // Add to dynamic table
100        self.dynamic_table.add(name.to_vec(), value.to_vec());
101    }
102
103    /// Encode a string literal (RFC 7541 Section 5.2).
104    fn encode_string_literal(&self, input: &[u8], output: &mut Vec<u8>) {
105        let (encoded, use_huffman) = huffman_encode_if_smaller(input);
106
107        // Write H flag and length (7-bit prefix)
108        // Always start a new byte for the string literal header
109        output.push(if use_huffman { 0x80 } else { 0x00 });
110        encode_integer(encoded.len(), 7, 0x7F, output).unwrap();
111        output.extend_from_slice(&encoded);
112    }
113}
114
115impl Default for Encoder {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_encode_static_entry() {
127        let mut encoder = Encoder::new();
128        let headers = [(b":method".as_slice(), b"GET".as_slice())];
129        let encoded = encoder.encode(&headers);
130        // Should encode as indexed header (index 2)
131        assert!(!encoded.is_empty());
132        assert_eq!(encoded[0] & 0x80, 0x80); // Top bit set
133    }
134
135    #[test]
136    fn test_encode_literal() {
137        let mut encoder = Encoder::new();
138        let headers = [(b"custom-key".as_slice(), b"custom-value".as_slice())];
139        let encoded = encoder.encode(&headers);
140        // Should encode as literal with incremental indexing
141        assert!(!encoded.is_empty());
142        assert_eq!(encoded[0] & 0xC0, 0x40); // Top 2 bits: 01
143    }
144
145    #[test]
146    fn test_encode_multiple_headers() {
147        let mut encoder = Encoder::new();
148        let headers = [
149            (b":method".as_slice(), b"GET".as_slice()),
150            (b":scheme".as_slice(), b"http".as_slice()),
151            (b":path".as_slice(), b"/".as_slice()),
152        ];
153        let encoded = encoder.encode(&headers);
154        assert!(!encoded.is_empty());
155    }
156}