//! 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()); } }