Skip to main content

zerodds_hpack/
decoder.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! HPACK Decoder — RFC 7541 §6.
5
6use alloc::vec::Vec;
7
8use crate::integer::{IntegerError, decode_integer};
9use crate::string::{StringError, decode_string};
10use crate::table::{HeaderField, Table};
11
12/// Decoder-Fehler.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum DecoderError {
15    /// Index ist 0 oder ueber Table-Range.
16    InvalidIndex(u64),
17    /// Integer-Decode-Fehler.
18    Integer(IntegerError),
19    /// String-Decode-Fehler.
20    String(StringError),
21    /// Buffer ist truncated.
22    Truncated,
23}
24
25impl core::fmt::Display for DecoderError {
26    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
27        match self {
28            Self::InvalidIndex(i) => write!(f, "invalid index {i}"),
29            Self::Integer(e) => write!(f, "integer: {e}"),
30            Self::String(e) => write!(f, "string: {e}"),
31            Self::Truncated => f.write_str("input truncated"),
32        }
33    }
34}
35
36#[cfg(feature = "std")]
37impl std::error::Error for DecoderError {}
38
39impl From<IntegerError> for DecoderError {
40    fn from(e: IntegerError) -> Self {
41        Self::Integer(e)
42    }
43}
44
45impl From<StringError> for DecoderError {
46    fn from(e: StringError) -> Self {
47        Self::String(e)
48    }
49}
50
51/// HPACK-Decoder.
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct Decoder {
54    table: Table,
55}
56
57impl Decoder {
58    /// Konstruktor.
59    #[must_use]
60    pub fn new() -> Self {
61        Self::default()
62    }
63
64    /// Konstruktor mit Max-Size.
65    #[must_use]
66    pub fn with_max_size(max: usize) -> Self {
67        Self {
68            table: Table::new(max),
69        }
70    }
71
72    /// Reference auf die Dynamic-Table.
73    #[must_use]
74    pub fn table(&self) -> &Table {
75        &self.table
76    }
77
78    /// Mut-Reference auf die Dynamic-Table (z.B. fuer Max-Size-Update).
79    pub fn table_mut(&mut self) -> &mut Table {
80        &mut self.table
81    }
82
83    /// Decode einen kompletten Header-Block.
84    ///
85    /// # Errors
86    /// Siehe [`DecoderError`].
87    pub fn decode(&mut self, mut input: &[u8]) -> Result<Vec<HeaderField>, DecoderError> {
88        let mut out = Vec::new();
89        while !input.is_empty() {
90            let first = input[0];
91            if first & 0x80 != 0 {
92                // §6.1 Indexed Header Field.
93                let (index, consumed) = decode_integer(input, 7)?;
94                input = &input[consumed..];
95                if index == 0 {
96                    return Err(DecoderError::InvalidIndex(0));
97                }
98                let h = self
99                    .table
100                    .get(index as usize)
101                    .ok_or(DecoderError::InvalidIndex(index))?;
102                out.push(h);
103            } else if first & 0xc0 == 0x40 {
104                // §6.2.1 Literal with Incremental Indexing.
105                let (index, consumed) = decode_integer(input, 6)?;
106                input = &input[consumed..];
107                let name = if index == 0 {
108                    let (s, c) = decode_string(input)?;
109                    input = &input[c..];
110                    s
111                } else {
112                    self.table
113                        .get(index as usize)
114                        .ok_or(DecoderError::InvalidIndex(index))?
115                        .name
116                };
117                let (value, c) = decode_string(input)?;
118                input = &input[c..];
119                let h = HeaderField { name, value };
120                self.table.add(h.clone());
121                out.push(h);
122            } else if first & 0xe0 == 0x20 {
123                // §6.3 Dynamic Table Size Update.
124                let (new_size, consumed) = decode_integer(input, 5)?;
125                input = &input[consumed..];
126                self.table.set_max_size(new_size as usize);
127            } else if first & 0xf0 == 0x00 || first & 0xf0 == 0x10 {
128                // §6.2.2 Literal without indexing OR §6.2.3 never indexed.
129                let prefix_bits = 4u8;
130                let (index, consumed) = decode_integer(input, prefix_bits)?;
131                input = &input[consumed..];
132                let name = if index == 0 {
133                    let (s, c) = decode_string(input)?;
134                    input = &input[c..];
135                    s
136                } else {
137                    self.table
138                        .get(index as usize)
139                        .ok_or(DecoderError::InvalidIndex(index))?
140                        .name
141                };
142                let (value, c) = decode_string(input)?;
143                input = &input[c..];
144                out.push(HeaderField { name, value });
145            } else {
146                return Err(DecoderError::Truncated);
147            }
148        }
149        Ok(out)
150    }
151}
152
153#[cfg(test)]
154#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
155mod tests {
156    use super::*;
157    use crate::encoder::Encoder;
158
159    fn hf(n: &str, v: &str) -> HeaderField {
160        HeaderField {
161            name: n.into(),
162            value: v.into(),
163        }
164    }
165
166    #[test]
167    fn round_trip_static_match() {
168        let mut e = Encoder::new();
169        let mut d = Decoder::new();
170        let headers = alloc::vec![hf(":method", "GET"), hf(":scheme", "https")];
171        let buf = e.encode(&headers);
172        let decoded = d.decode(&buf).unwrap();
173        assert_eq!(decoded, headers);
174    }
175
176    #[test]
177    fn round_trip_literal_with_indexing() {
178        let mut e = Encoder::new();
179        let mut d = Decoder::new();
180        let headers = alloc::vec![hf("custom-key", "custom-value")];
181        let buf = e.encode(&headers);
182        let decoded = d.decode(&buf).unwrap();
183        assert_eq!(decoded, headers);
184        // Both should have synced dynamic tables.
185        assert_eq!(d.table().len(), 1);
186    }
187
188    #[test]
189    fn round_trip_indexed_match_after_first() {
190        let mut e = Encoder::new();
191        let mut d = Decoder::new();
192        let headers = alloc::vec![hf("foo", "bar")];
193        let _ = d.decode(&e.encode(&headers)).unwrap();
194        // Encode again — encoder finds it in table → indexed.
195        let buf = e.encode(&headers);
196        let decoded = d.decode(&buf).unwrap();
197        assert_eq!(decoded, headers);
198    }
199
200    #[test]
201    fn round_trip_huffman_strings() {
202        let mut e = Encoder::with_max_size(4096);
203        e.use_huffman = true;
204        let mut d = Decoder::with_max_size(4096);
205        let headers = alloc::vec![hf("custom-key", "custom-value")];
206        let buf = e.encode(&headers);
207        let decoded = d.decode(&buf).unwrap();
208        assert_eq!(decoded, headers);
209    }
210
211    #[test]
212    fn invalid_index_zero_rejected() {
213        let mut d = Decoder::new();
214        // 0x80 = Indexed with index 0 → invalid.
215        let buf = alloc::vec![0x80];
216        assert!(matches!(d.decode(&buf), Err(DecoderError::InvalidIndex(0))));
217    }
218
219    #[test]
220    fn dynamic_table_size_update_applied() {
221        let mut d = Decoder::new();
222        // 0x20 = size update with prefix 5 bits → 0 size.
223        let buf = alloc::vec![0x20];
224        d.decode(&buf).unwrap();
225        assert_eq!(d.table().max_size(), 0);
226    }
227
228    #[test]
229    fn rfc7541_c2_1_literal_with_indexing() {
230        // Spec §C.2.1 — "custom-key: custom-header"
231        // Expected encoding (literal name + value, no Huffman):
232        //   400a 6375 7374 6f6d 2d6b 6579 0d63 7573 746f 6d2d 6865 6164 6572
233        let mut d = Decoder::new();
234        let buf = alloc::vec![
235            0x40, 0x0a, b'c', b'u', b's', b't', b'o', b'm', b'-', b'k', b'e', b'y', 0x0d, b'c',
236            b'u', b's', b't', b'o', b'm', b'-', b'h', b'e', b'a', b'd', b'e', b'r',
237        ];
238        let decoded = d.decode(&buf).unwrap();
239        assert_eq!(decoded, alloc::vec![hf("custom-key", "custom-header")]);
240    }
241
242    #[test]
243    fn literal_without_indexing_does_not_grow_table() {
244        let mut d = Decoder::new();
245        // 0x00 = Literal w/o indexing, new name.
246        // followed by name and value strings.
247        let buf = alloc::vec![0x00, 0x03, b'a', b'b', b'c', 0x03, b'1', b'2', b'3',];
248        let decoded = d.decode(&buf).unwrap();
249        assert_eq!(decoded, alloc::vec![hf("abc", "123")]);
250        assert!(d.table().is_empty());
251    }
252}