pub mod dynamic_table;
pub mod huffman;
pub mod integer;
pub mod static_table;
pub mod string;
use super::error::H2ParseError;
use dynamic_table::DynamicTable;
use integer::decode_integer;
use static_table::{STATIC_INDEX_AUTHORITY, STATIC_TABLE_MAX};
use string::decode_string;
pub const DEFAULT_HEADER_TABLE_SIZE: usize = 4096;
pub struct HpackDecoder {
dynamic_table: DynamicTable,
}
impl Default for HpackDecoder {
fn default() -> Self {
Self::new()
}
}
impl HpackDecoder {
pub fn new() -> Self {
Self {
dynamic_table: DynamicTable::new(DEFAULT_HEADER_TABLE_SIZE)
.expect("default table size is within bound"),
}
}
#[allow(dead_code)] pub fn reset(&mut self) {
self.dynamic_table = DynamicTable::new(DEFAULT_HEADER_TABLE_SIZE)
.expect("default table size is within bound");
}
pub fn decode_block(&mut self, block: &[u8]) -> Result<Option<DecodedAuthority>, H2ParseError> {
let mut cursor = block;
let mut found: Option<DecodedAuthority> = None;
while !cursor.is_empty() {
let first = cursor[0];
if first & 0b1000_0000 != 0 {
let (index, rest) = decode_integer(cursor, 7)?;
cursor = rest;
let (name, value) = self
.dynamic_table
.lookup(index)
.ok_or(H2ParseError::HpackInvalidIndex { index })?;
if found.is_none() && eq_authority(name.as_bytes()) && !value.is_empty() {
let provenance = if index <= STATIC_TABLE_MAX {
AuthorityProvenance::StaticIndexed
} else {
AuthorityProvenance::DynamicIndexed
};
found = Some(DecodedAuthority {
value: normalise_authority(value.as_bytes()),
provenance,
});
}
continue;
}
if first & 0b1100_0000 == 0b0100_0000 {
let (name, value, name_index, value_was_huffman, after) =
self.parse_literal(cursor, 6)?;
cursor = after;
let is_authority = if name_index == 0 {
eq_authority(name.as_bytes())
} else {
name_index == STATIC_INDEX_AUTHORITY
|| self
.dynamic_table
.lookup(name_index)
.map(|(n, _)| eq_authority(n.as_bytes()))
.unwrap_or(false)
};
if found.is_none() && is_authority && !value.is_empty() {
let provenance = match (name_index, value_was_huffman) {
(_, true) => AuthorityProvenance::Huffman,
(0, false) => AuthorityProvenance::StaticLiteral,
(i, false) if i <= STATIC_TABLE_MAX => AuthorityProvenance::StaticLiteral,
(_, false) => AuthorityProvenance::DynamicIndexed,
};
found = Some(DecodedAuthority {
value: normalise_authority(value.as_bytes()),
provenance,
});
}
let stored_name = if name_index == 0 {
name
} else {
self.dynamic_table
.lookup(name_index)
.ok_or(H2ParseError::HpackInvalidIndex { index: name_index })?
.0
.to_string()
};
self.dynamic_table.insert(stored_name, value);
continue;
}
if first & 0b1111_0000 == 0b0000_0000 || first & 0b1111_0000 == 0b0001_0000 {
let (name, value, name_index, value_was_huffman, after) =
self.parse_literal(cursor, 4)?;
cursor = after;
let is_authority = if name_index == 0 {
eq_authority(name.as_bytes())
} else {
name_index == STATIC_INDEX_AUTHORITY
|| self
.dynamic_table
.lookup(name_index)
.map(|(n, _)| eq_authority(n.as_bytes()))
.unwrap_or(false)
};
if found.is_none() && is_authority && !value.is_empty() {
let provenance = if value_was_huffman {
AuthorityProvenance::Huffman
} else if name_index == 0 || name_index <= STATIC_TABLE_MAX {
AuthorityProvenance::StaticLiteral
} else {
AuthorityProvenance::DynamicIndexed
};
found = Some(DecodedAuthority {
value: normalise_authority(value.as_bytes()),
provenance,
});
}
continue;
}
if first & 0b1110_0000 == 0b0010_0000 {
let (new_max, rest) = decode_integer(cursor, 5)?;
cursor = rest;
let new_max_usize =
usize::try_from(new_max).map_err(|_| H2ParseError::MalformedHeaders)?;
self.dynamic_table.update_max_size(new_max_usize)?;
continue;
}
return Err(H2ParseError::MalformedHeaders);
}
Ok(found)
}
fn parse_literal<'a>(
&mut self,
buf: &'a [u8],
prefix_bits: u32,
) -> Result<(String, String, u64, bool, &'a [u8]), H2ParseError> {
let (name_index, rest) = decode_integer(buf, prefix_bits)?;
let (name, after_name) = if name_index == 0 {
let (n, r) = decode_string(rest)?;
(n, r)
} else {
(String::new(), rest)
};
let value_was_huffman = !after_name.is_empty() && (after_name[0] & 0x80) != 0;
let (value, after_value) = decode_string(after_name)?;
Ok((name, value, name_index, value_was_huffman, after_value))
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AuthorityProvenance {
StaticIndexed,
StaticLiteral,
DynamicIndexed,
Huffman,
}
#[derive(Debug, Clone)]
pub struct DecodedAuthority {
pub value: String,
pub provenance: AuthorityProvenance,
}
fn eq_authority(name: &[u8]) -> bool {
if name.len() != b":authority".len() {
return false;
}
name.iter()
.zip(b":authority".iter())
.all(|(a, b)| a.eq_ignore_ascii_case(b))
}
pub(crate) fn normalise_authority(raw: &[u8]) -> String {
let trimmed = trim_ascii(raw);
let host = if trimmed.first() == Some(&b'[') {
if let Some(close) = trimmed.iter().position(|&b| b == b']') {
&trimmed[..=close]
} else {
trimmed
}
} else if let Some(colon) = trimmed.iter().position(|&b| b == b':') {
&trimmed[..colon]
} else {
trimmed
};
let mut s = String::from_utf8_lossy(host).to_string();
s.make_ascii_lowercase();
if s.ends_with('.') {
s.pop();
}
s
}
fn trim_ascii(s: &[u8]) -> &[u8] {
let mut start = 0;
while start < s.len() && (s[start] == b' ' || s[start] == b'\t') {
start += 1;
}
let mut end = s.len();
while end > start && (s[end - 1] == b' ' || s[end - 1] == b'\t') {
end -= 1;
}
&s[start..end]
}
#[cfg(test)]
mod tests {
use super::*;
fn lit_indexed_name(name_index: u8, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x40 | (name_index & 0x3F));
out.push(value.len() as u8); out.extend_from_slice(value.as_bytes());
out
}
fn lit_indexed_name_huffman(name_index: u8, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x40 | (name_index & 0x3F));
let payload = huffman::encode(value);
assert!(payload.len() < 0x7F);
out.push(0x80 | payload.len() as u8);
out.extend_from_slice(&payload);
out
}
#[test]
fn extracts_authority_via_static_literal() {
let mut d = HpackDecoder::new();
let block = lit_indexed_name(1, "api.example.com");
let result = d.decode_block(&block).unwrap().unwrap();
assert_eq!(result.value, "api.example.com");
assert_eq!(result.provenance, AuthorityProvenance::StaticLiteral);
assert_eq!(d.dynamic_table.entry_count(), 1);
}
#[test]
fn extracts_authority_via_huffman_literal() {
let mut d = HpackDecoder::new();
let block = lit_indexed_name_huffman(1, "api.example.com");
let result = d.decode_block(&block).unwrap().unwrap();
assert_eq!(result.value, "api.example.com");
assert_eq!(result.provenance, AuthorityProvenance::Huffman);
}
#[test]
fn extracts_authority_via_dynamic_table_reference() {
let mut d = HpackDecoder::new();
let block1 = lit_indexed_name(1, "api.example.com");
let r1 = d.decode_block(&block1).unwrap().unwrap();
assert_eq!(r1.value, "api.example.com");
let block2 = vec![0x80 | 62];
let r2 = d.decode_block(&block2).unwrap().unwrap();
assert_eq!(r2.value, "api.example.com");
assert_eq!(r2.provenance, AuthorityProvenance::DynamicIndexed);
}
#[test]
fn rejects_invalid_dynamic_index() {
let mut d = HpackDecoder::new();
let block = vec![0x80 | 62];
let err = d.decode_block(&block).unwrap_err();
assert!(matches!(err, H2ParseError::HpackInvalidIndex { .. }));
}
#[test]
fn dynamic_table_size_update_is_honoured() {
let mut d = HpackDecoder::new();
let block = vec![0x20]; d.decode_block(&block).unwrap();
assert_eq!(d.dynamic_table.max_size(), 0);
}
#[test]
fn block_with_no_authority_returns_none() {
let mut d = HpackDecoder::new();
let block = vec![0x80 | 2];
let result = d.decode_block(&block).unwrap();
assert!(result.is_none());
}
#[test]
fn literal_with_incremental_indexing_persists_across_blocks() {
let mut d = HpackDecoder::new();
let mut block1 = Vec::new();
block1.push(0x40); block1.push(8);
block1.extend_from_slice(b"x-custom");
block1.push(5);
block1.extend_from_slice(b"value");
d.decode_block(&block1).unwrap();
assert_eq!(d.dynamic_table.entry_count(), 1);
let (name, value) = d.dynamic_table.lookup(62).unwrap();
assert_eq!(name, "x-custom");
assert_eq!(value, "value");
}
}