diff --git a/crates/net/src/http2/hpack.rs b/crates/net/src/http2/hpack.rs new file mode 100644 index 0000000..7b0e80f --- /dev/null +++ b/crates/net/src/http2/hpack.rs @@ -0,0 +1,1674 @@ +//! HPACK: Header Compression for HTTP/2 (RFC 7541). +//! +//! Implements the full HPACK encoding and decoding format including: +//! - Integer representation with prefix bits (§5.1) +//! - Huffman coding for string literals (§5.2, Appendix B) +//! - Static table of 61 predefined headers (Appendix A) +//! - Dynamic table with configurable max size (§2.3) +//! - All header field representations (§6) + +use std::fmt; + +// --------------------------------------------------------------------------- +// Errors +// --------------------------------------------------------------------------- + +/// Errors that can occur during HPACK encoding or decoding. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum HpackError { + /// The encoded integer is incomplete (truncated input). + IntegerOverflow, + /// Unexpected end of input. + UnexpectedEnd, + /// An index into the header table is out of bounds. + InvalidIndex(usize), + /// The Huffman-encoded string is malformed. + InvalidHuffman, + /// Dynamic table size update exceeds the protocol limit. + InvalidTableSize(usize), +} + +impl fmt::Display for HpackError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + HpackError::IntegerOverflow => write!(f, "HPACK integer overflow"), + HpackError::UnexpectedEnd => write!(f, "unexpected end of HPACK data"), + HpackError::InvalidIndex(i) => write!(f, "invalid HPACK table index {i}"), + HpackError::InvalidHuffman => write!(f, "invalid Huffman-encoded string"), + HpackError::InvalidTableSize(s) => { + write!(f, "dynamic table size update {s} exceeds limit") + } + } + } +} + +// --------------------------------------------------------------------------- +// Integer encoding / decoding (RFC 7541 §5.1) +// --------------------------------------------------------------------------- + +/// Encode an integer using the HPACK variable-length integer format. +/// +/// `prefix_bits` is the number of bits available in the first octet (1..=8). +/// `prefix_value` is the value already present in the high bits of the first byte. +pub fn encode_integer(buf: &mut Vec, mut value: usize, prefix_bits: u8, prefix_value: u8) { + debug_assert!((1..=8).contains(&prefix_bits)); + let max_prefix = (1usize << prefix_bits) - 1; + + if value < max_prefix { + buf.push(prefix_value | value as u8); + } else { + buf.push(prefix_value | max_prefix as u8); + value -= max_prefix; + while value >= 128 { + buf.push((value % 128) as u8 | 0x80); + value /= 128; + } + buf.push(value as u8); + } +} + +/// Decode an integer from the HPACK variable-length integer format. +/// +/// Returns `(value, bytes_consumed)`. +pub fn decode_integer(data: &[u8], prefix_bits: u8) -> Result<(usize, usize), HpackError> { + debug_assert!((1..=8).contains(&prefix_bits)); + if data.is_empty() { + return Err(HpackError::UnexpectedEnd); + } + + let max_prefix = (1usize << prefix_bits) - 1; + let value = (data[0] as usize) & max_prefix; + + if value < max_prefix { + return Ok((value, 1)); + } + + let mut value = max_prefix; + let mut m = 0u32; + let mut i = 1; + + loop { + if i >= data.len() { + return Err(HpackError::UnexpectedEnd); + } + let b = data[i] as usize; + i += 1; + + // Guard against overflow: each continuation contributes 7 bits. + // With m reaching 56+ on a 64-bit system we'd overflow. + if m >= 56 { + return Err(HpackError::IntegerOverflow); + } + + value = value + .checked_add((b & 0x7F) << m) + .ok_or(HpackError::IntegerOverflow)?; + m += 7; + + if b & 0x80 == 0 { + return Ok((value, i)); + } + } +} + +// --------------------------------------------------------------------------- +// Huffman coding (RFC 7541 §5.2, Appendix B) +// --------------------------------------------------------------------------- + +/// Code lengths for each symbol 0..=256 (256 = EOS) per RFC 7541 Appendix B. +/// The canonical Huffman codes are derived from these lengths. +#[rustfmt::skip] +const HUFFMAN_CODE_LENGTHS: [u8; 257] = [ + 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, // 0-15 + 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28, // 16-31 + 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, // 32-47 + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, // 48-63 + 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // 64-79 + 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, // 80-95 + 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5, // 96-111 + 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 19, 11, 14, 13, 28, // 112-127 + 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23, // 128-143 + 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24, // 144-159 + 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, // 160-175 + 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, // 176-191 + 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25, // 192-207 + 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, // 208-223 + 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, // 224-239 + 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26, // 240-255 + 30, // 256 EOS +]; + +/// Build the canonical Huffman code table from the code lengths. +/// Returns `(code, length)` for each symbol. +fn build_huffman_table() -> Vec<(u32, u8)> { + let max_len = *HUFFMAN_CODE_LENGTHS.iter().max().unwrap() as usize; + + // Count codes of each length + let mut bl_count = vec![0u32; max_len + 1]; + for &len in &HUFFMAN_CODE_LENGTHS { + bl_count[len as usize] += 1; + } + + // Compute the starting code for each length + let mut next_code = vec![0u32; max_len + 1]; + let mut code = 0u32; + bl_count[0] = 0; + for bits in 1..=max_len { + code = (code + bl_count[bits - 1]) << 1; + next_code[bits] = code; + } + + // Assign codes to symbols in order + let mut table = Vec::with_capacity(HUFFMAN_CODE_LENGTHS.len()); + for &len in &HUFFMAN_CODE_LENGTHS { + let c = next_code[len as usize]; + next_code[len as usize] += 1; + table.push((c, len)); + } + + table +} + +/// Thread-local Huffman encode table for reuse. +fn with_huffman_table(f: impl FnOnce(&Vec<(u32, u8)>) -> R) -> R { + thread_local! { + static TABLE: Vec<(u32, u8)> = build_huffman_table(); + } + TABLE.with(f) +} + +/// Huffman-encode a byte slice. +pub fn huffman_encode(src: &[u8]) -> Vec { + with_huffman_table(|table| { + let mut buf = Vec::new(); + let mut current: u64 = 0; + let mut bits_left: u8 = 0; + + for &byte in src { + let (code, length) = table[byte as usize]; + current = (current << length) | code as u64; + bits_left += length; + + while bits_left >= 8 { + bits_left -= 8; + buf.push((current >> bits_left) as u8); + } + } + + // Pad with EOS prefix (all 1s) per RFC 7541 §5.2 + if bits_left > 0 { + let pad = 8 - bits_left; + current = (current << pad) | ((1u64 << pad) - 1); + buf.push(current as u8); + } + + buf + }) +} + +/// Huffman decode table node. We build a 256-entry lookup table for 8-bit +/// chunks to speed up decoding. +/// +/// For simplicity we use a bit-at-a-time approach with a binary tree, +/// which is correct and straightforward. +struct HuffmanDecoder { + /// Tree nodes: [left_child, right_child]. Index 0 is root. + /// Leaf nodes store the decoded symbol value with a sentinel flag. + nodes: Vec<[i32; 2]>, +} + +/// Sentinel: bit 16 set means "this is a leaf with symbol = value & 0x1FF". +const LEAF_FLAG: i32 = 0x10000; + +impl HuffmanDecoder { + fn new() -> Self { + let table = build_huffman_table(); + + // Start with root node. + // Children are either 0 (empty), a positive node index, or LEAF_FLAG | symbol. + let mut nodes = vec![[0i32; 2]]; + + for (sym, &(code, length)) in table.iter().enumerate() { + if sym == 256 { + break; // skip EOS — not a decodable symbol + } + + let mut node = 0usize; + for bit_pos in (0..length).rev() { + let bit = ((code >> bit_pos) & 1) as usize; + + if bit_pos == 0 { + // Last bit — store leaf directly in parent's child slot + debug_assert!( + nodes[node][bit] == 0, + "Huffman table conflict at symbol {sym}" + ); + nodes[node][bit] = LEAF_FLAG | sym as i32; + } else { + // Intermediate bit — navigate to or create child node + let child = nodes[node][bit]; + if child == 0 { + let new_node = nodes.len(); + nodes.push([0i32; 2]); + nodes[node][bit] = new_node as i32; + node = new_node; + } else { + debug_assert!( + child & LEAF_FLAG == 0, + "Huffman table conflict: leaf in prefix path at symbol {sym}" + ); + node = child as usize; + } + } + } + } + + HuffmanDecoder { nodes } + } + + fn decode(&self, data: &[u8], encoded_len: usize) -> Result, HpackError> { + let mut out = Vec::new(); + let mut node = 0usize; + + for &byte in data.iter().take(encoded_len) { + for bit_pos in (0..8u8).rev() { + let bit = ((byte >> bit_pos) & 1) as usize; + let child = self.nodes[node][bit]; + + if child == 0 { + return Err(HpackError::InvalidHuffman); + } + + if child & LEAF_FLAG != 0 { + let sym = child & 0x1FF; + out.push(sym as u8); + node = 0; + } else { + node = child as usize; + } + } + } + + Ok(out) + } +} + +/// Thread-local Huffman decoder for reuse. +fn with_huffman_decoder(f: impl FnOnce(&HuffmanDecoder) -> R) -> R { + thread_local! { + static DECODER: HuffmanDecoder = HuffmanDecoder::new(); + } + DECODER.with(f) +} + +/// Huffman-decode a byte slice. +pub fn huffman_decode(data: &[u8]) -> Result, HpackError> { + with_huffman_decoder(|dec| dec.decode(data, data.len())) +} + +// --------------------------------------------------------------------------- +// String encoding / decoding (RFC 7541 §5.2) +// --------------------------------------------------------------------------- + +/// Encode a string (with optional Huffman coding) into the HPACK format. +fn encode_string(buf: &mut Vec, value: &[u8], use_huffman: bool) { + if use_huffman { + let encoded = huffman_encode(value); + encode_integer(buf, encoded.len(), 7, 0x80); + buf.extend_from_slice(&encoded); + } else { + encode_integer(buf, value.len(), 7, 0x00); + buf.extend_from_slice(value); + } +} + +/// Decode a string from HPACK format. Returns `(decoded_bytes, bytes_consumed)`. +fn decode_string(data: &[u8]) -> Result<(Vec, usize), HpackError> { + if data.is_empty() { + return Err(HpackError::UnexpectedEnd); + } + + let huffman = data[0] & 0x80 != 0; + let (length, consumed) = decode_integer(data, 7)?; + + let total = consumed + length; + if total > data.len() { + return Err(HpackError::UnexpectedEnd); + } + + let raw = &data[consumed..total]; + let decoded = if huffman { + huffman_decode(raw)? + } else { + raw.to_vec() + }; + + Ok((decoded, total)) +} + +// --------------------------------------------------------------------------- +// Static table (RFC 7541 Appendix A) +// --------------------------------------------------------------------------- + +/// A header field is a name-value pair. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HeaderField { + pub name: Vec, + pub value: Vec, +} + +impl HeaderField { + pub fn new(name: impl Into>, value: impl Into>) -> Self { + HeaderField { + name: name.into(), + value: value.into(), + } + } + + /// The size of this entry per RFC 7541 §4.1: name length + value length + 32. + pub fn size(&self) -> usize { + self.name.len() + self.value.len() + 32 + } +} + +/// The 61 predefined static table entries (RFC 7541 Appendix A). +/// Index 1..=61 (1-based). +#[rustfmt::skip] +const STATIC_TABLE: [(&[u8], &[u8]); 61] = [ + (b":authority", b""), // 1 + (b":method", b"GET"), // 2 + (b":method", b"POST"), // 3 + (b":path", b"/"), // 4 + (b":path", b"/index.html"), // 5 + (b":scheme", b"http"), // 6 + (b":scheme", b"https"), // 7 + (b":status", b"200"), // 8 + (b":status", b"204"), // 9 + (b":status", b"206"), // 10 + (b":status", b"304"), // 11 + (b":status", b"400"), // 12 + (b":status", b"404"), // 13 + (b":status", b"500"), // 14 + (b"accept-charset", b""), // 15 + (b"accept-encoding", b"gzip, deflate"), // 16 + (b"accept-language", b""), // 17 + (b"accept-ranges", b""), // 18 + (b"accept", b""), // 19 + (b"access-control-allow-origin", b""), // 20 + (b"age", b""), // 21 + (b"allow", b""), // 22 + (b"authorization", b""), // 23 + (b"cache-control", b""), // 24 + (b"content-disposition", b""), // 25 + (b"content-encoding", b""), // 26 + (b"content-language", b""), // 27 + (b"content-length", b""), // 28 + (b"content-location", b""), // 29 + (b"content-range", b""), // 30 + (b"content-type", b""), // 31 + (b"cookie", b""), // 32 + (b"date", b""), // 33 + (b"etag", b""), // 34 + (b"expect", b""), // 35 + (b"expires", b""), // 36 + (b"from", b""), // 37 + (b"host", b""), // 38 + (b"if-match", b""), // 39 + (b"if-modified-since", b""), // 40 + (b"if-none-match", b""), // 41 + (b"if-range", b""), // 42 + (b"if-unmodified-since", b""), // 43 + (b"last-modified", b""), // 44 + (b"link", b""), // 45 + (b"location", b""), // 46 + (b"max-forwards", b""), // 47 + (b"proxy-authenticate", b""), // 48 + (b"proxy-authorization", b""), // 49 + (b"range", b""), // 50 + (b"referer", b""), // 51 + (b"refresh", b""), // 52 + (b"retry-after", b""), // 53 + (b"server", b""), // 54 + (b"set-cookie", b""), // 55 + (b"strict-transport-security", b""), // 56 + (b"transfer-encoding", b""), // 57 + (b"user-agent", b""), // 58 + (b"vary", b""), // 59 + (b"via", b""), // 60 + (b"www-authenticate", b""), // 61 +]; + +/// Look up a header in the static table. +/// Returns `Some((index, full_match))` where `full_match` is true if both +/// name and value match, false if only the name matches. +/// Index is 1-based. +fn static_table_find(name: &[u8], value: &[u8]) -> Option<(usize, bool)> { + let mut name_match = None; + for (i, &(n, v)) in STATIC_TABLE.iter().enumerate() { + if n == name { + if v == value { + return Some((i + 1, true)); + } + if name_match.is_none() { + name_match = Some(i + 1); + } + } + } + name_match.map(|idx| (idx, false)) +} + +/// Get a static table entry by 1-based index. +fn static_table_get(index: usize) -> Option<(&'static [u8], &'static [u8])> { + if index >= 1 && index <= STATIC_TABLE.len() { + Some(STATIC_TABLE[index - 1]) + } else { + None + } +} + +// --------------------------------------------------------------------------- +// Dynamic table (RFC 7541 §2.3) +// --------------------------------------------------------------------------- + +/// The dynamic table is a FIFO buffer of recently-seen headers. +/// Newest entries are at the front (index 0 = most recently inserted). +/// HPACK indices continue from after the static table: the first dynamic +/// entry has index `STATIC_TABLE.len() + 1` (= 62). +pub struct DynamicTable { + entries: Vec, + current_size: usize, + max_size: usize, + /// The protocol-level limit (from SETTINGS_HEADER_TABLE_SIZE). + protocol_max_size: usize, +} + +impl DynamicTable { + /// Create a new dynamic table with the given maximum size (in bytes). + pub fn new(max_size: usize) -> Self { + DynamicTable { + entries: Vec::new(), + current_size: 0, + max_size, + protocol_max_size: max_size, + } + } + + /// Insert a new entry at the front of the table, evicting old entries as needed. + pub fn insert(&mut self, field: HeaderField) { + let entry_size = field.size(); + + // Evict entries until there's room (or the table is empty). + while self.current_size + entry_size > self.max_size && !self.entries.is_empty() { + let removed = self.entries.pop().unwrap(); + self.current_size -= removed.size(); + } + + // If the entry itself is larger than max_size, the table is emptied + // but the entry is not added (per RFC 7541 §4.4). + if entry_size <= self.max_size { + self.current_size += entry_size; + self.entries.insert(0, field); + } + } + + /// Get an entry by 0-based dynamic table index. + pub fn get(&self, index: usize) -> Option<&HeaderField> { + self.entries.get(index) + } + + /// Update the maximum table size. Evicts entries if necessary. + pub fn set_max_size(&mut self, new_max: usize) { + self.max_size = new_max; + self.evict(); + } + + /// Set the protocol-level maximum size (from SETTINGS frame). + pub fn set_protocol_max_size(&mut self, new_max: usize) { + self.protocol_max_size = new_max; + if self.max_size > new_max { + self.max_size = new_max; + self.evict(); + } + } + + /// Number of entries in the dynamic table. + pub fn len(&self) -> usize { + self.entries.len() + } + + /// Whether the dynamic table is empty. + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + /// Current size in bytes. + pub fn size(&self) -> usize { + self.current_size + } + + /// Maximum table size in bytes. + pub fn max_size(&self) -> usize { + self.max_size + } + + /// Find a header in the dynamic table. + /// Returns `Some((index, full_match))` where index is 0-based within the + /// dynamic table. + fn find(&self, name: &[u8], value: &[u8]) -> Option<(usize, bool)> { + let mut name_match = None; + for (i, entry) in self.entries.iter().enumerate() { + if entry.name == name { + if entry.value == value { + return Some((i, true)); + } + if name_match.is_none() { + name_match = Some(i); + } + } + } + name_match.map(|idx| (idx, false)) + } + + /// Evict entries to fit within max_size. + fn evict(&mut self) { + while self.current_size > self.max_size && !self.entries.is_empty() { + let removed = self.entries.pop().unwrap(); + self.current_size -= removed.size(); + } + } +} + +// --------------------------------------------------------------------------- +// Unified table access +// --------------------------------------------------------------------------- + +/// The combined static + dynamic table index space. +const STATIC_TABLE_LEN: usize = 61; + +/// Resolve a 1-based HPACK index to a `HeaderField`. +fn table_get(index: usize, dynamic: &DynamicTable) -> Result { + if index == 0 { + return Err(HpackError::InvalidIndex(0)); + } + + if index <= STATIC_TABLE_LEN { + let (name, value) = static_table_get(index).unwrap(); + Ok(HeaderField::new(name, value)) + } else { + let dyn_index = index - STATIC_TABLE_LEN - 1; + dynamic + .get(dyn_index) + .cloned() + .ok_or(HpackError::InvalidIndex(index)) + } +} + +/// Find a header in the combined static + dynamic tables. +/// Returns `(index, full_match)` where index is 1-based HPACK index. +fn table_find(name: &[u8], value: &[u8], dynamic: &DynamicTable) -> Option<(usize, bool)> { + // Check static table first + let static_result = static_table_find(name, value); + if let Some((_, true)) = static_result { + return static_result; + } + + // Check dynamic table + if let Some((dyn_idx, full)) = dynamic.find(name, value) { + let hpack_idx = STATIC_TABLE_LEN + 1 + dyn_idx; + if full { + return Some((hpack_idx, true)); + } + // Return dynamic name-only match if static didn't match name either + if static_result.is_none() { + return Some((hpack_idx, false)); + } + } + + // Return static name-only match + static_result +} + +// --------------------------------------------------------------------------- +// Encoder +// --------------------------------------------------------------------------- + +/// Sensitivity hint for header encoding. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Sensitive { + /// Normal header — may be added to the dynamic table. + No, + /// Sensitive header (e.g., cookie) — literal, never indexed. + Yes, +} + +/// HPACK encoder. Maintains encoder-side dynamic table state. +pub struct Encoder { + dynamic_table: DynamicTable, + use_huffman: bool, +} + +impl Encoder { + /// Create a new encoder with the given maximum dynamic table size. + pub fn new(max_table_size: usize) -> Self { + Encoder { + dynamic_table: DynamicTable::new(max_table_size), + use_huffman: true, + } + } + + /// Set whether to use Huffman coding for string literals. + pub fn set_huffman(&mut self, use_huffman: bool) { + self.use_huffman = use_huffman; + } + + /// Encode a list of headers into an HPACK header block. + pub fn encode(&mut self, headers: &[HeaderField]) -> Vec { + let mut buf = Vec::new(); + for header in headers { + self.encode_header(&mut buf, header, Sensitive::No); + } + buf + } + + /// Encode a list of headers with sensitivity hints. + pub fn encode_with_sensitivity(&mut self, headers: &[(HeaderField, Sensitive)]) -> Vec { + let mut buf = Vec::new(); + for (header, sensitive) in headers { + self.encode_header(&mut buf, header, *sensitive); + } + buf + } + + /// Encode a dynamic table size update. + pub fn encode_table_size_update(&mut self, buf: &mut Vec, new_size: usize) { + encode_integer(buf, new_size, 5, 0x20); + self.dynamic_table.set_max_size(new_size); + } + + /// Encode a single header field. + fn encode_header(&mut self, buf: &mut Vec, header: &HeaderField, sensitive: Sensitive) { + if sensitive == Sensitive::Yes { + self.encode_literal_never_indexed(buf, header); + return; + } + + match table_find(&header.name, &header.value, &self.dynamic_table) { + Some((index, true)) => { + // Fully indexed — emit indexed header field (§6.1) + encode_integer(buf, index, 7, 0x80); + } + Some((index, false)) => { + // Name match — literal with incremental indexing (§6.2.1) + encode_integer(buf, index, 6, 0x40); + encode_string(buf, &header.value, self.use_huffman); + self.dynamic_table.insert(header.clone()); + } + None => { + // No match — literal with incremental indexing, new name (§6.2.1) + buf.push(0x40); + encode_string(buf, &header.name, self.use_huffman); + encode_string(buf, &header.value, self.use_huffman); + self.dynamic_table.insert(header.clone()); + } + } + } + + /// Encode a literal header field that must never be indexed (§6.2.3). + fn encode_literal_never_indexed(&mut self, buf: &mut Vec, header: &HeaderField) { + match static_table_find(&header.name, &header.value) { + Some((index, _)) => { + encode_integer(buf, index, 4, 0x10); + encode_string(buf, &header.value, self.use_huffman); + } + None => { + // Also check dynamic table for name-only match + match self.dynamic_table.find(&header.name, &header.value) { + Some((dyn_idx, _)) => { + let hpack_idx = STATIC_TABLE_LEN + 1 + dyn_idx; + encode_integer(buf, hpack_idx, 4, 0x10); + encode_string(buf, &header.value, self.use_huffman); + } + None => { + buf.push(0x10); + encode_string(buf, &header.name, self.use_huffman); + encode_string(buf, &header.value, self.use_huffman); + } + } + } + } + } + + /// Access the encoder's dynamic table. + pub fn dynamic_table(&self) -> &DynamicTable { + &self.dynamic_table + } +} + +// --------------------------------------------------------------------------- +// Decoder +// --------------------------------------------------------------------------- + +/// HPACK decoder. Maintains decoder-side dynamic table state. +pub struct Decoder { + dynamic_table: DynamicTable, +} + +impl Decoder { + /// Create a new decoder with the given maximum dynamic table size. + pub fn new(max_table_size: usize) -> Self { + Decoder { + dynamic_table: DynamicTable::new(max_table_size), + } + } + + /// Decode an HPACK header block into a list of header fields. + pub fn decode(&mut self, data: &[u8]) -> Result, HpackError> { + let mut headers = Vec::new(); + let mut pos = 0; + + while pos < data.len() { + let byte = data[pos]; + + if byte & 0x80 != 0 { + // §6.1 Indexed Header Field Representation + let (index, consumed) = decode_integer(&data[pos..], 7)?; + pos += consumed; + + if index == 0 { + return Err(HpackError::InvalidIndex(0)); + } + + let field = table_get(index, &self.dynamic_table)?; + headers.push(field); + } else if byte & 0x40 != 0 { + // §6.2.1 Literal Header Field with Incremental Indexing + let (index, consumed) = decode_integer(&data[pos..], 6)?; + pos += consumed; + + let name = if index > 0 { + table_get(index, &self.dynamic_table)?.name + } else { + let (n, c) = decode_string(&data[pos..])?; + pos += c; + n + }; + + let (value, consumed) = decode_string(&data[pos..])?; + pos += consumed; + + let field = HeaderField::new(name, value); + self.dynamic_table.insert(field.clone()); + headers.push(field); + } else if byte & 0x20 != 0 { + // §6.3 Dynamic Table Size Update + let (new_size, consumed) = decode_integer(&data[pos..], 5)?; + pos += consumed; + + if new_size > self.dynamic_table.protocol_max_size { + return Err(HpackError::InvalidTableSize(new_size)); + } + + self.dynamic_table.set_max_size(new_size); + } else if byte & 0x10 != 0 { + // §6.2.3 Literal Header Field Never Indexed + let (index, consumed) = decode_integer(&data[pos..], 4)?; + pos += consumed; + + let name = if index > 0 { + table_get(index, &self.dynamic_table)?.name + } else { + let (n, c) = decode_string(&data[pos..])?; + pos += c; + n + }; + + let (value, consumed) = decode_string(&data[pos..])?; + pos += consumed; + + headers.push(HeaderField::new(name, value)); + // Never indexed — do NOT add to dynamic table + } else { + // §6.2.2 Literal Header Field without Indexing + let (index, consumed) = decode_integer(&data[pos..], 4)?; + pos += consumed; + + let name = if index > 0 { + table_get(index, &self.dynamic_table)?.name + } else { + let (n, c) = decode_string(&data[pos..])?; + pos += c; + n + }; + + let (value, consumed) = decode_string(&data[pos..])?; + pos += consumed; + + headers.push(HeaderField::new(name, value)); + // Without indexing — do NOT add to dynamic table + } + } + + Ok(headers) + } + + /// Update the protocol-level maximum table size (from SETTINGS frame). + pub fn set_protocol_max_size(&mut self, max_size: usize) { + self.dynamic_table.set_protocol_max_size(max_size); + } + + /// Access the decoder's dynamic table. + pub fn dynamic_table(&self) -> &DynamicTable { + &self.dynamic_table + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + // ----------------------------------------------------------------------- + // Integer encoding / decoding (RFC 7541 §C.1) + // ----------------------------------------------------------------------- + + #[test] + fn test_encode_integer_10_with_5bit_prefix() { + // RFC 7541 §C.1.1: encode 10 with 5-bit prefix + let mut buf = Vec::new(); + encode_integer(&mut buf, 10, 5, 0x00); + assert_eq!(buf, vec![10]); + } + + #[test] + fn test_encode_integer_1337_with_5bit_prefix() { + // RFC 7541 §C.1.2: encode 1337 with 5-bit prefix + let mut buf = Vec::new(); + encode_integer(&mut buf, 1337, 5, 0x00); + assert_eq!(buf, vec![31, 154, 10]); + } + + #[test] + fn test_encode_integer_42_at_octet_boundary() { + // RFC 7541 §C.1.3: encode 42 starting at octet boundary (8-bit prefix) + let mut buf = Vec::new(); + encode_integer(&mut buf, 42, 8, 0x00); + assert_eq!(buf, vec![42]); + } + + #[test] + fn test_decode_integer_10_with_5bit_prefix() { + let data = [10u8]; + let (value, consumed) = decode_integer(&data, 5).unwrap(); + assert_eq!(value, 10); + assert_eq!(consumed, 1); + } + + #[test] + fn test_decode_integer_1337_with_5bit_prefix() { + let data = [31u8, 154, 10]; + let (value, consumed) = decode_integer(&data, 5).unwrap(); + assert_eq!(value, 1337); + assert_eq!(consumed, 3); + } + + #[test] + fn test_decode_integer_42_at_octet_boundary() { + let data = [42u8]; + let (value, consumed) = decode_integer(&data, 8).unwrap(); + assert_eq!(value, 42); + assert_eq!(consumed, 1); + } + + #[test] + fn test_integer_roundtrip() { + for prefix in 1..=8 { + for &value in &[0, 1, 30, 31, 127, 128, 255, 256, 1337, 65535, 1_000_000] { + let mut buf = Vec::new(); + encode_integer(&mut buf, value, prefix, 0x00); + let (decoded, consumed) = decode_integer(&buf, prefix).unwrap(); + assert_eq!(decoded, value, "prefix={prefix}, value={value}"); + assert_eq!(consumed, buf.len()); + } + } + } + + #[test] + fn test_integer_with_prefix_value() { + // Encode with high bits set in prefix + let mut buf = Vec::new(); + encode_integer(&mut buf, 10, 5, 0xE0); // top 3 bits = 111 + assert_eq!(buf[0], 0xE0 | 10); + assert_eq!(buf.len(), 1); + } + + #[test] + fn test_decode_integer_truncated() { + let data = [31u8, 154]; // missing last byte of 1337 + assert!(decode_integer(&data, 5).is_err()); + } + + #[test] + fn test_decode_integer_empty() { + assert!(decode_integer(&[], 5).is_err()); + } + + // ----------------------------------------------------------------------- + // Huffman coding + // ----------------------------------------------------------------------- + + #[test] + fn test_huffman_encode_www_example_com() { + // RFC 7541 §C.4.1: "www.example.com" Huffman encoding + let encoded = huffman_encode(b"www.example.com"); + assert_eq!( + encoded, + vec![0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff] + ); + } + + #[test] + fn test_huffman_decode_www_example_com() { + let data = vec![ + 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff, + ]; + let decoded = huffman_decode(&data).unwrap(); + assert_eq!(decoded, b"www.example.com"); + } + + #[test] + fn test_huffman_roundtrip() { + let test_strings: &[&[u8]] = &[ + b"", + b"a", + b"hello", + b"www.example.com", + b"no-cache", + b"custom-key", + b"custom-value", + b"/sample/path", + b"Mon, 21 Oct 2013 20:13:21 GMT", + b"https://www.example.com", + ]; + + for s in test_strings { + let encoded = huffman_encode(s); + let decoded = huffman_decode(&encoded).unwrap(); + assert_eq!( + &decoded, + s, + "roundtrip failed for {:?}", + String::from_utf8_lossy(s) + ); + } + } + + #[test] + fn test_huffman_encode_no_cache() { + // RFC 7541 §C.4.1 + let encoded = huffman_encode(b"no-cache"); + assert_eq!(encoded, vec![0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf]); + } + + // ----------------------------------------------------------------------- + // Static table + // ----------------------------------------------------------------------- + + #[test] + fn test_static_table_size() { + assert_eq!(STATIC_TABLE.len(), 61); + } + + #[test] + fn test_static_table_entries() { + // Verify a few known entries + assert_eq!(static_table_get(1), Some((&b":authority"[..], &b""[..]))); + assert_eq!(static_table_get(2), Some((&b":method"[..], &b"GET"[..]))); + assert_eq!(static_table_get(3), Some((&b":method"[..], &b"POST"[..]))); + assert_eq!(static_table_get(4), Some((&b":path"[..], &b"/"[..]))); + assert_eq!(static_table_get(8), Some((&b":status"[..], &b"200"[..]))); + assert_eq!( + static_table_get(61), + Some((&b"www-authenticate"[..], &b""[..])) + ); + assert_eq!(static_table_get(0), None); + assert_eq!(static_table_get(62), None); + } + + #[test] + fn test_static_table_find() { + // Full match + assert_eq!(static_table_find(b":method", b"GET"), Some((2, true))); + assert_eq!(static_table_find(b":method", b"POST"), Some((3, true))); + assert_eq!(static_table_find(b":path", b"/"), Some((4, true))); + + // Name-only match + assert_eq!(static_table_find(b":method", b"PUT"), Some((2, false))); + assert_eq!(static_table_find(b":status", b"201"), Some((8, false))); + + // No match + assert_eq!(static_table_find(b"x-custom", b"value"), None); + } + + // ----------------------------------------------------------------------- + // Dynamic table + // ----------------------------------------------------------------------- + + #[test] + fn test_dynamic_table_insert_and_get() { + let mut table = DynamicTable::new(4096); + table.insert(HeaderField::new( + b"custom-key".to_vec(), + b"custom-value".to_vec(), + )); + + assert_eq!(table.len(), 1); + let entry = table.get(0).unwrap(); + assert_eq!(entry.name, b"custom-key"); + assert_eq!(entry.value, b"custom-value"); + } + + #[test] + fn test_dynamic_table_fifo_order() { + let mut table = DynamicTable::new(4096); + table.insert(HeaderField::new("first", "1")); + table.insert(HeaderField::new("second", "2")); + table.insert(HeaderField::new("third", "3")); + + // Newest at index 0 + assert_eq!(table.get(0).unwrap().name, b"third"); + assert_eq!(table.get(1).unwrap().name, b"second"); + assert_eq!(table.get(2).unwrap().name, b"first"); + } + + #[test] + fn test_dynamic_table_eviction() { + // Each entry: name(1) + value(1) + 32 = 34 bytes. Max size = 70 → fits 2 entries. + let mut table = DynamicTable::new(70); + table.insert(HeaderField::new("a", "1")); + table.insert(HeaderField::new("b", "2")); + assert_eq!(table.len(), 2); + + // Third entry evicts oldest + table.insert(HeaderField::new("c", "3")); + assert_eq!(table.len(), 2); + assert_eq!(table.get(0).unwrap().name, b"c"); + assert_eq!(table.get(1).unwrap().name, b"b"); + } + + #[test] + fn test_dynamic_table_entry_too_large() { + // Table size < single entry size → table stays empty + let mut table = DynamicTable::new(10); + table.insert(HeaderField::new("custom-key", "custom-value")); + assert_eq!(table.len(), 0); + assert_eq!(table.size(), 0); + } + + #[test] + fn test_dynamic_table_size_update() { + let mut table = DynamicTable::new(4096); + table.insert(HeaderField::new("custom-key", "custom-value")); + assert!(table.size() > 0); + + // Shrink to 0 evicts everything + table.set_max_size(0); + assert_eq!(table.len(), 0); + assert_eq!(table.size(), 0); + } + + // ----------------------------------------------------------------------- + // Full encoder / decoder (RFC 7541 Appendix C examples) + // ----------------------------------------------------------------------- + + #[test] + fn test_rfc7541_c2_1_literal_with_indexing() { + // §C.2.1: Literal Header Field with Indexing + // custom-key: custom-header + let mut decoder = Decoder::new(4096); + + let data = [ + 0x40, // literal with indexing, new name + 0x0a, // name length = 10 + b'c', b'u', b's', b't', b'o', b'm', b'-', b'k', b'e', b'y', + 0x0d, // value length = 13 + b'c', b'u', b's', b't', b'o', b'm', b'-', b'h', b'e', b'a', b'd', b'e', b'r', + ]; + + let headers = decoder.decode(&data).unwrap(); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].name, b"custom-key"); + assert_eq!(headers[0].value, b"custom-header"); + + // Entry should be in dynamic table + assert_eq!(decoder.dynamic_table().len(), 1); + assert_eq!(decoder.dynamic_table().size(), 55); // 10 + 13 + 32 + } + + #[test] + fn test_rfc7541_c2_2_literal_without_indexing() { + // §C.2.2: Literal Header Field without Indexing + // :path: /sample/path + let mut decoder = Decoder::new(4096); + + let data = [ + 0x04, // literal without indexing, indexed name (index=4 → :path) + 0x0c, // value length = 12 + b'/', b's', b'a', b'm', b'p', b'l', b'e', b'/', b'p', b'a', b't', b'h', + ]; + + let headers = decoder.decode(&data).unwrap(); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].name, b":path"); + assert_eq!(headers[0].value, b"/sample/path"); + + // Should NOT be added to dynamic table + assert_eq!(decoder.dynamic_table().len(), 0); + } + + #[test] + fn test_rfc7541_c2_3_literal_never_indexed() { + // §C.2.3: Literal Header Field Never Indexed + // password: secret + let mut decoder = Decoder::new(4096); + + let data = [ + 0x10, // never indexed, new name + 0x08, // name length = 8 + b'p', b'a', b's', b's', b'w', b'o', b'r', b'd', 0x06, // value length = 6 + b's', b'e', b'c', b'r', b'e', b't', + ]; + + let headers = decoder.decode(&data).unwrap(); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].name, b"password"); + assert_eq!(headers[0].value, b"secret"); + + // Should NOT be added to dynamic table + assert_eq!(decoder.dynamic_table().len(), 0); + } + + #[test] + fn test_rfc7541_c2_4_indexed_header_field() { + // §C.2.4: Indexed Header Field + // :method: GET (index 2) + let mut decoder = Decoder::new(4096); + + let data = [0x82]; // indexed, index=2 + let headers = decoder.decode(&data).unwrap(); + assert_eq!(headers.len(), 1); + assert_eq!(headers[0].name, b":method"); + assert_eq!(headers[0].value, b"GET"); + } + + #[test] + fn test_rfc7541_c3_request_without_huffman() { + // §C.3: Request Examples without Huffman Coding + let mut decoder = Decoder::new(4096); + + // First request: :method GET, :scheme http, :path /, :authority www.example.com + let req1 = [ + 0x82, // :method: GET (indexed 2) + 0x86, // :scheme: http (indexed 6) + 0x84, // :path: / (indexed 4) + 0x41, // :authority (indexed name 1), literal with indexing + 0x0f, // value length = 15 + b'w', b'w', b'w', b'.', b'e', b'x', b'a', b'm', b'p', b'l', b'e', b'.', b'c', b'o', + b'm', + ]; + let headers1 = decoder.decode(&req1).unwrap(); + assert_eq!(headers1.len(), 4); + assert_eq!( + headers1[0], + HeaderField::new(b":method".to_vec(), b"GET".to_vec()) + ); + assert_eq!( + headers1[1], + HeaderField::new(b":scheme".to_vec(), b"http".to_vec()) + ); + assert_eq!( + headers1[2], + HeaderField::new(b":path".to_vec(), b"/".to_vec()) + ); + assert_eq!( + headers1[3], + HeaderField::new(b":authority".to_vec(), b"www.example.com".to_vec()) + ); + assert_eq!(decoder.dynamic_table().len(), 1); + assert_eq!(decoder.dynamic_table().size(), 57); // 10 + 15 + 32 + + // Second request: :method GET, :scheme http, :path /, :authority www.example.com, cache-control: no-cache + let req2 = [ + 0x82, // :method: GET (indexed 2) + 0x86, // :scheme: http (indexed 6) + 0x84, // :path: / (indexed 4) + 0xbe, // :authority: www.example.com (indexed 62, dynamic table entry 0) + 0x58, // cache-control (indexed name 24), literal with indexing + 0x08, // value length = 8 + b'n', b'o', b'-', b'c', b'a', b'c', b'h', b'e', + ]; + let headers2 = decoder.decode(&req2).unwrap(); + assert_eq!(headers2.len(), 5); + assert_eq!( + headers2[3], + HeaderField::new(b":authority".to_vec(), b"www.example.com".to_vec()) + ); + assert_eq!( + headers2[4], + HeaderField::new(b"cache-control".to_vec(), b"no-cache".to_vec()) + ); + assert_eq!(decoder.dynamic_table().len(), 2); + assert_eq!(decoder.dynamic_table().size(), 110); // 57 + 13 + 8 + 32 + + // Third request: :method GET, :scheme https, :path /index.html, :authority www.example.com, custom-key: custom-value + let req3 = [ + 0x82, // :method: GET (indexed 2) + 0x87, // :scheme: https (indexed 7) + 0x85, // :path: /index.html (indexed 5) + 0xbf, // :authority: www.example.com (indexed 63, dynamic table entry 1) + 0x40, // literal with indexing, new name + 0x0a, // name length = 10 + b'c', b'u', b's', b't', b'o', b'm', b'-', b'k', b'e', b'y', + 0x0c, // value length = 12 + b'c', b'u', b's', b't', b'o', b'm', b'-', b'v', b'a', b'l', b'u', b'e', + ]; + let headers3 = decoder.decode(&req3).unwrap(); + assert_eq!(headers3.len(), 5); + assert_eq!( + headers3[3], + HeaderField::new(b":authority".to_vec(), b"www.example.com".to_vec()) + ); + assert_eq!( + headers3[4], + HeaderField::new(b"custom-key".to_vec(), b"custom-value".to_vec()) + ); + assert_eq!(decoder.dynamic_table().len(), 3); + assert_eq!(decoder.dynamic_table().size(), 164); // 110 + 10 + 12 + 32 + } + + #[test] + fn test_rfc7541_c4_request_with_huffman() { + // §C.4: Request Examples with Huffman Coding + let mut decoder = Decoder::new(4096); + + // First request + let req1 = [ + 0x82, // :method: GET + 0x86, // :scheme: http + 0x84, // :path: / + 0x41, // :authority, literal with indexing + 0x8c, // Huffman-encoded value, length 12 + 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff, + ]; + let headers1 = decoder.decode(&req1).unwrap(); + assert_eq!(headers1.len(), 4); + assert_eq!( + headers1[3], + HeaderField::new(b":authority".to_vec(), b"www.example.com".to_vec()) + ); + assert_eq!(decoder.dynamic_table().size(), 57); + + // Second request + let req2 = [ + 0x82, // :method: GET + 0x86, // :scheme: http + 0x84, // :path: / + 0xbe, // :authority (dyn 62) + 0x58, // cache-control, literal with indexing + 0x86, // Huffman-encoded value, length 6 + 0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf, + ]; + let headers2 = decoder.decode(&req2).unwrap(); + assert_eq!(headers2.len(), 5); + assert_eq!( + headers2[4], + HeaderField::new(b"cache-control".to_vec(), b"no-cache".to_vec()) + ); + assert_eq!(decoder.dynamic_table().size(), 110); + + // Third request + let req3 = [ + 0x82, // :method: GET + 0x87, // :scheme: https + 0x85, // :path: /index.html + 0xbf, // :authority (dyn 63) + 0x40, // literal with indexing, new name + 0x88, // Huffman-encoded name, length 8 + 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f, + 0x89, // Huffman-encoded value, length 9 + 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf, + ]; + let headers3 = decoder.decode(&req3).unwrap(); + assert_eq!(headers3.len(), 5); + assert_eq!( + headers3[4], + HeaderField::new(b"custom-key".to_vec(), b"custom-value".to_vec()) + ); + assert_eq!(decoder.dynamic_table().size(), 164); + } + + #[test] + fn test_rfc7541_c5_response_without_huffman() { + // §C.5: Response Examples without Huffman Coding + // Dynamic table max size = 256 + let mut decoder = Decoder::new(256); + + // First response + let res1 = [ + 0x48, // :status (indexed name 8), literal with indexing + 0x03, // value length = 3 + b'3', b'0', b'2', 0x58, // cache-control (indexed name 24), literal with indexing + 0x07, // value length = 7 + b'p', b'r', b'i', b'v', b'a', b't', b'e', + 0x61, // date (indexed name 33), literal with indexing + 0x1d, // value length = 29 + b'M', b'o', b'n', b',', b' ', b'2', b'1', b' ', b'O', b'c', b't', b' ', b'2', b'0', + b'1', b'3', b' ', b'2', b'0', b':', b'1', b'3', b':', b'2', b'1', b' ', b'G', b'M', + b'T', 0x6e, // location (indexed name 46), literal with indexing + 0x17, // value length = 23 + b'h', b't', b't', b'p', b's', b':', b'/', b'/', b'w', b'w', b'w', b'.', b'e', b'x', + b'a', b'm', b'p', b'l', b'e', b'.', b'c', b'o', b'm', + ]; + let headers1 = decoder.decode(&res1).unwrap(); + assert_eq!(headers1.len(), 4); + assert_eq!( + headers1[0], + HeaderField::new(b":status".to_vec(), b"302".to_vec()) + ); + assert_eq!( + headers1[1], + HeaderField::new(b"cache-control".to_vec(), b"private".to_vec()) + ); + assert_eq!( + headers1[2], + HeaderField::new(b"date".to_vec(), b"Mon, 21 Oct 2013 20:13:21 GMT".to_vec()) + ); + assert_eq!( + headers1[3], + HeaderField::new(b"location".to_vec(), b"https://www.example.com".to_vec()) + ); + // Dynamic table: only entries that fit in 256 bytes + // location: 8+23+32=63, date: 4+29+32=65, cache-control: 13+7+32=52, :status: 7+3+32=42 + // Total = 63+65+52+42=222. All fit. + assert_eq!(decoder.dynamic_table().len(), 4); + assert_eq!(decoder.dynamic_table().size(), 222); + + // Second response + let res2 = [ + 0x48, // :status, literal with indexing + 0x03, b'3', b'0', b'7', 0xc1, // cache-control: private (dyn table index 63) + 0xc0, // date (dyn table index 64, but after insert above it shifted) + 0xbf, // location (dyn table index 65) + ]; + let headers2 = decoder.decode(&res2).unwrap(); + assert_eq!(headers2.len(), 4); + assert_eq!( + headers2[0], + HeaderField::new(b":status".to_vec(), b"307".to_vec()) + ); + assert_eq!( + headers2[1], + HeaderField::new(b"cache-control".to_vec(), b"private".to_vec()) + ); + + // Third response + let res3 = [ + 0x88, // :status: 200 (static index 8) + 0xc1, // cache-control: private (dyn entry) + 0x61, // date, literal with indexing + 0x1d, b'M', b'o', b'n', b',', b' ', b'2', b'1', b' ', b'O', b'c', b't', b' ', b'2', + b'0', b'1', b'3', b' ', b'2', b'0', b':', b'1', b'3', b':', b'2', b'2', b' ', b'G', + b'M', b'T', 0xc0, // location (dyn entry) + 0x5a, // content-encoding (indexed name 26), literal with indexing + 0x04, // value length = 4 + b'g', b'z', b'i', b'p', + ]; + let headers3 = decoder.decode(&res3).unwrap(); + assert_eq!(headers3.len(), 5); + assert_eq!( + headers3[0], + HeaderField::new(b":status".to_vec(), b"200".to_vec()) + ); + assert_eq!( + headers3[4], + HeaderField::new(b"content-encoding".to_vec(), b"gzip".to_vec()) + ); + } + + #[test] + fn test_rfc7541_c6_response_with_huffman() { + // §C.6: Response Examples with Huffman Coding + let mut decoder = Decoder::new(256); + + // First response (Huffman encoded) + let res1 = [ + 0x48, // :status, literal with indexing + 0x82, 0x64, 0x02, // Huffman: "302" + 0x58, // cache-control, literal with indexing + 0x85, 0xae, 0xc3, 0x77, 0x1a, 0x4b, // Huffman: "private" + 0x61, // date, literal with indexing + 0x96, // Huffman-encoded, length 22 + 0xd0, 0x7a, 0xbe, 0x94, 0x10, 0x54, 0xd4, 0x44, 0xa8, 0x20, 0x05, 0x95, 0x04, 0x0b, + 0x81, 0x66, 0xe0, 0x82, 0xa6, 0x2d, 0x1b, 0xff, + 0x6e, // location, literal with indexing + 0x91, // Huffman-encoded, length 17 + 0x9d, 0x29, 0xad, 0x17, 0x18, 0x63, 0xc7, 0x8f, 0x0b, 0x97, 0xc8, 0xe9, 0xae, 0x82, + 0xae, 0x43, 0xd3, + ]; + let headers1 = decoder.decode(&res1).unwrap(); + assert_eq!(headers1.len(), 4); + assert_eq!( + headers1[0], + HeaderField::new(b":status".to_vec(), b"302".to_vec()) + ); + assert_eq!( + headers1[1], + HeaderField::new(b"cache-control".to_vec(), b"private".to_vec()) + ); + assert_eq!( + headers1[2], + HeaderField::new(b"date".to_vec(), b"Mon, 21 Oct 2013 20:13:21 GMT".to_vec()) + ); + assert_eq!( + headers1[3], + HeaderField::new(b"location".to_vec(), b"https://www.example.com".to_vec()) + ); + assert_eq!(decoder.dynamic_table().size(), 222); + + // Second response + let res2 = [ + 0x48, // :status, literal with indexing + 0x83, 0x64, 0x0e, 0xff, // Huffman: "307" + 0xc1, // cache-control: private + 0xc0, // date + 0xbf, // location + ]; + let headers2 = decoder.decode(&res2).unwrap(); + assert_eq!(headers2.len(), 4); + assert_eq!( + headers2[0], + HeaderField::new(b":status".to_vec(), b"307".to_vec()) + ); + } + + // ----------------------------------------------------------------------- + // Encoder + Decoder roundtrip + // ----------------------------------------------------------------------- + + #[test] + fn test_encoder_decoder_roundtrip() { + let mut encoder = Encoder::new(4096); + let mut decoder = Decoder::new(4096); + + let headers = vec![ + HeaderField::new(b":method".to_vec(), b"GET".to_vec()), + HeaderField::new(b":scheme".to_vec(), b"https".to_vec()), + HeaderField::new(b":path".to_vec(), b"/".to_vec()), + HeaderField::new(b":authority".to_vec(), b"www.example.com".to_vec()), + HeaderField::new(b"user-agent".to_vec(), b"we-browser/0.1".to_vec()), + ]; + + let encoded = encoder.encode(&headers); + let decoded = decoder.decode(&encoded).unwrap(); + + assert_eq!(decoded, headers); + } + + #[test] + fn test_encoder_decoder_multiple_requests() { + let mut encoder = Encoder::new(4096); + let mut decoder = Decoder::new(4096); + + // First request + let req1 = vec![ + HeaderField::new(b":method".to_vec(), b"GET".to_vec()), + HeaderField::new(b":path".to_vec(), b"/".to_vec()), + HeaderField::new(b"host".to_vec(), b"example.com".to_vec()), + ]; + let encoded1 = encoder.encode(&req1); + let decoded1 = decoder.decode(&encoded1).unwrap(); + assert_eq!(decoded1, req1); + + // Second request — should benefit from dynamic table + let req2 = vec![ + HeaderField::new(b":method".to_vec(), b"GET".to_vec()), + HeaderField::new(b":path".to_vec(), b"/style.css".to_vec()), + HeaderField::new(b"host".to_vec(), b"example.com".to_vec()), + ]; + let encoded2 = encoder.encode(&req2); + let decoded2 = decoder.decode(&encoded2).unwrap(); + assert_eq!(decoded2, req2); + + // The second encoding should be smaller (host is in dynamic table) + assert!(encoded2.len() <= encoded1.len()); + } + + #[test] + fn test_encoder_sensitive_headers() { + let mut encoder = Encoder::new(4096); + let mut decoder = Decoder::new(4096); + + let headers = vec![ + ( + HeaderField::new(b":method".to_vec(), b"GET".to_vec()), + Sensitive::No, + ), + ( + HeaderField::new(b"cookie".to_vec(), b"session=abc123".to_vec()), + Sensitive::Yes, + ), + ]; + + let encoded = encoder.encode_with_sensitivity(&headers); + let decoded = decoder.decode(&encoded).unwrap(); + + assert_eq!(decoded.len(), 2); + assert_eq!(decoded[0].name, b":method"); + assert_eq!(decoded[1].name, b"cookie"); + assert_eq!(decoded[1].value, b"session=abc123"); + + // Cookie should NOT be in decoder's dynamic table + // (it was never-indexed), but :method was indexed (static match) + // So dynamic table should be empty (both were static/never-indexed) + assert_eq!(decoder.dynamic_table().len(), 0); + } + + #[test] + fn test_encoder_without_huffman() { + let mut encoder = Encoder::new(4096); + encoder.set_huffman(false); + let mut decoder = Decoder::new(4096); + + let headers = vec![ + HeaderField::new(b":method".to_vec(), b"GET".to_vec()), + HeaderField::new(b"x-custom".to_vec(), b"hello".to_vec()), + ]; + + let encoded = encoder.encode(&headers); + let decoded = decoder.decode(&encoded).unwrap(); + assert_eq!(decoded, headers); + } + + #[test] + fn test_table_size_update() { + let mut encoder = Encoder::new(4096); + let mut decoder = Decoder::new(4096); + + // Encode some headers to populate dynamic table + let headers = vec![HeaderField::new( + b"custom-key".to_vec(), + b"custom-value".to_vec(), + )]; + let encoded = encoder.encode(&headers); + decoder.decode(&encoded).unwrap(); + + assert_eq!(encoder.dynamic_table().len(), 1); + assert_eq!(decoder.dynamic_table().len(), 1); + + // Encode a table size update to 0 (clears table) + let mut buf = Vec::new(); + encoder.encode_table_size_update(&mut buf, 0); + // Then encode more headers + let headers2 = vec![HeaderField::new(b":method".to_vec(), b"GET".to_vec())]; + buf.extend(encoder.encode(&headers2)); + + let decoded = decoder.decode(&buf).unwrap(); + assert_eq!(decoded, headers2); + assert_eq!(decoder.dynamic_table().len(), 0); + } + + #[test] + fn test_decode_invalid_index() { + let mut decoder = Decoder::new(4096); + // Index 0 is invalid + let data = [0x80]; + assert!(decoder.decode(&data).is_err()); + + // Index far beyond table + let data = [0xFF, 0x80, 0x80, 0x01]; // large index + assert!(decoder.decode(&data).is_err()); + } + + #[test] + fn test_decode_empty() { + let mut decoder = Decoder::new(4096); + let headers = decoder.decode(&[]).unwrap(); + assert!(headers.is_empty()); + } + + #[test] + fn test_dynamic_table_entry_size() { + let field = HeaderField::new(b"custom-key".to_vec(), b"custom-value".to_vec()); + assert_eq!(field.size(), 10 + 12 + 32); // = 54 + } + + // ----------------------------------------------------------------------- + // String encoding / decoding + // ----------------------------------------------------------------------- + + #[test] + fn test_string_encode_decode_plain() { + let mut buf = Vec::new(); + encode_string(&mut buf, b"hello", false); + let (decoded, consumed) = decode_string(&buf).unwrap(); + assert_eq!(decoded, b"hello"); + assert_eq!(consumed, buf.len()); + } + + #[test] + fn test_string_encode_decode_huffman() { + let mut buf = Vec::new(); + encode_string(&mut buf, b"www.example.com", true); + let (decoded, consumed) = decode_string(&buf).unwrap(); + assert_eq!(decoded, b"www.example.com"); + assert_eq!(consumed, buf.len()); + } + + #[test] + fn test_string_decode_truncated() { + let data = [0x05, b'h', b'e']; // claims length 5 but only 2 bytes + assert!(decode_string(&data).is_err()); + } +} diff --git a/crates/net/src/http2/mod.rs b/crates/net/src/http2/mod.rs new file mode 100644 index 0000000..84c3fbc --- /dev/null +++ b/crates/net/src/http2/mod.rs @@ -0,0 +1,3 @@ +//! HTTP/2 protocol implementation. + +pub mod hpack; diff --git a/crates/net/src/lib.rs b/crates/net/src/lib.rs index 1240ef3..51f506c 100644 --- a/crates/net/src/lib.rs +++ b/crates/net/src/lib.rs @@ -5,6 +5,7 @@ pub mod cookie; pub mod cors; pub mod dns; pub mod http; +pub mod http2; pub mod referrer; pub mod tcp; pub mod tls;