use std::borrow::Cow;
use std::collections::{HashMap, VecDeque};
use std::sync::LazyLock;
use crate::bytes::{Bytes, BytesMut};
use super::error::H2Error;
const HUFF_ACCEPTED: u8 = 0x01;
const HUFF_SYM: u8 = 0x02;
const HUFF_FAIL: u8 = 0x04;
#[derive(Copy, Clone, Default)]
struct HuffmanDecodeEntry {
next_state: u8,
flags: u8,
sym: u8,
_pad: u8,
}
static HUFFMAN_DECODE_TABLE: LazyLock<Box<[[HuffmanDecodeEntry; 16]; 256]>> =
LazyLock::new(build_huffman_decode_table);
#[allow(clippy::too_many_lines)] fn build_huffman_decode_table() -> Box<[[HuffmanDecodeEntry; 16]; 256]> {
#[derive(Default, Clone)]
struct TrieNode {
children: [Option<usize>; 2],
sym: Option<u16>,
}
let mut nodes: Vec<TrieNode> = vec![TrieNode::default()]; for (sym_idx, &(code, code_bits)) in HUFFMAN_TABLE.iter().enumerate() {
let mut cur = 0usize;
for bit_pos in (0..code_bits).rev() {
let bit = ((code >> bit_pos) & 1) as usize;
cur = match nodes[cur].children[bit] {
Some(idx) => idx,
None => {
let new_idx = nodes.len();
nodes.push(TrieNode::default());
nodes[cur].children[bit] = Some(new_idx);
new_idx
}
};
}
nodes[cur].sym = Some(sym_idx as u16);
}
let mut state_of_node: Vec<Option<u8>> = vec![None; nodes.len()];
let mut next_state_id: u32 = 0;
for (idx, n) in nodes.iter().enumerate() {
if n.sym.is_none() {
assert!(
next_state_id < 256,
"HPACK trie exceeded 256 internal nodes"
);
state_of_node[idx] = Some(next_state_id as u8);
next_state_id += 1;
}
}
let num_states = next_state_id as usize;
let mut node_of_state: Vec<usize> = vec![0; num_states];
for (idx, &maybe_id) in state_of_node.iter().enumerate() {
if let Some(id) = maybe_id {
node_of_state[id as usize] = idx;
}
}
let mut accepted_state = [false; 256];
accepted_state[0] = true;
let mut walk = 0usize;
for _depth in 1..=7 {
match nodes[walk].children[1] {
Some(idx) if nodes[idx].sym.is_none() => {
let st = state_of_node[idx].expect("EOS prefix node missing state ID");
accepted_state[st as usize] = true;
walk = idx;
}
_ => break,
}
}
let mut table: Box<[[HuffmanDecodeEntry; 16]; 256]> =
Box::new([[HuffmanDecodeEntry::default(); 16]; 256]);
for state in 0..num_states {
let start_node = node_of_state[state];
for nibble in 0u8..16 {
let mut cur_node = start_node;
let mut emitted: Option<u8> = None;
let mut fail = false;
for bit_idx in 0..4u8 {
let bit = ((nibble >> (3 - bit_idx)) & 1) as usize;
cur_node = match nodes[cur_node].children[bit] {
Some(idx) => idx,
None => {
fail = true;
break;
}
};
if let Some(sym) = nodes[cur_node].sym {
if sym == 256 {
fail = true; break;
}
if emitted.is_some() {
fail = true;
break;
}
emitted = Some(sym as u8);
cur_node = 0; }
}
let entry = if fail {
HuffmanDecodeEntry {
next_state: 0,
flags: HUFF_FAIL,
sym: 0,
_pad: 0,
}
} else {
let next_state = state_of_node[cur_node].expect("trie walk landed on a leaf");
let mut flags = 0u8;
if emitted.is_some() {
flags |= HUFF_SYM;
}
if accepted_state[next_state as usize] {
flags |= HUFF_ACCEPTED;
}
HuffmanDecodeEntry {
next_state,
flags,
sym: emitted.unwrap_or(0),
_pad: 0,
}
};
table[state][nibble as usize] = entry;
}
}
table
}
static STATIC_EXACT_INDEX: LazyLock<HashMap<(&'static str, &'static str), usize>> =
LazyLock::new(|| {
STATIC_TABLE
.iter()
.enumerate()
.map(|(i, &(n, v))| ((n, v), i.saturating_add(1)))
.collect()
});
static STATIC_NAME_INDEX: LazyLock<HashMap<&'static str, usize>> = LazyLock::new(|| {
let mut map = HashMap::with_capacity(STATIC_TABLE.len());
for (i, &(name, _)) in STATIC_TABLE.iter().enumerate() {
map.entry(name).or_insert(i.saturating_add(1));
}
map
});
const MAX_ALLOWED_TABLE_SIZE: usize = 1024 * 1024;
pub const DEFAULT_MAX_TABLE_SIZE: usize = 4096;
static STATIC_TABLE: &[(&str, &str)] = &[
(":authority", ""), (":method", "GET"), (":method", "POST"), (":path", "/"), (":path", "/index.html"), (":scheme", "http"), (":scheme", "https"), (":status", "200"), (":status", "204"), (":status", "206"), (":status", "304"), (":status", "400"), (":status", "404"), (":status", "500"), ("accept-charset", ""), ("accept-encoding", "gzip, deflate"), ("accept-language", ""), ("accept-ranges", ""), ("accept", ""), ("access-control-allow-origin", ""), ("age", ""), ("allow", ""), ("authorization", ""), ("cache-control", ""), ("content-disposition", ""), ("content-encoding", ""), ("content-language", ""), ("content-length", ""), ("content-location", ""), ("content-range", ""), ("content-type", ""), ("cookie", ""), ("date", ""), ("etag", ""), ("expect", ""), ("expires", ""), ("from", ""), ("host", ""), ("if-match", ""), ("if-modified-since", ""), ("if-none-match", ""), ("if-range", ""), ("if-unmodified-since", ""), ("last-modified", ""), ("link", ""), ("location", ""), ("max-forwards", ""), ("proxy-authenticate", ""), ("proxy-authorization", ""), ("range", ""), ("referer", ""), ("refresh", ""), ("retry-after", ""), ("server", ""), ("set-cookie", ""), ("strict-transport-security", ""), ("transfer-encoding", ""), ("user-agent", ""), ("vary", ""), ("via", ""), ("www-authenticate", ""), ];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Header {
pub name: String,
pub value: String,
}
impl Header {
#[must_use]
pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
Self {
name: name.into(),
value: value.into(),
}
}
#[must_use]
pub fn size(&self) -> usize {
self.name
.len()
.saturating_add(self.value.len())
.saturating_add(32)
}
}
#[derive(Debug, Clone)]
struct DynamicTableEntry {
name: std::sync::Arc<str>,
value: std::sync::Arc<str>,
generation: u64,
}
impl DynamicTableEntry {
fn from_strings(name: String, value: String, generation: u64) -> Self {
Self {
name: std::sync::Arc::from(name.into_boxed_str()),
value: std::sync::Arc::from(value.into_boxed_str()),
generation,
}
}
fn to_header(&self) -> Header {
Header {
name: self.name.as_ref().to_string(),
value: self.value.as_ref().to_string(),
}
}
fn size(&self) -> usize {
self.name
.len()
.saturating_add(self.value.len())
.saturating_add(32)
}
}
#[derive(Debug, Default)]
struct DynamicTableIndex {
by_name_value: HashMap<std::sync::Arc<str>, HashMap<std::sync::Arc<str>, VecDeque<u64>>>,
by_name: HashMap<std::sync::Arc<str>, VecDeque<u64>>,
}
impl DynamicTableIndex {
fn add(&mut self, name: &std::sync::Arc<str>, value: &std::sync::Arc<str>, generation: u64) {
self.by_name_value
.entry(std::sync::Arc::clone(name))
.or_default()
.entry(std::sync::Arc::clone(value))
.or_default()
.push_front(generation);
self.by_name
.entry(std::sync::Arc::clone(name))
.or_default()
.push_front(generation);
}
fn remove_oldest(
&mut self,
name: &std::sync::Arc<str>,
value: &std::sync::Arc<str>,
generation: u64,
) {
if let Some(inner) = self.by_name_value.get_mut(name.as_ref()) {
if let Some(deque) = inner.get_mut(value.as_ref()) {
debug_assert_eq!(
deque.back().copied(),
Some(generation),
"evicted generation must be the oldest in by_name_value bucket"
);
deque.pop_back();
if deque.is_empty() {
inner.remove(value.as_ref());
}
}
if inner.is_empty() {
self.by_name_value.remove(name.as_ref());
}
}
if let Some(deque) = self.by_name.get_mut(name.as_ref()) {
debug_assert_eq!(
deque.back().copied(),
Some(generation),
"evicted generation must be the oldest in by_name bucket"
);
deque.pop_back();
if deque.is_empty() {
self.by_name.remove(name.as_ref());
}
}
}
fn newest_for_pair(&self, name: &str, value: &str) -> Option<u64> {
self.by_name_value.get(name)?.get(value)?.front().copied()
}
fn newest_for_name(&self, name: &str) -> Option<u64> {
self.by_name.get(name)?.front().copied()
}
fn clear(&mut self) {
self.by_name_value.clear();
self.by_name.clear();
}
}
#[derive(Debug)]
pub struct DynamicTable {
entries: VecDeque<DynamicTableEntry>,
size: usize,
max_size: usize,
insert_count: u64,
index: DynamicTableIndex,
}
impl DynamicTable {
#[must_use]
pub fn new() -> Self {
Self::with_max_size(DEFAULT_MAX_TABLE_SIZE)
}
#[must_use]
pub fn with_max_size(max_size: usize) -> Self {
Self {
entries: VecDeque::new(),
size: 0,
max_size: max_size.min(MAX_ALLOWED_TABLE_SIZE),
insert_count: 0,
index: DynamicTableIndex::default(),
}
}
#[must_use]
pub fn size(&self) -> usize {
self.size
}
#[must_use]
pub fn max_size(&self) -> usize {
self.max_size
}
pub fn set_max_size(&mut self, max_size: usize) {
self.max_size = max_size.min(MAX_ALLOWED_TABLE_SIZE);
self.evict();
}
pub fn insert(&mut self, header: Header) {
let generation = self.insert_count;
let entry = DynamicTableEntry::from_strings(header.name, header.value, generation);
let entry_size = entry.size();
while self.size.saturating_add(entry_size) > self.max_size && !self.entries.is_empty() {
self.pop_back_with_index_cleanup();
}
if entry_size <= self.max_size {
self.index.add(&entry.name, &entry.value, generation);
self.insert_count = self.insert_count.saturating_add(1);
self.size = self.size.saturating_add(entry_size);
self.entries.push_front(entry);
} else {
debug_assert!(self.entries.is_empty());
self.index.clear();
}
}
#[must_use]
pub fn get(&self, index: usize) -> Option<Header> {
if index == 0 || index > self.entries.len() {
None
} else {
Some(self.entries[index.saturating_sub(1)].to_header())
}
}
#[must_use]
pub fn find(&self, name: &str, value: &str) -> Option<usize> {
let generation = self.index.newest_for_pair(name, value)?;
Some(self.index_from_generation(generation))
}
#[must_use]
pub fn find_name(&self, name: &str) -> Option<usize> {
let generation = self.index.newest_for_name(name)?;
Some(self.index_from_generation(generation))
}
fn index_from_generation(&self, generation: u64) -> usize {
debug_assert!(self.insert_count > generation);
STATIC_TABLE.len()
+ usize::try_from(self.insert_count.saturating_sub(generation))
.unwrap_or(self.entries.len())
}
fn pop_back_with_index_cleanup(&mut self) {
if let Some(evicted) = self.entries.pop_back() {
self.size = self.size.saturating_sub(evicted.size());
self.index
.remove_oldest(&evicted.name, &evicted.value, evicted.generation);
}
}
fn evict(&mut self) {
while self.size > self.max_size && !self.entries.is_empty() {
self.pop_back_with_index_cleanup();
}
}
}
impl Default for DynamicTable {
fn default() -> Self {
Self::new()
}
}
fn find_static(name: &str, value: &str) -> Option<usize> {
STATIC_EXACT_INDEX.get(&(name, value)).copied()
}
fn find_static_name(name: &str) -> Option<usize> {
STATIC_NAME_INDEX.get(name).copied()
}
fn get_static(index: usize) -> Option<(&'static str, &'static str)> {
if index == 0 || index > STATIC_TABLE.len() {
None
} else {
Some(STATIC_TABLE[index.saturating_sub(1)])
}
}
#[derive(Debug)]
pub struct Encoder {
dynamic_table: DynamicTable,
use_huffman: bool,
min_size_update: Option<usize>,
pending_size_update: Option<usize>,
}
impl Encoder {
#[must_use]
pub fn new() -> Self {
Self::with_max_size(DEFAULT_MAX_TABLE_SIZE)
}
#[must_use]
pub fn with_max_size(max_size: usize) -> Self {
Self {
dynamic_table: DynamicTable::with_max_size(max_size),
use_huffman: true,
min_size_update: None,
pending_size_update: None,
}
}
pub fn set_use_huffman(&mut self, use_huffman: bool) {
self.use_huffman = use_huffman;
}
pub fn set_max_table_size(&mut self, size: usize) {
let capped = size.min(MAX_ALLOWED_TABLE_SIZE);
self.dynamic_table.set_max_size(capped);
if let Some(min_so_far) = self.min_size_update {
self.min_size_update = Some(min_so_far.min(capped));
} else {
self.min_size_update = Some(capped);
}
self.pending_size_update = Some(capped);
}
#[must_use]
pub fn dynamic_table_size(&self) -> usize {
self.dynamic_table.size()
}
#[must_use]
pub fn dynamic_table_max_size(&self) -> usize {
self.dynamic_table.max_size()
}
pub fn encode(&mut self, headers: &[Header], dst: &mut BytesMut) {
self.emit_pending_size_update(dst);
for header in headers {
self.encode_header(header, dst, true);
}
}
pub fn encode_sensitive(&mut self, headers: &[Header], dst: &mut BytesMut) {
self.emit_pending_size_update(dst);
for header in headers {
self.encode_header(header, dst, false);
}
}
fn emit_pending_size_update(&mut self, dst: &mut BytesMut) {
if let Some(min_size) = self.min_size_update.take() {
if let Some(final_size) = self.pending_size_update.take() {
if min_size < final_size {
encode_integer(dst, min_size, 5, 0x20);
}
encode_integer(dst, final_size, 5, 0x20);
}
}
}
fn encode_header(&mut self, header: &Header, dst: &mut BytesMut, index: bool) {
let normalized_name = if header.name.bytes().any(|byte| byte.is_ascii_uppercase()) {
Cow::Owned(header.name.to_ascii_lowercase())
} else {
Cow::Borrowed(header.name.as_str())
};
let name = normalized_name.as_ref();
let value = header.value.as_str();
if index {
if let Some(idx) =
find_static(name, value).or_else(|| self.dynamic_table.find(name, value))
{
encode_integer(dst, idx, 7, 0x80);
return;
}
}
let name_idx = find_static_name(name).or_else(|| self.dynamic_table.find_name(name));
if index {
if let Some(idx) = name_idx {
encode_integer(dst, idx, 6, 0x40);
} else {
dst.put_u8(0x40);
encode_string(dst, name, self.use_huffman);
}
encode_string(dst, value, self.use_huffman);
let indexed_header = Header {
name: normalized_name.into_owned(),
value: header.value.clone(),
};
self.dynamic_table.insert(indexed_header);
} else {
if let Some(idx) = name_idx {
encode_integer(dst, idx, 4, 0x10);
} else {
dst.put_u8(0x10);
encode_string(dst, name, self.use_huffman);
}
encode_string(dst, value, self.use_huffman);
}
}
}
impl Default for Encoder {
fn default() -> Self {
Self::new()
}
}
const MAX_STRING_LENGTH: usize = 256 * 1024;
const MAX_SIZE_UPDATES: usize = 16;
#[derive(Debug)]
pub struct Decoder {
dynamic_table: DynamicTable,
max_header_list_size: usize,
allowed_table_size: usize,
}
impl Decoder {
#[must_use]
pub fn new() -> Self {
Self::with_max_size(DEFAULT_MAX_TABLE_SIZE)
}
#[must_use]
pub fn with_max_size(max_size: usize) -> Self {
let capped_size = max_size.min(MAX_ALLOWED_TABLE_SIZE);
Self {
dynamic_table: DynamicTable::with_max_size(capped_size),
max_header_list_size: 16384,
allowed_table_size: capped_size,
}
}
pub fn set_max_header_list_size(&mut self, size: usize) {
self.max_header_list_size = size;
}
pub fn set_allowed_table_size(&mut self, size: usize) {
self.allowed_table_size = size.min(MAX_ALLOWED_TABLE_SIZE);
}
#[must_use]
pub fn dynamic_table_size(&self) -> usize {
self.dynamic_table.size()
}
#[must_use]
pub fn dynamic_table_max_size(&self) -> usize {
self.dynamic_table.max_size()
}
#[must_use]
pub fn allowed_table_size(&self) -> usize {
self.allowed_table_size
}
pub fn decode(&mut self, src: &mut Bytes) -> Result<Vec<Header>, H2Error> {
let mut headers = Vec::with_capacity(8);
let mut total_size = 0;
let mut size_update_count: usize = 0;
while !src.is_empty() && (src[0] & 0xe0 == 0x20) {
size_update_count = size_update_count.saturating_add(1);
if size_update_count > MAX_SIZE_UPDATES {
return Err(H2Error::compression(
"too many consecutive dynamic table size updates",
));
}
let new_size = decode_integer(src, 5)?;
if new_size > self.allowed_table_size {
return Err(H2Error::compression(
"dynamic table size update exceeds allowed maximum",
));
}
self.dynamic_table.set_max_size(new_size);
}
while !src.is_empty() {
let remaining_budget = self.max_header_list_size.saturating_sub(total_size);
let header = self.decode_header(src, remaining_budget)?;
total_size = total_size.saturating_add(header.size());
if total_size > self.max_header_list_size {
return Err(H2Error::compression("header list too large"));
}
headers.push(header);
}
Ok(headers)
}
fn decode_header(
&mut self,
src: &mut Bytes,
remaining_budget: usize,
) -> Result<Header, H2Error> {
if src.is_empty() {
return Err(H2Error::compression("unexpected end of header block"));
}
let first = src[0];
if first & 0x80 != 0 {
let index = decode_integer(src, 7)?;
return self.get_indexed(index);
}
if first & 0x40 != 0 {
let (name, value) = self.decode_literal(src, 6, remaining_budget)?;
let header = Header::new(name, value);
self.dynamic_table.insert(header.clone());
return Ok(header);
}
if first & 0x20 != 0 {
return Err(H2Error::compression(
"dynamic table size update after first header in block",
));
}
if first & 0x10 != 0 {
let (name, value) = self.decode_literal(src, 4, remaining_budget)?;
return Ok(Header::new(name, value));
}
let (name, value) = self.decode_literal(src, 4, remaining_budget)?;
Ok(Header::new(name, value))
}
fn decode_literal(
&self,
src: &mut Bytes,
prefix_bits: u8,
remaining_budget: usize,
) -> Result<(String, String), H2Error> {
let index = decode_integer(src, prefix_bits)?;
let name = if index == 0 {
let n = decode_string_bounded(src, remaining_budget)?;
validate_header_name(&n)?;
n
} else {
self.get_indexed_name(index)?
};
let value_budget = remaining_budget.saturating_sub(name.len());
let value = decode_string_bounded(src, value_budget)?;
validate_header_value(&value)?;
Ok((name, value))
}
fn get_indexed(&self, index: usize) -> Result<Header, H2Error> {
if index == 0 {
return Err(H2Error::compression("invalid index 0"));
}
if index <= STATIC_TABLE.len() {
let (name, value) =
get_static(index).ok_or_else(|| H2Error::compression("invalid static index"))?;
Ok(Header::new(name, value))
} else {
let dyn_index = index - STATIC_TABLE.len();
self.dynamic_table
.get(dyn_index)
.ok_or_else(|| H2Error::compression("invalid dynamic index"))
}
}
fn get_indexed_name(&self, index: usize) -> Result<String, H2Error> {
if index == 0 {
return Err(H2Error::compression("invalid index 0"));
}
if index <= STATIC_TABLE.len() {
let (name, _) =
get_static(index).ok_or_else(|| H2Error::compression("invalid static index"))?;
Ok(name.to_string())
} else {
let dyn_index = index - STATIC_TABLE.len();
self.dynamic_table
.get(dyn_index)
.map(|h| h.name)
.ok_or_else(|| H2Error::compression("invalid dynamic index"))
}
}
}
impl Default for Decoder {
fn default() -> Self {
Self::new()
}
}
#[inline]
fn encode_integer(dst: &mut BytesMut, value: usize, prefix_bits: u8, prefix: u8) {
let max_first = (1_usize << prefix_bits).saturating_sub(1);
if value < max_first {
dst.put_u8(prefix | value as u8);
} else {
dst.put_u8(prefix | max_first as u8);
let mut remaining = value - max_first;
while remaining >= 128 {
dst.put_u8((remaining & 0x7f) as u8 | 0x80);
remaining >>= 7;
}
dst.put_u8(remaining as u8);
}
}
fn decode_integer(src: &mut Bytes, prefix_bits: u8) -> Result<usize, H2Error> {
if src.is_empty() {
return Err(H2Error::compression("unexpected end of integer"));
}
let max_first = (1_usize << prefix_bits).saturating_sub(1);
let first = src[0] & max_first as u8;
let _ = src.split_to(1);
if (first as usize) < max_first {
return Ok(first as usize);
}
let mut value = max_first;
let mut shift = 0;
loop {
if src.is_empty() {
return Err(H2Error::compression("unexpected end of integer"));
}
let byte = src[0];
let _ = src.split_to(1);
if shift > 28 {
return Err(H2Error::compression("integer too large"));
}
let multiplier = 1usize
.checked_shl(shift)
.ok_or_else(|| H2Error::compression("integer overflow in shift"))?;
let increment = ((byte & 0x7f) as usize)
.checked_mul(multiplier)
.ok_or_else(|| H2Error::compression("integer overflow in multiply"))?;
value = value
.checked_add(increment)
.ok_or_else(|| H2Error::compression("integer overflow in addition"))?;
shift += 7;
if byte & 0x80 == 0 {
break;
}
}
Ok(value)
}
const fn build_bit_masks() -> [u64; 65] {
let mut masks = [0u64; 65];
let mut i = 0usize;
while i <= 64 {
masks[i] = if i == 64 {
u64::MAX
} else {
(1u64 << i).saturating_sub(1)
};
i += 1;
}
masks
}
const BIT_MASKS: [u64; 65] = build_bit_masks();
pub(crate) fn huffman_encoded_size(src: &[u8]) -> usize {
let mut total_bits: u32 = 0;
for &byte in src {
let (_, code_bits) = HUFFMAN_TABLE[byte as usize];
total_bits += u32::from(code_bits);
}
((total_bits + 7) / 8) as usize
}
pub(crate) fn encode_huffman_to_buffer(dst: &mut BytesMut, src: &[u8]) {
dst.reserve(src.len());
let mut accumulator: u64 = 0;
let mut bits: u32 = 0;
for &byte in src {
let (code, code_bits) = HUFFMAN_TABLE[byte as usize];
let code_bits_u32 = u32::from(code_bits);
accumulator = (accumulator << code_bits_u32) | u64::from(code);
bits += code_bits_u32;
while bits >= 8 {
bits -= 8;
dst.put_u8((accumulator >> bits) as u8);
accumulator &= BIT_MASKS[bits as usize];
}
}
if bits > 0 {
let padding = 8 - bits;
accumulator = (accumulator << padding) | BIT_MASKS[padding as usize];
dst.put_u8(accumulator as u8);
}
}
#[cfg(test)]
fn encode_huffman(src: &[u8]) -> Vec<u8> {
let mut buf = BytesMut::new();
encode_huffman_to_buffer(&mut buf, src);
buf.to_vec()
}
#[inline]
fn encode_string(dst: &mut BytesMut, value: &str, use_huffman: bool) {
if use_huffman {
let src_bytes = value.as_bytes();
let encoded_size = huffman_encoded_size(src_bytes);
encode_integer(dst, encoded_size, 7, 0x80);
encode_huffman_to_buffer(dst, src_bytes);
} else {
let bytes = value.as_bytes();
encode_integer(dst, bytes.len(), 7, 0x00);
dst.extend_from_slice(bytes);
}
}
fn validate_header_name(name: &str) -> Result<(), H2Error> {
if name.is_empty() {
return Err(H2Error::compression("empty header name"));
}
for (i, b) in name.bytes().enumerate() {
let valid = matches!(b,
b'a'..=b'z' | b'0'..=b'9'
| b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*'
| b'+' | b'-' | b'.' | b'^' | b'_' | b'`' | b'|' | b'~'
) || (b == b':' && i == 0);
if !valid {
return Err(H2Error::compression(
"invalid character in header name (RFC 9113 Section 8.2.1)",
));
}
}
Ok(())
}
fn validate_header_value(value: &str) -> Result<(), H2Error> {
for b in value.bytes() {
if matches!(b, b'\0' | b'\r' | b'\n') {
return Err(H2Error::compression(
"invalid character in header value (RFC 9113 Section 8.2.1)",
));
}
}
Ok(())
}
#[cfg(test)]
fn decode_string(src: &mut Bytes) -> Result<String, H2Error> {
decode_string_bounded(src, MAX_STRING_LENGTH)
}
fn decode_string_bounded(src: &mut Bytes, max_len: usize) -> Result<String, H2Error> {
if src.is_empty() {
return Err(H2Error::compression("unexpected end of string"));
}
let huffman = src[0] & 0x80 != 0;
let length = decode_integer(src, 7)?;
let effective_max = max_len.min(MAX_STRING_LENGTH);
if length > effective_max {
return Err(H2Error::compression("string length exceeds budget"));
}
if src.len() < length {
return Err(H2Error::compression("string length exceeds buffer"));
}
let data = src.split_to(length);
if huffman {
decode_huffman(&data)
} else {
std::str::from_utf8(&data)
.map(str::to_owned)
.map_err(|_| H2Error::compression("invalid UTF-8 in header"))
}
}
#[rustfmt::skip]
#[allow(clippy::unreadable_literal)]
static HUFFMAN_TABLE: [(u32, u8); 257] = [
(0x1ff8, 13), (0x7fffd8, 23), (0xfffffe2, 28), (0xfffffe3, 28), (0xfffffe4, 28), (0xfffffe5, 28), (0xfffffe6, 28), (0xfffffe7, 28), (0xfffffe8, 28), (0xffffea, 24), (0x3ffffffc, 30), (0xfffffe9, 28), (0xfffffea, 28), (0x3ffffffd, 30), (0xfffffeb, 28), (0xfffffec, 28), (0xfffffed, 28), (0xfffffee, 28), (0xfffffef, 28), (0xffffff0, 28), (0xffffff1, 28), (0xffffff2, 28), (0x3ffffffe, 30), (0xffffff3, 28), (0xffffff4, 28), (0xffffff5, 28), (0xffffff6, 28), (0xffffff7, 28), (0xffffff8, 28), (0xffffff9, 28), (0xffffffa, 28), (0xffffffb, 28), (0x14, 6), (0x3f8, 10), (0x3f9, 10), (0xffa, 12), (0x1ff9, 13), (0x15, 6), (0xf8, 8), (0x7fa, 11), (0x3fa, 10), (0x3fb, 10), (0xf9, 8), (0x7fb, 11), (0xfa, 8), (0x16, 6), (0x17, 6), (0x18, 6), (0x0, 5), (0x1, 5), (0x2, 5), (0x19, 6), (0x1a, 6), (0x1b, 6), (0x1c, 6), (0x1d, 6), (0x1e, 6), (0x1f, 6), (0x5c, 7), (0xfb, 8), (0x7ffc, 15), (0x20, 6), (0xffb, 12), (0x3fc, 10), (0x1ffa, 13), (0x21, 6), (0x5d, 7), (0x5e, 7), (0x5f, 7), (0x60, 7), (0x61, 7), (0x62, 7), (0x63, 7), (0x64, 7), (0x65, 7), (0x66, 7), (0x67, 7), (0x68, 7), (0x69, 7), (0x6a, 7), (0x6b, 7), (0x6c, 7), (0x6d, 7), (0x6e, 7), (0x6f, 7), (0x70, 7), (0x71, 7), (0x72, 7), (0xfc, 8), (0x73, 7), (0xfd, 8), (0x1ffb, 13), (0x7fff0, 19), (0x1ffc, 13), (0x3ffc, 14), (0x22, 6), (0x7ffd, 15), (0x3, 5), (0x23, 6), (0x4, 5), (0x24, 6), (0x5, 5), (0x25, 6), (0x26, 6), (0x27, 6), (0x6, 5), (0x74, 7), (0x75, 7), (0x28, 6), (0x29, 6), (0x2a, 6), (0x7, 5), (0x2b, 6), (0x76, 7), (0x2c, 6), (0x8, 5), (0x9, 5), (0x2d, 6), (0x77, 7), (0x78, 7), (0x79, 7), (0x7a, 7), (0x7b, 7), (0x7ffe, 15), (0x7fc, 11), (0x3ffd, 14), (0x1ffd, 13), (0xffffffc, 28), (0xfffe6, 20), (0x3fffd2, 22), (0xfffe7, 20), (0xfffe8, 20), (0x3fffd3, 22), (0x3fffd4, 22), (0x3fffd5, 22), (0x7fffd9, 23), (0x3fffd6, 22), (0x7fffda, 23), (0x7fffdb, 23), (0x7fffdc, 23), (0x7fffdd, 23), (0x7fffde, 23), (0xffffeb, 24), (0x7fffdf, 23), (0xffffec, 24), (0xffffed, 24), (0x3fffd7, 22), (0x7fffe0, 23), (0xffffee, 24), (0x7fffe1, 23), (0x7fffe2, 23), (0x7fffe3, 23), (0x7fffe4, 23), (0x1fffdc, 21), (0x3fffd8, 22), (0x7fffe5, 23), (0x3fffd9, 22), (0x7fffe6, 23), (0x7fffe7, 23), (0xffffef, 24), (0x3fffda, 22), (0x1fffdd, 21), (0xfffe9, 20), (0x3fffdb, 22), (0x3fffdc, 22), (0x7fffe8, 23), (0x7fffe9, 23), (0x1fffde, 21), (0x7fffea, 23), (0x3fffdd, 22), (0x3fffde, 22), (0xfffff0, 24), (0x1fffdf, 21), (0x3fffdf, 22), (0x7fffeb, 23), (0x7fffec, 23), (0x1fffe0, 21), (0x1fffe1, 21), (0x3fffe0, 22), (0x1fffe2, 21), (0x7fffed, 23), (0x3fffe1, 22), (0x7fffee, 23), (0x7fffef, 23), (0xfffea, 20), (0x3fffe2, 22), (0x3fffe3, 22), (0x3fffe4, 22), (0x7ffff0, 23), (0x3fffe5, 22), (0x3fffe6, 22), (0x7ffff1, 23), (0x3ffffe0, 26), (0x3ffffe1, 26), (0xfffeb, 20), (0x7fff1, 19), (0x3fffe7, 22), (0x7ffff2, 23), (0x3fffe8, 22), (0x1ffffec, 25), (0x3ffffe2, 26), (0x3ffffe3, 26), (0x3ffffe4, 26), (0x7ffffde, 27), (0x7ffffdf, 27), (0x3ffffe5, 26), (0xfffff1, 24), (0x1ffffed, 25), (0x7fff2, 19), (0x1fffe3, 21), (0x3ffffe6, 26), (0x7ffffe0, 27), (0x7ffffe1, 27), (0x3ffffe7, 26), (0x7ffffe2, 27), (0xfffff2, 24), (0x1fffe4, 21), (0x1fffe5, 21), (0x3ffffe8, 26), (0x3ffffe9, 26), (0xffffffd, 28), (0x7ffffe3, 27), (0x7ffffe4, 27), (0x7ffffe5, 27), (0xfffec, 20), (0xfffff3, 24), (0xfffed, 20), (0x1fffe6, 21), (0x3fffe9, 22), (0x1fffe7, 21), (0x1fffe8, 21), (0x7ffff3, 23), (0x3fffea, 22), (0x3fffeb, 22), (0x1ffffee, 25), (0x1ffffef, 25), (0xfffff4, 24), (0xfffff5, 24), (0x3ffffea, 26), (0x7ffff4, 23), (0x3ffffeb, 26), (0x7ffffe6, 27), (0x3ffffec, 26), (0x3ffffed, 26), (0x7ffffe7, 27), (0x7ffffe8, 27), (0x7ffffe9, 27), (0x7ffffea, 27), (0x7ffffeb, 27), (0xffffffe, 28), (0x7ffffec, 27), (0x7ffffed, 27), (0x7ffffee, 27), (0x7ffffef, 27), (0x7fffff0, 27), (0x3ffffee, 26), (0x3fffffff, 30), ];
pub(crate) fn decode_huffman(src: &Bytes) -> Result<String, H2Error> {
let estimated_symbols = src.len().saturating_mul(8).saturating_add(4) / 5;
let mut result = Vec::with_capacity(estimated_symbols);
let table: &[[HuffmanDecodeEntry; 16]; 256] = &HUFFMAN_DECODE_TABLE;
let mut state: u8 = 0;
let mut accepted = true;
for &byte in src.iter() {
let entry = table[state as usize][((byte >> 4) & 0x0F) as usize];
if entry.flags & HUFF_FAIL != 0 {
return Err(H2Error::compression("invalid huffman code"));
}
if entry.flags & HUFF_SYM != 0 {
result.push(entry.sym);
}
state = entry.next_state;
let entry = table[state as usize][(byte & 0x0F) as usize];
if entry.flags & HUFF_FAIL != 0 {
return Err(H2Error::compression("invalid huffman code"));
}
if entry.flags & HUFF_SYM != 0 {
result.push(entry.sym);
}
state = entry.next_state;
accepted = entry.flags & HUFF_ACCEPTED != 0;
}
if !accepted {
return Err(H2Error::compression("invalid huffman padding"));
}
String::from_utf8(result).map_err(|_| H2Error::compression("invalid UTF-8 in huffman"))
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::super::error::ErrorCode;
use super::*;
fn assert_compression_error<T>(result: Result<T, H2Error>) {
match result {
Ok(_) => panic!("expected compression error"),
Err(err) => assert_eq!(err.code, ErrorCode::CompressionError),
}
}
#[test]
fn test_integer_encoding_small() {
let mut buf = BytesMut::new();
encode_integer(&mut buf, 10, 5, 0x00);
assert_eq!(buf.as_ref(), &[10]);
let mut src = buf.freeze();
let decoded = decode_integer(&mut src, 5).unwrap();
assert_eq!(decoded, 10);
}
#[test]
fn test_integer_encoding_large() {
let mut buf = BytesMut::new();
encode_integer(&mut buf, 1337, 5, 0x00);
assert_eq!(buf.as_ref(), &[31, 154, 10]);
let mut src = buf.freeze();
let decoded = decode_integer(&mut src, 5).unwrap();
assert_eq!(decoded, 1337);
}
#[test]
fn test_integer_decode_empty() {
let mut src = Bytes::new();
assert_compression_error(decode_integer(&mut src, 5));
}
#[test]
fn test_integer_decode_truncated() {
let mut src = Bytes::from_static(&[0x1f, 0x80]);
assert_compression_error(decode_integer(&mut src, 5));
}
#[test]
fn test_integer_decode_shift_overflow() {
let mut bytes = vec![0x1f];
bytes.extend_from_slice(&[0x80; 6]);
let mut src = Bytes::from(bytes);
assert_compression_error(decode_integer(&mut src, 5));
}
#[test]
fn test_string_encoding_literal() {
let mut buf = BytesMut::new();
encode_string(&mut buf, "hello", false);
let mut src = buf.freeze();
let decoded = decode_string(&mut src).unwrap();
assert_eq!(decoded, "hello");
}
#[test]
fn test_string_decode_length_exceeds_buffer() {
let mut src = Bytes::from_static(&[0x03, b'a', b'b']);
assert_compression_error(decode_string(&mut src));
}
#[test]
fn test_string_decode_invalid_utf8() {
let mut src = Bytes::from_static(&[0x01, 0xff]);
assert_compression_error(decode_string(&mut src));
}
#[test]
fn test_huffman_decode_invalid_padding() {
let mut src = Bytes::from_static(&[0x81, 0x00]);
assert_compression_error(decode_string(&mut src));
}
#[test]
fn test_indexed_header_zero_rejected() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0x80]); assert_compression_error(decoder.decode(&mut src));
}
#[test]
fn test_dynamic_table_size_update_exceeds_allowed() {
let mut decoder = Decoder::new();
decoder.set_allowed_table_size(1);
let mut buf = BytesMut::new();
encode_integer(&mut buf, 2, 5, 0x20);
let mut src = buf.freeze();
assert_compression_error(decoder.decode(&mut src));
}
#[test]
fn test_dynamic_table_size_update_without_header_is_accepted() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
encode_integer(&mut buf, 0, 5, 0x20);
let mut src = buf.freeze();
let headers = decoder.decode(&mut src).unwrap();
assert!(headers.is_empty());
assert_eq!(decoder.dynamic_table.max_size(), 0);
}
#[test]
fn test_multiple_size_updates_without_headers_apply_last_value() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
encode_integer(&mut buf, 1024, 5, 0x20);
encode_integer(&mut buf, 512, 5, 0x20);
let mut src = buf.freeze();
let headers = decoder.decode(&mut src).unwrap();
assert!(headers.is_empty());
assert_eq!(decoder.dynamic_table.max_size(), 512);
}
#[test]
fn test_dynamic_table_size_update_too_many() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
for _ in 0..17 {
encode_integer(&mut buf, 0, 5, 0x20);
}
let mut src = buf.freeze();
assert_compression_error(decoder.decode(&mut src));
}
#[test]
fn test_header_list_size_exceeded() {
let mut decoder = Decoder::new();
decoder.set_max_header_list_size(1);
let mut buf = BytesMut::new();
encode_integer(&mut buf, 0, 4, 0x00);
encode_string(&mut buf, "a", false);
encode_string(&mut buf, "b", false);
let mut src = buf.freeze();
assert_compression_error(decoder.decode(&mut src));
}
#[test]
fn test_decoder_caps_allowed_table_size() {
let decoder = Decoder::with_max_size(MAX_ALLOWED_TABLE_SIZE.saturating_add(1));
assert_eq!(decoder.allowed_table_size, MAX_ALLOWED_TABLE_SIZE);
assert_eq!(decoder.dynamic_table.max_size(), MAX_ALLOWED_TABLE_SIZE);
}
#[test]
fn test_encoder_with_max_size_caps_to_allowed_maximum() {
let encoder = Encoder::with_max_size(MAX_ALLOWED_TABLE_SIZE.saturating_add(1));
assert_eq!(encoder.dynamic_table_max_size(), MAX_ALLOWED_TABLE_SIZE);
}
#[test]
fn test_dynamic_table_set_max_size_caps_to_allowed_maximum() {
let mut table = DynamicTable::new();
table.set_max_size(MAX_ALLOWED_TABLE_SIZE.saturating_add(1));
assert_eq!(table.max_size(), MAX_ALLOWED_TABLE_SIZE);
}
#[test]
fn test_set_allowed_table_size_caps() {
let mut decoder = Decoder::new();
decoder.set_allowed_table_size(MAX_ALLOWED_TABLE_SIZE.saturating_add(1));
assert_eq!(decoder.allowed_table_size, MAX_ALLOWED_TABLE_SIZE);
}
#[test]
fn test_dynamic_table_insert() {
let mut table = DynamicTable::new();
table.insert(Header::new("custom-header", "custom-value"));
assert_eq!(
table.size(),
"custom-header".len() + "custom-value".len() + 32
);
assert!(table.get(1).is_some());
}
#[test]
fn test_dynamic_table_eviction() {
let mut table = DynamicTable::with_max_size(100);
table.insert(Header::new("header1", "value1")); table.insert(Header::new("header2", "value2"));
assert!(table.size() <= 100);
}
#[test]
fn dynamic_table_find_matches_linear_scan_after_inserts() {
let mut table = DynamicTable::new();
table.insert(Header::new("a", "1"));
table.insert(Header::new("b", "2"));
table.insert(Header::new("c", "3"));
let base = STATIC_TABLE.len();
assert_eq!(table.find("c", "3"), Some(base + 1));
assert_eq!(table.find("b", "2"), Some(base + 2));
assert_eq!(table.find("a", "1"), Some(base + 3));
assert_eq!(table.find("nope", "nope"), None);
assert_eq!(table.find_name("a"), Some(base + 3));
assert_eq!(table.find_name("b"), Some(base + 2));
assert_eq!(table.find_name("c"), Some(base + 1));
assert_eq!(table.find_name("missing"), None);
}
#[test]
fn dynamic_table_find_returns_newest_among_duplicates() {
let mut table = DynamicTable::new();
table.insert(Header::new("a", "1")); table.insert(Header::new("b", "2")); table.insert(Header::new("a", "1"));
let base = STATIC_TABLE.len();
assert_eq!(table.find("a", "1"), Some(base + 1));
assert_eq!(table.find_name("a"), Some(base + 1));
let entry_size = 32 + 1 + 1;
table.set_max_size(entry_size * 2);
assert!(table.size() <= entry_size * 2);
assert_eq!(table.find("a", "1"), Some(base + 1));
assert_eq!(table.find_name("a"), Some(base + 1));
}
#[test]
fn dynamic_table_find_name_picks_newest_for_distinct_values() {
let mut table = DynamicTable::new();
table.insert(Header::new("a", "old"));
table.insert(Header::new("b", "filler"));
table.insert(Header::new("a", "new"));
let base = STATIC_TABLE.len();
assert_eq!(table.find_name("a"), Some(base + 1));
assert_eq!(table.find("a", "new"), Some(base + 1));
assert_eq!(table.find("a", "old"), Some(base + 3));
assert_eq!(table.find("a", "missing"), None);
}
#[test]
fn dynamic_table_index_drops_evicted_entries() {
let mut table = DynamicTable::with_max_size(90);
table.insert(Header::new("header1", "value1"));
table.insert(Header::new("header2", "value2"));
table.insert(Header::new("header3", "value3"));
assert!(table.size() <= 90);
assert_eq!(table.find("header1", "value1"), None);
assert_eq!(table.find_name("header1"), None);
let base = STATIC_TABLE.len();
assert_eq!(table.find("header3", "value3"), Some(base + 1));
assert_eq!(table.find("header2", "value2"), Some(base + 2));
}
#[test]
fn dynamic_table_oversized_insert_clears_indices() {
let mut table = DynamicTable::with_max_size(100);
table.insert(Header::new("a", "1"));
table.insert(Header::new("b", "2"));
assert!(table.find("a", "1").is_some());
let big_value: String = "x".repeat(200);
table.insert(Header::new("big", &big_value));
assert_eq!(table.size(), 0);
assert_eq!(table.find("a", "1"), None);
assert_eq!(table.find("b", "2"), None);
assert_eq!(table.find("big", &big_value), None);
assert_eq!(table.find_name("a"), None);
assert_eq!(table.find_name("big"), None);
}
#[test]
fn dynamic_table_set_max_size_shrink_updates_indices() {
let mut table = DynamicTable::new();
table.insert(Header::new("a", "1"));
table.insert(Header::new("b", "2"));
table.insert(Header::new("c", "3"));
let entry_size = 32 + 1 + 1;
table.set_max_size(entry_size);
assert_eq!(table.size(), entry_size);
let base = STATIC_TABLE.len();
assert_eq!(table.find("c", "3"), Some(base + 1));
assert_eq!(table.find("b", "2"), None);
assert_eq!(table.find("a", "1"), None);
assert_eq!(table.find_name("a"), None);
assert_eq!(table.find_name("b"), None);
assert_eq!(table.find_name("c"), Some(base + 1));
}
#[test]
fn dynamic_table_index_consistent_under_churn() {
let mut table = DynamicTable::with_max_size(4 * (32 + 4 + 4));
for i in 0..32 {
table.insert(Header::new(format!("n{i:03}"), format!("v{i:03}")));
}
let base = STATIC_TABLE.len();
for hpack_idx in 1..=table.entries.len() {
let header = table.get(hpack_idx).expect("entry must exist");
assert_eq!(
table.find(&header.name, &header.value),
Some(base + hpack_idx),
"find disagrees with get at hpack_idx={hpack_idx}"
);
assert_eq!(
table.find_name(&header.name),
Some(base + hpack_idx),
"find_name disagrees with get at hpack_idx={hpack_idx}"
);
}
assert_eq!(table.find("n000", "v000"), None);
assert_eq!(table.find_name("n000"), None);
}
#[test]
fn test_encoder_decoder_roundtrip() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let headers = vec![
Header::new(":method", "GET"),
Header::new(":path", "/"),
Header::new(":scheme", "https"),
Header::new(":authority", "example.com"),
Header::new("accept", "text/html"),
];
let mut encoded_block = BytesMut::new();
encoder.encode(&headers, &mut encoded_block);
let mut decoder = Decoder::new();
let mut src = encoded_block.freeze();
let decoded_headers = decoder.decode(&mut src).unwrap();
assert_eq!(decoded_headers.len(), headers.len());
for (orig, dec) in headers.iter().zip(decoded_headers.iter()) {
assert_eq!(orig.name, dec.name);
assert_eq!(orig.value, dec.value);
}
}
#[test]
fn test_static_table_indexed() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0x82]); let headers = decoder.decode(&mut src).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, ":method");
assert_eq!(headers[0].value, "GET");
}
#[test]
fn test_huffman_encode_decode_roundtrip() {
let inputs = [
"www.example.com",
"no-cache",
"custom-key",
"custom-value",
"",
"a",
"Hello, World!",
];
for &input in &inputs {
let encoded = encode_huffman(input.as_bytes());
let encoded_bytes = Bytes::from(encoded);
let decoded = decode_huffman(&encoded_bytes).unwrap();
assert_eq!(decoded, input, "roundtrip failed for {input:?}");
}
}
#[test]
fn test_huffman_encoding_is_smaller() {
let input = b"www.example.com";
let encoded = encode_huffman(input);
assert!(
encoded.len() < input.len(),
"huffman should compress ASCII text: {} >= {}",
encoded.len(),
input.len()
);
}
#[test]
fn test_string_encoding_huffman_roundtrip() {
let mut buf = BytesMut::new();
encode_string(&mut buf, "hello", true);
assert_ne!(buf[0] & 0x80, 0, "huffman flag should be set");
let mut src = buf.freeze();
let decoded = decode_string(&mut src).unwrap();
assert_eq!(decoded, "hello");
}
#[test]
fn test_encoder_decoder_roundtrip_with_huffman() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(true);
let headers = vec![
Header::new(":method", "GET"),
Header::new(":path", "/index.html"),
Header::new(":scheme", "https"),
Header::new(":authority", "www.example.com"),
Header::new("accept-encoding", "gzip, deflate"),
];
let mut encoded_block = BytesMut::new();
encoder.encode(&headers, &mut encoded_block);
let mut decoder = Decoder::new();
let mut src = encoded_block.freeze();
let decoded_headers = decoder.decode(&mut src).unwrap();
assert_eq!(decoded_headers.len(), headers.len());
for (orig, dec) in headers.iter().zip(decoded_headers.iter()) {
assert_eq!(orig.name, dec.name, "name mismatch for {:?}", orig.name);
assert_eq!(orig.value, dec.value, "value mismatch for {:?}", orig.name);
}
}
#[test]
fn test_rfc7541_c1_integer_representation() {
let mut buf = BytesMut::new();
encode_integer(&mut buf, 10, 5, 0x00);
assert_eq!(&buf[..], &[0x0a]);
buf.clear();
encode_integer(&mut buf, 1337, 5, 0x00);
assert_eq!(&buf[..], &[0x1f, 0x9a, 0x0a]);
buf.clear();
encode_integer(&mut buf, 42, 8, 0x00);
assert_eq!(&buf[..], &[0x2a]);
}
#[test]
fn test_rfc7541_integer_decode_roundtrip() {
for &(value, prefix_bits) in &[
(0_usize, 5_u8),
(1, 5),
(30, 5),
(31, 5),
(32, 5),
(127, 7),
(128, 7),
(255, 8),
(256, 8),
(1337, 5),
(65535, 8),
] {
let mut buf = BytesMut::new();
encode_integer(&mut buf, value, prefix_bits, 0x00);
let mut src = buf.freeze();
let decoded = decode_integer(&mut src, prefix_bits).unwrap();
assert_eq!(
decoded, value,
"roundtrip failed for {value} with {prefix_bits}-bit prefix"
);
}
}
#[test]
fn test_rfc7541_c2_header_field_indexed() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0x82]);
let headers = decoder.decode(&mut src).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, ":method");
assert_eq!(headers[0].value, "GET");
}
#[test]
fn test_rfc7541_c3_request_without_huffman() {
let wire: &[u8] = &[
0x82, 0x86, 0x84, 0x41, 0x0f, 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 mut decoder = Decoder::new();
let mut src = Bytes::copy_from_slice(wire);
let headers = decoder.decode(&mut src).unwrap();
assert_eq!(headers.len(), 4);
assert_eq!(headers[0].name, ":method");
assert_eq!(headers[0].value, "GET");
assert_eq!(headers[1].name, ":scheme");
assert_eq!(headers[1].value, "http");
assert_eq!(headers[2].name, ":path");
assert_eq!(headers[2].value, "/");
assert_eq!(headers[3].name, ":authority");
assert_eq!(headers[3].value, "www.example.com");
}
#[test]
fn test_rfc7541_c4_request_with_huffman() {
let mut enc = Encoder::new();
enc.set_use_huffman(true);
let headers = vec![
Header::new(":method", "GET"),
Header::new(":scheme", "http"),
Header::new(":path", "/"),
Header::new(":authority", "www.example.com"),
];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let mut src = buf.freeze();
let headers_out = dec.decode(&mut src).unwrap();
assert_eq!(headers_out.len(), 4);
assert_eq!(headers_out[3].value, "www.example.com");
}
#[test]
fn test_rfc7541_c4_1_first_request_exact_wire_with_huffman() {
let headers = vec![
Header::new(":method", "GET"),
Header::new(":scheme", "http"),
Header::new(":path", "/"),
Header::new(":authority", "www.example.com"),
];
let expected_wire: &[u8] = &[
0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab,
0x90, 0xf4, 0xff,
];
let mut decoder = Decoder::new();
let mut src = Bytes::copy_from_slice(expected_wire);
let decoded = decoder.decode(&mut src).expect("RFC 7541 C.4.1 decode");
assert_eq!(decoded, headers);
let mut encoder = Encoder::new();
encoder.set_use_huffman(true);
let mut encoded = BytesMut::new();
encoder.encode(&headers, &mut encoded);
assert_eq!(
encoded.as_ref(),
expected_wire,
"RFC 7541 C.4.1 wire image must match the specification exactly"
);
}
#[test]
fn test_rfc7541_c5_response_without_huffman() {
let mut enc = Encoder::new();
enc.set_use_huffman(false);
let headers = vec![
Header::new(":status", "302"),
Header::new("cache-control", "private"),
Header::new("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
Header::new("location", "https://www.example.com"),
];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let mut src = buf.freeze();
let headers_out = dec.decode(&mut src).unwrap();
assert_eq!(headers_out.len(), 4);
assert_eq!(headers_out[0].name, ":status");
assert_eq!(headers_out[0].value, "302");
assert_eq!(headers_out[3].name, "location");
assert_eq!(headers_out[3].value, "https://www.example.com");
}
#[test]
fn test_rfc7541_c5_1_first_response_exact_wire_without_huffman() {
let headers = vec![
Header::new(":status", "302"),
Header::new("cache-control", "private"),
Header::new("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
Header::new("location", "https://www.example.com"),
];
let expected_wire: &[u8] = &[
0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20,
0x32, 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x31, 0x20,
0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77,
0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
];
let mut decoder = Decoder::new();
let mut src = Bytes::copy_from_slice(expected_wire);
let decoded = decoder.decode(&mut src).expect("RFC 7541 C.5.1 decode");
assert_eq!(decoded, headers);
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let mut encoded = BytesMut::new();
encoder.encode(&headers, &mut encoded);
assert_eq!(
encoded.as_ref(),
expected_wire,
"RFC 7541 C.5.1 wire image must match the specification exactly"
);
}
#[test]
fn test_rfc7541_c6_1_first_response_exact_wire_with_huffman() {
let headers = vec![
Header::new(":status", "302"),
Header::new("cache-control", "private"),
Header::new("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
Header::new("location", "https://www.example.com"),
];
let expected_wire: &[u8] = &[
0x48, 0x82, 0x64, 0x02, 0x58, 0x85, 0xae, 0xc3, 0x77, 0x1a, 0x4b, 0x61, 0x96, 0xd0,
0x7a, 0xbe, 0x94, 0x10, 0x54, 0xd4, 0x44, 0xa8, 0x20, 0x05, 0x95, 0x04, 0x0b, 0x81,
0x66, 0xe0, 0x82, 0xa6, 0x2d, 0x1b, 0xff, 0x6e, 0x91, 0x9d, 0x29, 0xad, 0x17, 0x18,
0x63, 0xc7, 0x8f, 0x0b, 0x97, 0xc8, 0xe9, 0xae, 0x82, 0xae, 0x43, 0xd3,
];
let mut decoder = Decoder::new();
let mut src = Bytes::copy_from_slice(expected_wire);
let decoded = decoder.decode(&mut src).expect("RFC 7541 C.6.1 decode");
assert_eq!(decoded, headers);
let mut encoder = Encoder::new();
encoder.set_use_huffman(true);
let mut encoded = BytesMut::new();
encoder.encode(&headers, &mut encoded);
assert_eq!(
encoded.as_ref(),
expected_wire,
"RFC 7541 C.6.1 wire image must match the specification exactly"
);
}
#[test]
fn test_rfc7541_huffman_decode_www_example_com() {
let huffman_encoded: &[u8] = &[
0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4, 0xff,
];
let decoded = decode_huffman(&Bytes::copy_from_slice(huffman_encoded)).unwrap();
assert_eq!(decoded, "www.example.com");
}
#[test]
fn test_dynamic_table_empty() {
let table = DynamicTable::new();
assert_eq!(table.size(), 0);
assert!(table.get(1).is_none());
assert!(table.get(0).is_none());
assert!(table.get(100).is_none());
}
#[test]
fn test_dynamic_table_single_entry() {
let mut table = DynamicTable::new();
table.insert(Header::new("x-custom", "value"));
let entry = table.get(1).unwrap();
assert_eq!(entry.name, "x-custom");
assert_eq!(entry.value, "value");
assert!(table.get(2).is_none());
}
#[test]
fn test_dynamic_table_fifo_order() {
let mut table = DynamicTable::new();
table.insert(Header::new("first", "1"));
table.insert(Header::new("second", "2"));
table.insert(Header::new("third", "3"));
assert_eq!(table.get(1).unwrap().name, "third");
assert_eq!(table.get(2).unwrap().name, "second");
assert_eq!(table.get(3).unwrap().name, "first");
}
#[test]
fn test_dynamic_table_size_calculation() {
let mut table = DynamicTable::new();
let header = Header::new("custom", "value"); table.insert(header);
assert_eq!(table.size(), 43);
table.insert(Header::new("a", "b")); assert_eq!(table.size(), 43 + 34);
}
#[test]
fn test_dynamic_table_max_size_zero() {
let mut table = DynamicTable::with_max_size(0);
table.insert(Header::new("header", "value"));
assert_eq!(table.size(), 0);
assert!(table.get(1).is_none());
}
#[test]
fn test_dynamic_table_exact_fit() {
let mut table = DynamicTable::with_max_size(43);
table.insert(Header::new("custom", "value"));
assert_eq!(table.size(), 43);
assert!(table.get(1).is_some());
table.insert(Header::new("newkey", "newva")); assert_eq!(table.size(), 43);
assert_eq!(table.get(1).unwrap().name, "newkey");
assert!(table.get(2).is_none()); }
#[test]
fn test_dynamic_table_cascade_eviction() {
let mut table = DynamicTable::with_max_size(100);
table.insert(Header::new("a", "1"));
table.insert(Header::new("b", "2"));
table.insert(Header::new("c", "3"));
assert_eq!(table.size(), 68);
assert!(table.size() <= 100);
}
#[test]
fn test_dynamic_table_set_max_size() {
let mut table = DynamicTable::new();
table.insert(Header::new("header1", "value1")); table.insert(Header::new("header2", "value2"));
let initial_size = table.size();
assert_eq!(initial_size, 90);
table.set_max_size(50);
assert!(table.size() <= 50);
}
#[test]
fn test_dynamic_table_resize_to_zero() {
let mut table = DynamicTable::new();
table.insert(Header::new("key", "val"));
assert!(table.size() > 0);
table.set_max_size(0);
assert_eq!(table.size(), 0);
assert!(table.get(1).is_none());
}
#[test]
fn test_encoder_dynamic_table_reuse() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let headers1 = vec![Header::new("x-custom", "value1")];
let mut buf1 = BytesMut::new();
encoder.encode(&headers1, &mut buf1);
let headers2 = vec![Header::new("x-custom", "value2")];
let mut buf2 = BytesMut::new();
encoder.encode(&headers2, &mut buf2);
let mut decoder = Decoder::new();
let decoded1 = decoder.decode(&mut buf1.freeze()).unwrap();
let decoded2 = decoder.decode(&mut buf2.freeze()).unwrap();
assert_eq!(decoded1[0].name, "x-custom");
assert_eq!(decoded2[0].name, "x-custom");
}
#[test]
fn test_decoder_shared_state_across_blocks() {
let mut enc = Encoder::new();
enc.set_use_huffman(false);
let mut dec = Decoder::new();
let headers1 = vec![Header::new("x-custom", "initial")];
let mut buf1 = BytesMut::new();
enc.encode(&headers1, &mut buf1);
dec.decode(&mut buf1.freeze()).unwrap();
let headers2 = vec![Header::new("x-custom", "updated")];
let mut buf2 = BytesMut::new();
enc.encode(&headers2, &mut buf2);
let headers_out = dec.decode(&mut buf2.freeze()).unwrap();
assert_eq!(headers_out[0].value, "updated");
}
#[test]
fn test_decode_empty_input() {
let mut decoder = Decoder::new();
let mut src = Bytes::new();
let headers = decoder.decode(&mut src).unwrap();
assert!(headers.is_empty());
}
#[test]
fn test_decode_invalid_indexed_zero() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0x80]); let result = decoder.decode(&mut src);
assert!(result.is_err());
}
#[test]
fn test_decode_invalid_index_too_large() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0xff, 0xff, 0xff, 0x7f]); let result = decoder.decode(&mut src);
assert!(result.is_err());
}
#[test]
fn test_decode_truncated_integer() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[0x1f]); let result = decoder.decode(&mut src);
assert!(result.is_err());
}
#[test]
fn test_decode_truncated_string() {
let mut decoder = Decoder::new();
let mut src = Bytes::from_static(&[
0x40, 0x0a, b'a', b'b', b'c', ]);
let result = decoder.decode(&mut src);
assert!(result.is_err());
}
#[test]
fn test_decode_huffman_invalid_eos() {
let invalid_huffman: &[u8] = &[0xff, 0xff, 0xff, 0xff]; let result = decode_huffman(&Bytes::copy_from_slice(invalid_huffman));
assert_compression_error(result);
}
#[test]
fn test_decode_integer_overflow_protection() {
let mut src =
Bytes::from_static(&[0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]);
let result = decode_integer(&mut src, 7);
assert!(result.is_err());
}
#[test]
fn test_decode_literal_with_empty_name() {
let mut enc = Encoder::new();
enc.set_use_huffman(false);
let headers = vec![Header::new("", "value")];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let mut src = buf.freeze();
let result = dec.decode(&mut src);
assert_compression_error(result);
}
#[test]
fn test_decode_literal_with_empty_value() {
let mut enc = Encoder::new();
enc.set_use_huffman(false);
let headers = vec![Header::new("x-empty", "")];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let headers_out = dec.decode(&mut buf.freeze()).unwrap();
assert_eq!(headers_out[0].name, "x-empty");
assert_eq!(headers_out[0].value, "");
}
#[test]
fn test_static_table_all_entries_accessible() {
for idx in 1..=61usize {
let entry = get_static(idx);
assert!(entry.is_some(), "static table entry {idx} should exist");
}
assert!(get_static(62).is_none());
assert!(get_static(0).is_none());
}
#[test]
fn test_static_table_known_entries() {
let method_get = get_static(2).unwrap();
assert_eq!(method_get.0, ":method");
assert_eq!(method_get.1, "GET");
let method_post = get_static(3).unwrap();
assert_eq!(method_post.0, ":method");
assert_eq!(method_post.1, "POST");
let status_200 = get_static(8).unwrap();
assert_eq!(status_200.0, ":status");
assert_eq!(status_200.1, "200");
let status_404 = get_static(13).unwrap();
assert_eq!(status_404.0, ":status");
assert_eq!(status_404.1, "404");
}
#[test]
fn test_huffman_all_ascii_printable() {
let mut input = String::new();
for c in 32u8..=126 {
input.push(c as char);
}
let encoded = encode_huffman(input.as_bytes());
let decoded = decode_huffman(&Bytes::from(encoded)).unwrap();
assert_eq!(decoded, input);
}
#[test]
fn test_huffman_empty_string() {
let encoded = encode_huffman(b"");
assert!(encoded.is_empty());
let decoded = decode_huffman(&Bytes::new()).unwrap();
assert_eq!(decoded, "");
}
#[test]
fn test_sensitive_header_encoding() {
let mut enc = Encoder::new();
let mut dec = Decoder::new();
let headers = vec![
Header::new(":method", "GET"),
Header::new("authorization", "Bearer secret123"),
];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let headers_out = dec.decode(&mut buf.freeze()).unwrap();
assert_eq!(headers_out.len(), 2);
assert_eq!(headers_out[1].name, "authorization");
assert_eq!(headers_out[1].value, "Bearer secret123");
}
#[test]
fn test_large_header_value() {
let mut enc = Encoder::new();
enc.set_use_huffman(false);
let large_value: String = "x".repeat(4096);
let headers = vec![Header::new("x-large", &large_value)];
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let headers_out = dec.decode(&mut buf.freeze()).unwrap();
assert_eq!(headers_out[0].value, large_value);
}
#[test]
fn test_many_headers() {
let mut enc = Encoder::new();
enc.set_use_huffman(true);
let headers: Vec<Header> = (0..100)
.map(|i| Header::new(format!("x-header-{i}"), format!("value-{i}")))
.collect();
let mut buf = BytesMut::new();
enc.encode(&headers, &mut buf);
let mut dec = Decoder::new();
let headers_out = dec.decode(&mut buf.freeze()).unwrap();
assert_eq!(headers_out.len(), 100);
for (i, header) in headers_out.iter().enumerate() {
assert_eq!(header.name, format!("x-header-{i}"));
assert_eq!(header.value, format!("value-{i}"));
}
}
#[test]
fn test_deterministic_encoding() {
let mut encoder1 = Encoder::new();
let mut encoder2 = Encoder::new();
let headers = vec![
Header::new(":method", "GET"),
Header::new(":path", "/api/test"),
Header::new("content-type", "application/json"),
];
let mut buf1 = BytesMut::new();
let mut buf2 = BytesMut::new();
encoder1.encode(&headers, &mut buf1);
encoder2.encode(&headers, &mut buf2);
assert_eq!(buf1, buf2, "encoding should be deterministic");
}
#[test]
fn stress_test_hpack_integer_malformed() {
for shift in 0..=40u8 {
let mut data = vec![0x7f_u8]; data.extend(std::iter::repeat_n(0xff, shift as usize));
data.push(0x00); let mut src = Bytes::from(data);
let _ = decode_integer(&mut src, 7);
}
for seed in 0..1000u16 {
let len = ((seed % 10) + 1) as usize;
let mut data = Vec::with_capacity(len);
for i in 0..len {
data.push(((seed.wrapping_mul(31).wrapping_add(i as u16)) & 0xff) as u8);
}
if !data.is_empty() {
data[0] |= 0x1f;
}
let mut src = Bytes::from(data);
let _ = decode_integer(&mut src, 5);
}
}
#[test]
fn stress_test_huffman_random_bytes() {
for seed in 0..2000u32 {
let len = ((seed % 200) + 1) as usize;
let mut data = Vec::with_capacity(len);
for i in 0..len {
data.push(((seed.wrapping_mul(97).wrapping_add(i as u32)) & 0xff) as u8);
}
let _ = decode_huffman(&Bytes::from(data));
}
}
#[test]
fn stress_test_dynamic_table_churn() {
let mut table = DynamicTable::new();
for i in 0..5000u32 {
if i % 3 == 0 {
table.set_max_size(0);
} else if i % 3 == 1 {
table.set_max_size(4096);
}
table.insert(Header::new(format!("x-churn-{i}"), format!("value-{i}")));
assert!(table.size() <= 4096);
}
}
#[test]
fn stress_test_decoder_malformed_blocks() {
for seed in 0..1000u32 {
let len = ((seed % 100) + 1) as usize;
let mut data = Vec::with_capacity(len);
for i in 0..len {
data.push(((seed.wrapping_mul(53).wrapping_add(i as u32 * 7)) & 0xff) as u8);
}
let mut decoder = Decoder::new();
let mut src = Bytes::from(data);
let _ = decoder.decode(&mut src);
}
}
#[test]
fn test_huffman_all_single_bytes() {
for byte in 0..=255u8 {
let input = [byte];
let encoded = encode_huffman(&input);
let result = decode_huffman(&Bytes::from(encoded));
if std::str::from_utf8(&input).is_ok() {
let decoded = result.unwrap_or_else(|e| {
panic!("decode failed for valid UTF-8 byte 0x{byte:02x}: {e:?}")
});
assert_eq!(
decoded.as_bytes(),
&input,
"roundtrip failed for byte 0x{byte:02x}"
);
} else {
let _ = result;
}
}
}
#[test]
fn test_huffman_long_code_symbols() {
let mut input = Vec::new();
for b in 0..=31u8 {
input.push(b);
}
let encoded = encode_huffman(&input);
let decoded = decode_huffman(&Bytes::from(encoded)).unwrap();
assert_eq!(decoded.as_bytes(), &input[..]);
}
#[test]
fn test_integer_max_valid_value() {
let value = 1_000_000_usize;
let mut buf = BytesMut::new();
encode_integer(&mut buf, value, 5, 0x00);
let mut src = buf.freeze();
let decoded = decode_integer(&mut src, 5).unwrap();
assert_eq!(decoded, value);
}
#[test]
fn test_integer_all_prefix_sizes() {
for prefix in [5_u8, 6, 7, 8] {
for &value in &[0_usize, 1, 30, 31, 127, 128, 255, 256, 65535] {
let mut buf = BytesMut::new();
encode_integer(&mut buf, value, prefix, 0x00);
let mut src = buf.freeze();
let decoded = decode_integer(&mut src, prefix).unwrap();
assert_eq!(decoded, value, "prefix={prefix}, value={value}");
}
}
}
#[test]
fn test_encoder_emits_size_update_on_wire() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
encoder.set_max_table_size(256);
let headers = vec![Header::new(":method", "GET")];
let mut buf = BytesMut::new();
encoder.encode(&headers, &mut buf);
assert_eq!(
buf[0] & 0xe0,
0x20,
"first byte should be dynamic table size update prefix"
);
let mut decoder = Decoder::new();
decoder.set_allowed_table_size(256);
let mut src = buf.freeze();
let decoded_headers = decoder.decode(&mut src).unwrap();
assert_eq!(decoded_headers.len(), 1);
assert_eq!(decoded_headers[0].name, ":method");
assert_eq!(decoded_headers[0].value, "GET");
}
#[test]
fn test_encoder_size_update_not_repeated() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
encoder.set_max_table_size(256);
let mut buf1 = BytesMut::new();
encoder.encode(&[Header::new(":method", "GET")], &mut buf1);
assert_eq!(buf1[0] & 0xe0, 0x20, "first block should have size update");
let mut buf2 = BytesMut::new();
encoder.encode(&[Header::new(":method", "POST")], &mut buf2);
assert_ne!(
buf2[0] & 0xe0,
0x20,
"second block should not repeat size update"
);
}
#[test]
fn test_encoder_size_update_roundtrip_full() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let headers1 = vec![Header::new("x-test", "value1")];
let mut buf1 = BytesMut::new();
encoder.encode(&headers1, &mut buf1);
let dec1 = decoder.decode(&mut buf1.freeze()).unwrap();
assert_eq!(dec1[0].value, "value1");
encoder.set_max_table_size(128);
decoder.set_allowed_table_size(128);
let headers2 = vec![Header::new("x-test", "value2")];
let mut buf2 = BytesMut::new();
encoder.encode(&headers2, &mut buf2);
let dec2 = decoder.decode(&mut buf2.freeze()).unwrap();
assert_eq!(dec2[0].value, "value2");
}
#[test]
fn test_encoder_emits_min_then_final_size_update_after_shrink_then_grow() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
encoder.set_max_table_size(128);
encoder.set_max_table_size(256);
let headers = vec![Header::new(":method", "GET")];
let mut buf = BytesMut::new();
encoder.encode(&headers, &mut buf);
assert_eq!(
buf[0] & 0xe0,
0x20,
"first instruction should be size update"
);
let mut src = buf.freeze();
let first_update = decode_integer(&mut src, 5).unwrap();
assert_eq!(first_update, 128);
assert_eq!(
src[0] & 0xe0,
0x20,
"second instruction should be size update"
);
let second_update = decode_integer(&mut src, 5).unwrap();
assert_eq!(second_update, 256);
let mut decoder = Decoder::new();
decoder.set_allowed_table_size(256);
let decoded_headers = decoder.decode(&mut src).unwrap();
assert_eq!(decoded_headers, headers);
}
#[test]
fn test_encoder_shrink_to_zero_does_not_reuse_dynamic_entries() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let headers = vec![
Header::new("cache-control", "gzip, deflate"),
Header::new("cache-control", "gzip, deflate"),
];
let mut initial = BytesMut::new();
encoder.encode(&headers, &mut initial);
let decoded_initial = decoder.decode(&mut initial.freeze()).unwrap();
assert_eq!(decoded_initial, headers);
assert!(encoder.dynamic_table_size() > 0);
assert!(decoder.dynamic_table_size() > 0);
encoder.set_max_table_size(0);
decoder.set_allowed_table_size(0);
let mut resized = BytesMut::new();
encoder.encode(&headers, &mut resized);
let decoded_resized = decoder.decode(&mut resized.freeze()).unwrap();
assert_eq!(decoded_resized, headers);
assert_eq!(encoder.dynamic_table_size(), 0);
assert_eq!(decoder.dynamic_table_size(), 0);
}
#[test]
fn test_integer_decode_checked_mul_overflow() {
let mut data = vec![0x1f_u8];
data.extend_from_slice(&[0xff, 0xff, 0xff, 0xff]);
data.push(0x7f); let mut src = Bytes::from(data);
let _ = decode_integer(&mut src, 5);
}
#[test]
fn test_size_update_before_first_header_accepted() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
encode_integer(&mut buf, 2048, 5, 0x20);
buf.put_u8(0x82);
let mut src = buf.freeze();
let headers = decoder.decode(&mut src).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, ":method");
assert_eq!(headers[0].value, "GET");
}
#[test]
fn test_size_update_after_first_header_rejected() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
buf.put_u8(0x82);
encode_integer(&mut buf, 2048, 5, 0x20);
buf.put_u8(0x84);
let mut src = buf.freeze();
let result = decoder.decode(&mut src);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, ErrorCode::CompressionError);
}
#[test]
fn test_multiple_size_updates_then_header_ok() {
let mut decoder = Decoder::new();
let mut buf = BytesMut::new();
encode_integer(&mut buf, 1024, 5, 0x20);
encode_integer(&mut buf, 2048, 5, 0x20);
buf.put_u8(0x82);
let mut src = buf.freeze();
let headers = decoder.decode(&mut src).unwrap();
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, ":method");
}
#[test]
fn test_size_updates_apply_intermediate_eviction_before_dynamic_lookup() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let mut decoder = Decoder::new();
let seed_headers = vec![Header::new("x-evict-me", "value")];
let mut seeded = BytesMut::new();
encoder.encode(&seed_headers, &mut seeded);
let decoded = decoder.decode(&mut seeded.freeze()).unwrap();
assert_eq!(decoded, seed_headers);
assert!(decoder.dynamic_table_size() > 0);
let mut buf = BytesMut::new();
encode_integer(&mut buf, 0, 5, 0x20);
encode_integer(&mut buf, 256, 5, 0x20);
encode_integer(&mut buf, STATIC_TABLE.len() + 1, 7, 0x80);
let mut src = buf.freeze();
assert_compression_error(decoder.decode(&mut src));
assert_eq!(decoder.dynamic_table_size(), 0);
assert_eq!(decoder.dynamic_table_max_size(), 256);
}
#[test]
fn test_string_length_exceeds_maximum() {
let mut buf = BytesMut::new();
encode_integer(&mut buf, 300_000, 7, 0x00);
let mut src = buf.freeze();
let result = decode_string(&mut src);
assert!(result.is_err());
}
#[test]
fn test_string_length_at_maximum_boundary() {
let data = vec![b'x'; MAX_STRING_LENGTH];
let mut buf = BytesMut::new();
encode_integer(&mut buf, MAX_STRING_LENGTH, 7, 0x00);
buf.extend_from_slice(&data);
let mut src = buf.freeze();
let result = decode_string(&mut src);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), MAX_STRING_LENGTH);
}
#[test]
fn header_debug_clone_eq() {
let h = Header::new("content-type", "application/json");
let dbg = format!("{h:?}");
assert!(dbg.contains("content-type"));
assert!(dbg.contains("application/json"));
let h2 = h.clone();
assert_eq!(h, h2);
let h3 = Header::new("accept", "*/*");
assert_ne!(h, h3);
}
#[test]
fn static_index_exact_matches_linear_scan() {
for (i, &(name, value)) in STATIC_TABLE.iter().enumerate() {
let expected = i + 1;
assert_eq!(
find_static(name, value),
Some(expected),
"exact match failed for ({name}, {value}) at index {expected}"
);
}
assert_eq!(find_static("x-custom", "foo"), None);
assert_eq!(find_static(":method", "DELETE"), None);
}
#[test]
fn static_name_index_matches_first_occurrence() {
assert_eq!(find_static_name(":method"), Some(2)); assert_eq!(find_static_name(":path"), Some(4)); assert_eq!(find_static_name(":status"), Some(8)); assert_eq!(find_static_name(":scheme"), Some(6)); assert_eq!(find_static_name("content-type"), Some(31));
assert_eq!(find_static_name("x-nonexistent"), None);
}
#[test]
fn test_rfc7541_c2_1_literal_incremental_new_name() {
let mut decoder = Decoder::new();
let encoded = &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
];
let mut bytes = Bytes::copy_from_slice(encoded);
let headers = decoder
.decode(&mut bytes)
.expect("C.2.1 decode should work");
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, "custom-key");
assert_eq!(headers[0].value, "custom-header");
}
#[test]
fn test_rfc7541_c6_indexed_header_field() {
let mut decoder = Decoder::new();
let encoded = &[0x82];
let mut bytes = Bytes::copy_from_slice(encoded);
let headers = decoder.decode(&mut bytes).expect("C.6 decode should work");
assert_eq!(headers.len(), 1);
assert_eq!(headers[0].name, ":method");
assert_eq!(headers[0].value, "GET");
}
#[test]
fn test_rfc7541_c3_multiple_requests() {
let mut decoder = Decoder::new();
let encoded_1 = &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
];
let mut bytes = Bytes::copy_from_slice(encoded_1);
let headers_1 = decoder
.decode(&mut bytes)
.expect("C.3.1 decode should work");
assert_eq!(headers_1.len(), 1);
assert_eq!(headers_1[0].name, "custom-key");
assert_eq!(headers_1[0].value, "custom-header");
let encoded_2 = &[0xbe];
let mut bytes = Bytes::copy_from_slice(encoded_2);
let headers_2 = decoder
.decode(&mut bytes)
.expect("C.3.2 decode should work");
assert_eq!(headers_2.len(), 1);
assert_eq!(headers_2[0].name, "custom-key");
assert_eq!(headers_2[0].value, "custom-header");
}
#[test]
fn test_rfc7541_c4_request_sequence() {
let mut decoder = Decoder::new();
let encoded_1 = &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
];
let mut bytes = Bytes::copy_from_slice(encoded_1);
let headers_1 = decoder
.decode(&mut bytes)
.expect("C.4.1 decode should work");
assert_eq!(headers_1.len(), 1);
assert_eq!(headers_1[0].name, "custom-key");
assert_eq!(headers_1[0].value, "custom-header");
let encoded_2 = &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0c, 0x63,
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x76, 0x61, 0x6c, 0x75, 0x65,
];
let mut bytes = Bytes::copy_from_slice(encoded_2);
let headers_2 = decoder
.decode(&mut bytes)
.expect("C.4.2 decode should work");
assert_eq!(headers_2.len(), 1);
assert_eq!(headers_2[0].name, "custom-key");
assert_eq!(headers_2[0].value, "custom-value");
let encoded_3 = &[0xbf, 0xbe];
let mut bytes = Bytes::copy_from_slice(encoded_3);
let headers_3 = decoder
.decode(&mut bytes)
.expect("C.4.3 decode should work");
assert_eq!(headers_3.len(), 2);
assert_eq!(headers_3[0].name, "custom-key");
assert_eq!(headers_3[0].value, "custom-header");
assert_eq!(headers_3[1].name, "custom-key");
assert_eq!(headers_3[1].value, "custom-value");
}
#[test]
fn test_rfc7541_round_trip_basic() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let headers = vec![
Header::new(":method", "GET"),
Header::new(":path", "/"),
Header::new("custom-key", "custom-value"),
];
let mut encoded = BytesMut::new();
encoder.encode(&headers, &mut encoded);
let mut src = encoded.freeze();
let decoded = decoder
.decode(&mut src)
.expect("Round-trip decode should work");
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].name, ":method");
assert_eq!(decoded[0].value, "GET");
assert_eq!(decoded[1].name, ":path");
assert_eq!(decoded[1].value, "/");
assert_eq!(decoded[2].name, "custom-key");
assert_eq!(decoded[2].value, "custom-value");
}
#[test]
fn conformance_static_table_all_61_entries_encode_correctly() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
for (i, &(name, value)) in STATIC_TABLE.iter().enumerate() {
let expected_index = i + 1;
let header = Header::new(name, value);
let mut encoded = BytesMut::new();
encoder.encode(std::slice::from_ref(&header), &mut encoded);
let expected_encoded = 0x80 | expected_index;
if expected_index <= 127 {
assert_eq!(
encoded[0], expected_encoded as u8,
"Static table entry {} ({}, {}) should encode as indexed 0x{:02x}",
expected_index, name, value, expected_encoded
);
} else {
assert_eq!(
encoded[0], 0xff,
"Static table entry {} should start with 0xff for multi-byte encoding",
expected_index
);
}
let mut src = encoded.freeze();
let decoded = decoder.decode(&mut src).unwrap_or_else(|_| {
panic!("Failed to decode static table entry {}", expected_index)
});
assert_eq!(decoded.len(), 1);
assert_eq!(decoded[0].name, name);
assert_eq!(decoded[0].value, value);
}
}
#[test]
fn conformance_referenced_literal_vs_indexed_encoding() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let exact_match = Header::new(":method", "GET"); let mut encoded = BytesMut::new();
encoder.encode(&[exact_match], &mut encoded);
assert_eq!(
encoded[0], 0x82,
"Exact static table match should use indexed encoding"
);
let name_match = Header::new(":method", "DELETE"); let mut encoded = BytesMut::new();
encoder.encode(&[name_match], &mut encoded);
assert_eq!(
encoded[0], 0x42,
"Static table name match with different value should use literal with name reference"
);
let mut src = encoded.freeze();
let _ = src.split_to(1);
let value_str = decode_string(&mut src).expect("Should decode DELETE value");
assert_eq!(value_str, "DELETE");
let new_header = Header::new("x-custom", "test-value");
let mut encoded = BytesMut::new();
encoder.encode(&[new_header], &mut encoded);
assert_eq!(
encoded[0], 0x40,
"New header should use literal without name reference"
);
let mut src = encoded.freeze();
let decoded = decoder
.decode(&mut src)
.expect("Should decode custom header");
assert_eq!(decoded[0].name, "x-custom");
assert_eq!(decoded[0].value, "test-value");
}
#[test]
fn conformance_case_insensitive_name_matching() {
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let test_cases = vec![
("Content-Type", ""), ("CONTENT-TYPE", ""), ("content-type", ""), ("Content-Length", ""), ("ACCEPT", ""), ];
for (mixed_case_name, value) in test_cases {
let header = Header::new(mixed_case_name, value);
let mut encoded = BytesMut::new();
let normalized_name = mixed_case_name.to_lowercase();
let expected_index = find_static_name(&normalized_name)
.unwrap_or_else(|| panic!("Should find static index for {}", normalized_name));
encoder.encode(&[header], &mut encoded);
let expected_encoded = 0x80 | expected_index;
assert_eq!(
encoded[0], expected_encoded as u8,
"Case-insensitive name {} should match static table index {}",
mixed_case_name, expected_index
);
}
}
#[test]
fn conformance_dynamic_table_eviction_static_first() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
encoder.set_max_table_size(256); decoder.set_allowed_table_size(256);
let headers_1 = vec![
Header::new("x-custom-1", "value-1"), Header::new("x-custom-2", "value-2"), Header::new("x-custom-3", "value-3"), Header::new("x-custom-4", "value-4"), ];
let mut encoded = BytesMut::new();
encoder.encode(&headers_1, &mut encoded);
let mut src = encoded.freeze();
decoder.decode(&mut src).expect("Should decode first batch");
let evicting_header = Header::new("x-custom-5", "value-5");
let mut encoded = BytesMut::new();
encoder.encode(std::slice::from_ref(&evicting_header), &mut encoded);
let mut src = encoded.freeze();
decoder
.decode(&mut src)
.expect("Should decode evicting header");
let static_header = Header::new(":method", "GET");
let mut encoded = BytesMut::new();
encoder.encode(&[static_header], &mut encoded);
assert_eq!(
encoded[0], 0x82,
"Static table entry should remain accessible after dynamic table eviction"
);
let large_headers = vec![
Header::new("x-very-long-header-name-1", "very-long-value-1"),
Header::new("x-very-long-header-name-2", "very-long-value-2"),
];
let mut encoded = BytesMut::new();
encoder.encode(&large_headers, &mut encoded);
let mut src = encoded.freeze();
let decoded = decoder
.decode(&mut src)
.expect("Should handle dynamic table eviction properly");
assert_eq!(decoded.len(), 2);
}
#[test]
fn conformance_never_indexed_header_preservation() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let sensitive_headers = vec![
Header::new("authorization", "Bearer secret-token"),
Header::new("cookie", "sessionid=secret123"),
Header::new("x-api-key", "secret-api-key"),
];
let mut encoded = BytesMut::new();
encoder.encode_sensitive(&sensitive_headers, &mut encoded);
let mut src = encoded.clone().freeze();
let decoded = decoder
.decode(&mut src)
.expect("Should decode never-indexed headers");
assert_eq!(decoded.len(), 3);
assert_eq!(decoded[0].name, "authorization");
assert_eq!(decoded[0].value, "Bearer secret-token");
let mut encoded_again = BytesMut::new();
encoder.encode_sensitive(&sensitive_headers, &mut encoded_again);
for &byte in &encoded_again[0..3] {
let is_never_indexed = (byte & 0xF0) == 0x10;
let is_literal_no_index = (byte & 0xF0) == 0x00;
assert!(
is_never_indexed || is_literal_no_index,
"Never-indexed header should not use indexed representation, got 0x{:02x}",
byte
);
}
let mut src = encoded_again.freeze();
let decoded_again = decoder
.decode(&mut src)
.expect("Should decode never-indexed headers again");
assert_eq!(
decoded_again, decoded,
"Never-indexed headers should decode consistently"
);
let mut encoded_regular = BytesMut::new();
encoder.encode(&sensitive_headers[0..1], &mut encoded_regular);
let first_byte = encoded_regular[0];
let is_indexed = (first_byte & 0x80) != 0;
assert!(
!is_indexed,
"Never-indexed header should not be found in dynamic table on subsequent encode"
);
}
#[test]
fn sensitive_exact_static_match_never_uses_indexed_representation() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let header = Header::new(":method", "GET");
let mut encoded = BytesMut::new();
encoder.encode_sensitive(std::slice::from_ref(&header), &mut encoded);
assert_eq!(
encoded[0] & 0xF0,
0x10,
"sensitive headers must use the RFC 7541 never-indexed wire form, not indexed lookup"
);
assert_eq!(encoder.dynamic_table_size(), 0);
let mut src = encoded.freeze();
let decoded = decoder.decode(&mut src).expect("decode sensitive header");
assert_eq!(decoded, vec![header]);
assert_eq!(decoder.dynamic_table_size(), 0);
}
#[test]
fn sensitive_exact_dynamic_match_never_uses_indexed_representation() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
encoder.set_use_huffman(false);
let header = Header::new("authorization", "Bearer secret-token");
let mut indexed = BytesMut::new();
encoder.encode(std::slice::from_ref(&header), &mut indexed);
let mut indexed_src = indexed.freeze();
let decoded = decoder
.decode(&mut indexed_src)
.expect("decode indexed header");
assert_eq!(decoded, vec![header.clone()]);
let encoder_table_before = encoder.dynamic_table_size();
let decoder_table_before = decoder.dynamic_table_size();
let mut sensitive = BytesMut::new();
encoder.encode_sensitive(std::slice::from_ref(&header), &mut sensitive);
assert_eq!(
sensitive[0] & 0xF0,
0x10,
"sensitive exact matches must not collapse to indexed dynamic-table lookups"
);
let mut sensitive_src = sensitive.freeze();
let decoded_sensitive = decoder
.decode(&mut sensitive_src)
.expect("decode sensitive header");
assert_eq!(decoded_sensitive, vec![header]);
assert_eq!(encoder.dynamic_table_size(), encoder_table_before);
assert_eq!(decoder.dynamic_table_size(), decoder_table_before);
}
#[test]
fn conformance_static_table_bounds_checking() {
assert!(get_static(0).is_none(), "Index 0 should be invalid");
assert!(
get_static(1).is_some(),
"Index 1 should be valid (:authority)"
);
assert!(
get_static(61).is_some(),
"Index 61 should be valid (www-authenticate)"
);
assert!(
get_static(62).is_none(),
"Index 62 should be invalid (beyond static table)"
);
assert!(get_static(1000).is_none(), "Large index should be invalid");
assert_eq!(get_static(1).unwrap(), (":authority", ""));
assert_eq!(get_static(2).unwrap(), (":method", "GET"));
assert_eq!(get_static(61).unwrap(), ("www-authenticate", ""));
}
#[test]
fn conformance_static_table_completeness() {
assert_eq!(
STATIC_TABLE.len(),
61,
"Static table must have exactly 61 entries per RFC 7541 Appendix A"
);
assert_eq!(STATIC_TABLE[0], (":authority", "")); assert_eq!(STATIC_TABLE[1], (":method", "GET")); assert_eq!(STATIC_TABLE[2], (":method", "POST")); assert_eq!(STATIC_TABLE[3], (":path", "/")); assert_eq!(STATIC_TABLE[4], (":path", "/index.html")); assert_eq!(STATIC_TABLE[5], (":scheme", "http")); assert_eq!(STATIC_TABLE[6], (":scheme", "https")); assert_eq!(STATIC_TABLE[7], (":status", "200")); assert_eq!(STATIC_TABLE[60], ("www-authenticate", ""));
assert_eq!(
STATIC_EXACT_INDEX.len(),
61,
"Exact index should have 61 entries"
);
assert_eq!(find_static(":method", "GET"), Some(2));
assert_eq!(find_static(":method", "POST"), Some(3));
assert_eq!(find_static(":status", "200"), Some(8));
assert_eq!(find_static("content-type", ""), Some(31));
assert_eq!(find_static_name(":method"), Some(2)); assert_eq!(find_static_name(":path"), Some(4)); assert_eq!(find_static_name(":status"), Some(8)); }
#[test]
fn hpack_static_table_encoding_decoding_stability() {
use insta::assert_debug_snapshot;
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
let test_headers = vec![
Header::new(":method", "GET"), Header::new(":method", "POST"), Header::new(":path", "/"), Header::new(":path", "/index.html"), Header::new(":scheme", "https"), Header::new(":status", "200"), Header::new(":status", "404"), Header::new("accept-encoding", "gzip, deflate"), Header::new("cache-control", ""), Header::new("content-type", ""), Header::new("host", ""), Header::new("user-agent", "custom"), ];
let mut encoded = BytesMut::new();
encoder.encode(&test_headers, &mut encoded);
assert_debug_snapshot!("hpack_static_table_encoded", encoded.as_ref());
let mut encoded_bytes = encoded.freeze();
let decoded_headers = decoder
.decode(&mut encoded_bytes)
.expect("decode should succeed");
assert_debug_snapshot!("hpack_static_table_decoded", decoded_headers);
assert_eq!(test_headers, decoded_headers);
}
#[test]
fn conformance_rfc7541_b3_response_encoding() {
let headers = vec![
Header::new(":status", "302"),
Header::new("cache-control", "private"),
Header::new("date", "Mon, 21 Oct 2013 20:13:21 GMT"),
Header::new("location", "https://www.example.com"),
];
let expected_rfc_wire: &[u8] = &[
0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20,
0x32, 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x31, 0x20,
0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77,
0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
];
let mut encoder = Encoder::new();
encoder.set_use_huffman(false);
let mut encoded = BytesMut::new();
encoder.encode(&headers, &mut encoded);
assert_eq!(
encoded.as_ref(),
expected_rfc_wire,
"CONFORMANCE FAILURE: RFC 7541 §B.3 encoder output diverges from specification\n\
Expected (RFC): {:02x?}\n\
Actual (ours): {:02x?}\n\
\n\
This is a MUST-level requirement for HPACK conformance.\n\
Our encoder must produce identical byte sequences to ensure\n\
interoperability with other HPACK implementations.",
expected_rfc_wire,
encoded.as_ref()
);
let mut decoder = Decoder::new();
let mut encoded_bytes = encoded.freeze();
let decoded = decoder
.decode(&mut encoded_bytes)
.expect("RFC 7541 §B.3 conformance: decoder must parse encoder output");
assert_eq!(
decoded, headers,
"RFC 7541 §B.3 round-trip conformance: decoded headers must match original"
);
assert_eq!(
decoded.len(),
4,
"RFC 7541 §B.3: must encode exactly 4 headers"
);
assert_eq!(
decoded[0].name, ":status",
"RFC 7541 §B.3: first header must be :status"
);
assert_eq!(
decoded[0].value, "302",
"RFC 7541 §B.3: status value must be 302"
);
}
#[derive(Debug, Clone)]
struct Rfc7541TestCase {
id: &'static str,
description: &'static str,
headers: Vec<Header>,
expected_encoding: &'static [u8],
requirement_level: &'static str, }
fn get_rfc7541_test_cases() -> Vec<Rfc7541TestCase> {
vec![
Rfc7541TestCase {
id: "RFC7541-C.2.1",
description: "Literal Header Field with Incremental Indexing — New Name",
headers: vec![Header {
name: "custom-key".to_string(),
value: "custom-header".to_string(),
}],
expected_encoding: &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d,
0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
],
requirement_level: "MUST",
},
Rfc7541TestCase {
id: "RFC7541-C.2.2",
description: "Literal Header Field with Incremental Indexing — Indexed Name",
headers: vec![Header {
name: ":path".to_string(),
value: "/sample/path".to_string(),
}],
expected_encoding: &[
0x04, 0x0c, 0x2f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x70, 0x61, 0x74,
0x68,
],
requirement_level: "MUST",
},
Rfc7541TestCase {
id: "RFC7541-C.3.1",
description: "Dynamic Table Size Update",
headers: vec![],
expected_encoding: &[0x20], requirement_level: "MUST",
},
Rfc7541TestCase {
id: "RFC7541-C.4.1",
description: "Literal Header Field without Indexing — New Name",
headers: vec![Header {
name: "custom-key".to_string(),
value: "custom-header".to_string(),
}],
expected_encoding: &[
0x00, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d,
0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
],
requirement_level: "MUST",
},
]
}
#[test]
fn hpack_rfc7541_appendix_c_conformance() {
let all_test_cases = get_rfc7541_test_cases();
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
let mut pass_count = 0;
let mut fail_count = 0;
for test_case in all_test_cases {
let mut output = BytesMut::new();
if test_case.headers.is_empty() {
if test_case.id == "RFC7541-C.3.1" {
encoder.set_max_table_size(0);
encoder.encode(&[], &mut output);
}
} else {
encoder.encode(&test_case.headers, &mut output);
}
let encoded = output.freeze();
let exact_encoding_match = encoded.as_ref() == test_case.expected_encoding;
if !test_case.headers.is_empty() {
let mut decoder_bytes = encoded.clone();
match decoder.decode(&mut decoder_bytes) {
Ok(decoded_headers) => {
if decoded_headers == test_case.headers {
pass_count += 1;
eprintln!(
"✓ {} [{}]: {} (exact_encoding_match={})",
test_case.id,
test_case.requirement_level,
test_case.description,
exact_encoding_match
);
} else {
fail_count += 1;
eprintln!(
"✗ {} [{}]: Header mismatch\n Expected: {:?}\n Got: {:?}\n Exact encoding match: {}",
test_case.id,
test_case.requirement_level,
test_case.headers,
decoded_headers,
exact_encoding_match
);
}
}
Err(e) => {
fail_count += 1;
eprintln!(
"✗ {} [{}]: Decode error: {}",
test_case.id, test_case.requirement_level, e
);
}
}
} else {
pass_count += 1;
eprintln!(
"✓ {} [{}]: {} (exact_encoding_match={})",
test_case.id,
test_case.requirement_level,
test_case.description,
exact_encoding_match
);
}
}
eprintln!(
"RFC 7541 Conformance: {}/{} tests passed",
pass_count,
pass_count + fail_count
);
assert_eq!(
fail_count, 0,
"{} RFC 7541 conformance tests failed",
fail_count
);
}
#[test]
fn hpack_dynamic_table_eviction_conformance() {
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
let small_table_size = 1024;
encoder.set_max_table_size(small_table_size);
decoder.set_allowed_table_size(small_table_size);
let large_headers = (0..10)
.map(|i| Header {
name: format!("x-large-header-{i}"),
value: "x".repeat(200), })
.collect::<Vec<_>>();
let mut all_encoded = Vec::new();
for chunk in large_headers.chunks(2) {
let mut output = BytesMut::new();
encoder.encode(chunk, &mut output);
all_encoded.push(output.freeze());
}
for (i, encoded_chunk) in all_encoded.iter().enumerate() {
let mut bytes = encoded_chunk.clone();
match decoder.decode(&mut bytes) {
Ok(decoded) => {
let expected_chunk =
&large_headers[i * 2..(i * 2 + 2).min(large_headers.len())];
assert_eq!(
decoded, expected_chunk,
"Dynamic table eviction test failed at chunk {i}"
);
}
Err(e) => panic!("Dynamic table eviction decode failed at chunk {i}: {e}"),
}
}
eprintln!("✓ Dynamic table eviction conformance test passed");
}
#[test]
fn hpack_huffman_encoding_round_trip_conformance() {
let test_strings = vec![
"", "a", "GET", "/very/long/path/with/many/segments", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Mozilla/5.0 (compatible; asupersync/0.3.1)", "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9", ];
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
for (i, test_value) in test_strings.iter().enumerate() {
let headers = vec![Header {
name: format!("x-test-{i}"),
value: test_value.to_string(),
}];
let mut output = BytesMut::new();
encoder.encode(&headers, &mut output);
let encoded = output.freeze();
let mut bytes = encoded;
let decoded = decoder
.decode(&mut bytes)
.expect("Huffman encoding round-trip must succeed");
assert_eq!(
decoded, headers,
"Huffman encoding round-trip failed for string: {:?}",
test_value
);
}
eprintln!("✓ Huffman encoding round-trip conformance test passed");
}
#[test]
fn hpack_static_table_conformance() {
let static_table_entries = vec![
(":authority", ""),
(":method", "GET"),
(":method", "POST"),
(":path", "/"),
(":path", "/index.html"),
(":scheme", "http"),
(":scheme", "https"),
(":status", "200"),
(":status", "204"),
(":status", "206"),
(":status", "304"),
(":status", "400"),
(":status", "404"),
(":status", "500"),
("accept-charset", ""),
("accept-encoding", "gzip, deflate"),
("accept-language", ""),
("accept-ranges", ""),
("accept", ""),
("access-control-allow-origin", ""),
("age", ""),
("allow", ""),
("authorization", ""),
("cache-control", ""),
("content-disposition", ""),
("content-encoding", ""),
("content-language", ""),
("content-length", ""),
("content-location", ""),
("content-range", ""),
("content-type", ""),
("cookie", ""),
("date", ""),
("etag", ""),
("expect", ""),
("expires", ""),
("from", ""),
("host", ""),
("if-match", ""),
("if-modified-since", ""),
("if-none-match", ""),
("if-range", ""),
("if-unmodified-since", ""),
("last-modified", ""),
("link", ""),
("location", ""),
("max-forwards", ""),
("proxy-authenticate", ""),
("proxy-authorization", ""),
("range", ""),
("referer", ""),
("refresh", ""),
("retry-after", ""),
("server", ""),
("set-cookie", ""),
("strict-transport-security", ""),
("transfer-encoding", ""),
("user-agent", ""),
("vary", ""),
("via", ""),
("www-authenticate", ""),
];
let mut encoder = Encoder::new();
let mut decoder = Decoder::new();
for (name, value) in static_table_entries {
let headers = vec![Header {
name: name.to_string(),
value: value.to_string(),
}];
let mut output = BytesMut::new();
encoder.encode(&headers, &mut output);
let encoded = output.freeze();
let mut bytes = encoded;
let decoded = decoder
.decode(&mut bytes)
.expect("Static table entry encoding must succeed");
assert_eq!(
decoded, headers,
"Static table conformance failed for entry: {}:{}",
name, value
);
}
eprintln!("✓ Static table conformance test passed");
}
#[test]
fn hpack_full_conformance_suite() {
hpack_rfc7541_appendix_c_conformance();
hpack_dynamic_table_eviction_conformance();
hpack_huffman_encoding_round_trip_conformance();
hpack_static_table_conformance();
eprintln!("✓ All HPACK RFC 7541 conformance tests passed");
}
}