Skip to main content

hpack_html/
decoder.rs

1use crate::varint::decode_varint;
2use crate::Error;
3use std::collections::HashMap;
4
5// Format constants (must match TypeScript)
6const MAGIC: [u8; 4] = [0x89, 0x48, 0x50, 0x4B];
7const VERSION: u8 = 0x01;
8const FLAG_MINIFIED: u8 = 1 << 0;
9const FLAG_CHECKSUM: u8 = 1 << 1;
10const FLAG_COMPRESSION_MASK: u8 = 0b00001100;
11const FLAG_COMPRESSION_SHIFT: u8 = 2;
12const CRC32_SIZE: usize = 4;
13const MIN_PACKET_SIZE: usize = 7; // magic(4) + version(1) + flags(1) + headerLen(1)
14const FIELD_CUSTOM_START: u8 = 0x10;
15
16// Known field type IDs
17const FIELD_URL: u8 = 0x01;
18const FIELD_ETAG: u8 = 0x02;
19const FIELD_SIGNATURE: u8 = 0x03;
20const FIELD_CONTENT_TYPE: u8 = 0x04;
21const FIELD_TIMESTAMP: u8 = 0x05;
22const FIELD_ENCODING: u8 = 0x06;
23
24/// Compression algorithm.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum CompressionAlgorithm {
27    Deflate,
28    Gzip,
29}
30
31/// Result of unpacking an .hpack packet.
32#[derive(Debug)]
33pub struct UnpackResult {
34    pub url: String,
35    pub etag: Option<String>,
36    pub signature: Option<String>,
37    pub content_type: Option<String>,
38    pub timestamp: Option<u64>,
39    pub encoding: Option<String>,
40    pub custom: HashMap<String, String>,
41    pub html: String,
42    pub version: u8,
43    pub minified: bool,
44    pub compression: CompressionAlgorithm,
45    pub checksum_valid: Option<bool>,
46}
47
48/// Options for unpacking.
49pub struct UnpackOptions {
50    pub verify_checksum: bool,
51    pub headers_only: bool,
52}
53
54impl Default for UnpackOptions {
55    fn default() -> Self {
56        Self {
57            verify_checksum: true,
58            headers_only: false,
59        }
60    }
61}
62
63/// Internal decoded header field.
64struct HeaderField {
65    field_type: u8,
66    name: Option<String>,
67    value: String,
68}
69
70/// Decode the packet header and body structure.
71#[allow(clippy::type_complexity)]
72fn decode_packet(
73    data: &[u8],
74) -> Result<
75    (
76        u8,                   // version
77        bool,                 // minified
78        bool,                 // has_checksum
79        CompressionAlgorithm, // compression
80        Vec<HeaderField>,     // fields
81        &[u8],                // compressed_body
82        usize,                // uncompressed_length
83        Option<u32>,          // crc32
84    ),
85    Error,
86> {
87    if data.len() < MIN_PACKET_SIZE {
88        return Err(Error::TruncatedPacket(format!(
89            "Packet too small: {} bytes (minimum: {})",
90            data.len(),
91            MIN_PACKET_SIZE
92        )));
93    }
94
95    let mut offset = 0;
96
97    // Magic
98    if data[0..4] != MAGIC {
99        return Err(Error::InvalidMagic);
100    }
101    offset += 4;
102
103    // Version
104    let version = data[offset];
105    if version != VERSION {
106        return Err(Error::UnsupportedVersion(version));
107    }
108    offset += 1;
109
110    // Flags
111    let flags_byte = data[offset];
112    offset += 1;
113    let minified = flags_byte & FLAG_MINIFIED != 0;
114    let has_checksum = flags_byte & FLAG_CHECKSUM != 0;
115    let compression_id = (flags_byte & FLAG_COMPRESSION_MASK) >> FLAG_COMPRESSION_SHIFT;
116    let compression = match compression_id {
117        1 => CompressionAlgorithm::Gzip,
118        _ => CompressionAlgorithm::Deflate,
119    };
120
121    // Header length
122    let (header_len, n) = decode_varint(data, offset)?;
123    offset += n;
124    let header_len = header_len as usize;
125    let header_end = offset + header_len;
126
127    if header_end > data.len() {
128        return Err(Error::TruncatedPacket(
129            "Header section extends beyond packet".into(),
130        ));
131    }
132
133    // Field count
134    let (field_count, n) = decode_varint(data, offset)?;
135    offset += n;
136
137    // Fields
138    let mut fields = Vec::with_capacity(field_count as usize);
139    for i in 0..field_count as usize {
140        if offset >= header_end {
141            return Err(Error::TruncatedPacket(format!(
142                "Header field {} extends beyond header section",
143                i
144            )));
145        }
146
147        let field_type = data[offset];
148        offset += 1;
149
150        if field_type >= FIELD_CUSTOM_START {
151            // Custom field
152            if offset >= header_end {
153                return Err(Error::TruncatedPacket(format!(
154                    "Custom field {} name length missing",
155                    i
156                )));
157            }
158            let name_len = data[offset] as usize;
159            offset += 1;
160
161            if offset + name_len > header_end {
162                return Err(Error::TruncatedPacket(format!(
163                    "Custom field {} name truncated",
164                    i
165                )));
166            }
167            let name = String::from_utf8_lossy(&data[offset..offset + name_len]).into_owned();
168            offset += name_len;
169
170            let (value_len, n) = decode_varint(data, offset)?;
171            offset += n;
172            let value_len = value_len as usize;
173
174            if offset + value_len > header_end {
175                return Err(Error::TruncatedPacket(format!(
176                    "Custom field {} value truncated",
177                    i
178                )));
179            }
180            let value = String::from_utf8_lossy(&data[offset..offset + value_len]).into_owned();
181            offset += value_len;
182
183            fields.push(HeaderField {
184                field_type,
185                name: Some(name),
186                value,
187            });
188        } else {
189            // Known field
190            let (value_len, n) = decode_varint(data, offset)?;
191            offset += n;
192            let value_len = value_len as usize;
193
194            if offset + value_len > header_end {
195                return Err(Error::TruncatedPacket(format!(
196                    "Field {} (type 0x{:02X}) value truncated",
197                    i, field_type
198                )));
199            }
200            let value = String::from_utf8_lossy(&data[offset..offset + value_len]).into_owned();
201            offset += value_len;
202
203            fields.push(HeaderField {
204                field_type,
205                name: None,
206                value,
207            });
208        }
209    }
210
211    // Skip remaining header bytes
212    offset = header_end;
213
214    // Body section
215    if offset >= data.len() {
216        return Err(Error::TruncatedPacket("Body section missing".into()));
217    }
218
219    let (uncompressed_len, n) = decode_varint(data, offset)?;
220    offset += n;
221
222    let crc_size = if has_checksum { CRC32_SIZE } else { 0 };
223    let compressed_body_end = data.len() - crc_size;
224
225    if offset > compressed_body_end {
226        return Err(Error::TruncatedPacket("Compressed body truncated".into()));
227    }
228
229    let compressed_body = &data[offset..compressed_body_end];
230
231    let crc32 = if has_checksum {
232        let crc_offset = compressed_body_end;
233        if crc_offset + CRC32_SIZE > data.len() {
234            return Err(Error::TruncatedPacket("CRC32 checksum truncated".into()));
235        }
236        Some(
237            data[crc_offset] as u32
238                | (data[crc_offset + 1] as u32) << 8
239                | (data[crc_offset + 2] as u32) << 16
240                | (data[crc_offset + 3] as u32) << 24,
241        )
242    } else {
243        None
244    };
245
246    Ok((
247        version,
248        minified,
249        has_checksum,
250        compression,
251        fields,
252        compressed_body,
253        uncompressed_len as usize,
254        crc32,
255    ))
256}
257
258/// Extract metadata from header fields.
259#[allow(clippy::type_complexity)]
260fn extract_fields(
261    fields: &[HeaderField],
262) -> (
263    String,
264    Option<String>,
265    Option<String>,
266    Option<String>,
267    Option<u64>,
268    Option<String>,
269    HashMap<String, String>,
270) {
271    let mut url = String::new();
272    let mut etag = None;
273    let mut signature = None;
274    let mut content_type = None;
275    let mut timestamp = None;
276    let mut encoding = None;
277    let mut custom = HashMap::new();
278
279    for field in fields {
280        match field.field_type {
281            FIELD_URL => url = field.value.clone(),
282            FIELD_ETAG => etag = Some(field.value.clone()),
283            FIELD_SIGNATURE => signature = Some(field.value.clone()),
284            FIELD_CONTENT_TYPE => content_type = Some(field.value.clone()),
285            FIELD_TIMESTAMP => timestamp = field.value.parse().ok(),
286            FIELD_ENCODING => encoding = Some(field.value.clone()),
287            t if t >= FIELD_CUSTOM_START => {
288                if let Some(name) = &field.name {
289                    custom.insert(name.clone(), field.value.clone());
290                }
291            }
292            _ => {} // skip unknown known-range fields
293        }
294    }
295
296    (
297        url,
298        etag,
299        signature,
300        content_type,
301        timestamp,
302        encoding,
303        custom,
304    )
305}
306
307/// Unpack an .hpack binary packet.
308pub fn unpack(data: &[u8]) -> Result<UnpackResult, Error> {
309    unpack_with_options(data, &UnpackOptions::default())
310}
311
312/// Unpack with options.
313pub fn unpack_with_options(data: &[u8], options: &UnpackOptions) -> Result<UnpackResult, Error> {
314    let (
315        version,
316        minified,
317        _has_checksum,
318        compression,
319        fields,
320        compressed_body,
321        _uncompressed_len,
322        crc32_value,
323    ) = decode_packet(data)?;
324
325    let (url, etag, signature, content_type, timestamp, encoding, custom) = extract_fields(&fields);
326
327    if options.headers_only {
328        return Ok(UnpackResult {
329            url,
330            etag,
331            signature,
332            content_type,
333            timestamp,
334            encoding,
335            custom,
336            html: String::new(),
337            version,
338            minified,
339            compression,
340            checksum_valid: None,
341        });
342    }
343
344    // Decompress
345    let decompressed = decompress(compressed_body, compression)?;
346
347    // CRC32 verification
348    let checksum_valid = if let Some(expected) = crc32_value {
349        let actual = crc32fast::hash(&decompressed);
350        if options.verify_checksum && actual != expected {
351            return Err(Error::ChecksumMismatch { expected, actual });
352        }
353        Some(actual == expected)
354    } else {
355        None
356    };
357
358    let html = String::from_utf8(decompressed).map_err(Error::InvalidUtf8)?;
359
360    Ok(UnpackResult {
361        url,
362        etag,
363        signature,
364        content_type,
365        timestamp,
366        encoding,
367        custom,
368        html,
369        version,
370        minified,
371        compression,
372        checksum_valid,
373    })
374}
375
376/// Read only headers without decompressing the body.
377pub fn read_headers(data: &[u8]) -> Result<UnpackResult, Error> {
378    unpack_with_options(
379        data,
380        &UnpackOptions {
381            verify_checksum: false,
382            headers_only: true,
383        },
384    )
385}
386
387/// Decompress data based on the algorithm.
388fn decompress(data: &[u8], algorithm: CompressionAlgorithm) -> Result<Vec<u8>, Error> {
389    use std::io::Read;
390
391    match algorithm {
392        CompressionAlgorithm::Gzip => {
393            let mut decoder = flate2::read::GzDecoder::new(data);
394            let mut buf = Vec::new();
395            decoder
396                .read_to_end(&mut buf)
397                .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
398            Ok(buf)
399        }
400        CompressionAlgorithm::Deflate => {
401            let mut decoder = flate2::read::DeflateDecoder::new(data);
402            let mut buf = Vec::new();
403            decoder
404                .read_to_end(&mut buf)
405                .map_err(|e| Error::DecompressionFailed(e.to_string()))?;
406            Ok(buf)
407        }
408    }
409}