pub mod error;
pub mod frame;
pub mod hpack;
pub use error::{H2ParseError, ReassemblerOverflowKind};
#[allow(unused_imports)]
pub use frame::{FrameHeader, DEFAULT_MAX_FRAME_SIZE};
#[allow(unused_imports)]
pub use hpack::{AuthorityProvenance, DecodedAuthority, HpackDecoder};
use frame::{
parse_one_frame, strip_headers_padding_and_priority, FLAG_END_HEADERS, FRAME_TYPE_CONTINUATION,
FRAME_TYPE_HEADERS, FRAME_TYPE_SETTINGS,
};
use std::collections::HashMap;
pub const HTTP2_PREFACE: &[u8] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
pub const MAX_HEADER_BLOCK_SIZE: usize = 64 * 1024;
pub fn is_h2c_preface(bytes: &[u8]) -> bool {
bytes.len() >= HTTP2_PREFACE.len() && &bytes[..HTTP2_PREFACE.len()] == HTTP2_PREFACE
}
pub type ReassembledBlock<'a> = (Vec<u8>, &'a [u8]);
pub fn reassemble_header_block(buf: &[u8]) -> Result<Option<ReassembledBlock<'_>>, H2ParseError> {
let mut cursor = buf;
let mut accumulated: Vec<u8> = Vec::new();
let mut first_frame = true;
loop {
let (header, payload, rest) = match parse_one_frame(cursor)? {
Some(p) => p,
None => return Ok(None),
};
if first_frame {
if header.frame_type != FRAME_TYPE_HEADERS {
return Err(H2ParseError::UnexpectedFirstFrame {
frame_type: header.frame_type,
});
}
let block = strip_headers_padding_and_priority(payload, header.flags)?;
if accumulated.len() + block.len() > MAX_HEADER_BLOCK_SIZE {
return Err(H2ParseError::HpackOversizedHeaderBlock {
total: accumulated.len() + block.len(),
max: MAX_HEADER_BLOCK_SIZE,
});
}
accumulated.extend_from_slice(block);
cursor = rest;
if header.flags & FLAG_END_HEADERS != 0 {
return Ok(Some((accumulated, cursor)));
}
first_frame = false;
} else {
if header.frame_type != FRAME_TYPE_CONTINUATION {
return Err(H2ParseError::InterleavedFrame {
frame_type: header.frame_type,
});
}
if accumulated.len() + payload.len() > MAX_HEADER_BLOCK_SIZE {
return Err(H2ParseError::HpackOversizedHeaderBlock {
total: accumulated.len() + payload.len(),
max: MAX_HEADER_BLOCK_SIZE,
});
}
accumulated.extend_from_slice(payload);
cursor = rest;
if header.flags & FLAG_END_HEADERS != 0 {
return Ok(Some((accumulated, cursor)));
}
}
}
}
pub fn extract_h2_authority(after_preface: &[u8]) -> Result<Option<String>, H2ParseError> {
extract_h2_authority_with(after_preface, &mut HpackDecoder::new())
.map(|opt| opt.map(|d| d.value))
}
pub fn extract_h2_authority_with(
after_preface: &[u8],
decoder: &mut HpackDecoder,
) -> Result<Option<DecodedAuthority>, H2ParseError> {
let mut cursor = after_preface;
if let Some((header, _payload, rest)) = parse_one_frame(cursor)? {
if header.frame_type == FRAME_TYPE_SETTINGS {
cursor = rest;
}
} else {
return Ok(None);
}
let (block, _rest_after) = match reassemble_header_block(cursor)? {
Some(p) => p,
None => return Ok(None),
};
decoder.decode_block(&block)
}
pub const REASSEMBLER_TOTAL_IN_FLIGHT_MAX: usize = 256 * 1024;
pub const REASSEMBLER_MAX_CONCURRENT_STREAMS: usize = 64;
pub struct H2StreamReassembler {
blocks: HashMap<u32, Vec<u8>>,
active_stream: Option<u32>,
total_in_flight: usize,
}
impl Default for H2StreamReassembler {
fn default() -> Self {
Self::new()
}
}
impl H2StreamReassembler {
pub fn new() -> Self {
Self {
blocks: HashMap::new(),
active_stream: None,
total_in_flight: 0,
}
}
pub fn ingest(
&mut self,
frame: &FrameHeader,
payload: &[u8],
) -> Result<Option<(u32, Vec<u8>)>, H2ParseError> {
if let Some(active) = self.active_stream {
if frame.frame_type == FRAME_TYPE_CONTINUATION {
if frame.stream_id != active {
return Err(H2ParseError::InterleavedFrame {
frame_type: frame.frame_type,
});
}
let entry = self
.blocks
.get_mut(&active)
.expect("active_stream invariant: blocks contains active");
if entry.len() + payload.len() > MAX_HEADER_BLOCK_SIZE {
return Err(H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::PerStreamBlock,
});
}
if self.total_in_flight + payload.len() > REASSEMBLER_TOTAL_IN_FLIGHT_MAX {
return Err(H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::TotalInFlight,
});
}
entry.extend_from_slice(payload);
self.total_in_flight += payload.len();
if frame.flags & FLAG_END_HEADERS != 0 {
let block = self
.blocks
.remove(&active)
.expect("active reassembly entry");
self.total_in_flight = self.total_in_flight.saturating_sub(block.len());
self.active_stream = None;
return Ok(Some((active, block)));
}
return Ok(None);
}
return Err(H2ParseError::InterleavedFrame {
frame_type: frame.frame_type,
});
}
if frame.frame_type == FRAME_TYPE_HEADERS {
if self.blocks.len() >= REASSEMBLER_MAX_CONCURRENT_STREAMS {
return Err(H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::ConcurrentStreams,
});
}
let inner = strip_headers_padding_and_priority(payload, frame.flags)?;
if inner.len() > MAX_HEADER_BLOCK_SIZE {
return Err(H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::PerStreamBlock,
});
}
if self.total_in_flight + inner.len() > REASSEMBLER_TOTAL_IN_FLIGHT_MAX {
return Err(H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::TotalInFlight,
});
}
if frame.flags & FLAG_END_HEADERS != 0 {
return Ok(Some((frame.stream_id, inner.to_vec())));
}
self.blocks.insert(frame.stream_id, inner.to_vec());
self.total_in_flight += inner.len();
self.active_stream = Some(frame.stream_id);
return Ok(None);
}
if frame.frame_type == FRAME_TYPE_CONTINUATION {
return Err(H2ParseError::InterleavedFrame {
frame_type: frame.frame_type,
});
}
Ok(None)
}
#[cfg(test)]
pub fn pending_count(&self) -> usize {
self.blocks.len()
}
}
#[derive(Debug, Clone)]
pub struct H2HeadersDecoded {
pub stream_id: u32,
pub authority: Option<String>,
pub via_dynamic_table: bool,
pub via_huffman: bool,
}
pub struct H2ConnectionDecoder {
hpack: HpackDecoder,
reassembler: H2StreamReassembler,
}
impl Default for H2ConnectionDecoder {
fn default() -> Self {
Self::new()
}
}
impl H2ConnectionDecoder {
pub fn new() -> Self {
Self {
hpack: HpackDecoder::new(),
reassembler: H2StreamReassembler::new(),
}
}
pub fn feed_frame(
&mut self,
frame: &FrameHeader,
payload: &[u8],
) -> Result<Option<H2HeadersDecoded>, H2ParseError> {
let (stream_id, block) = match self.reassembler.ingest(frame, payload)? {
Some(p) => p,
None => return Ok(None),
};
let decoded = self.hpack.decode_block(&block)?;
let (authority, via_dynamic_table, via_huffman) = match decoded {
Some(d) => {
let dyn_indexed = matches!(d.provenance, AuthorityProvenance::DynamicIndexed);
let huff = matches!(d.provenance, AuthorityProvenance::Huffman);
(Some(d.value), dyn_indexed, huff)
}
None => (None, false, false),
};
Ok(Some(H2HeadersDecoded {
stream_id,
authority,
via_dynamic_table,
via_huffman,
}))
}
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::frame::{FRAME_TYPE_CONTINUATION, FRAME_TYPE_HEADERS, FRAME_TYPE_SETTINGS};
use super::hpack::huffman;
pub fn frame_header(length: u32, frame_type: u8, flags: u8, stream_id: u32) -> Vec<u8> {
let mut out = Vec::with_capacity(9);
out.push(((length >> 16) & 0xFF) as u8);
out.push(((length >> 8) & 0xFF) as u8);
out.push((length & 0xFF) as u8);
out.push(frame_type);
out.push(flags);
out.extend_from_slice(&(stream_id & 0x7FFF_FFFF).to_be_bytes());
out
}
pub fn empty_settings_frame() -> Vec<u8> {
frame_header(0, FRAME_TYPE_SETTINGS, 0, 0)
}
pub fn headers_frame(header_block: &[u8]) -> Vec<u8> {
let length = header_block.len() as u32;
let flags = super::frame::FLAG_END_HEADERS | 0x1; let mut out = frame_header(length, FRAME_TYPE_HEADERS, flags, 1);
out.extend_from_slice(header_block);
out
}
pub fn continuation_fragmented_headers(header_block: &[u8]) -> Vec<u8> {
let length = header_block.len() as u32;
let flags = 0x0;
let mut out = frame_header(length, FRAME_TYPE_HEADERS, flags, 1);
out.extend_from_slice(header_block);
out
}
pub fn continuation_frame(header_block: &[u8], end_headers: bool) -> Vec<u8> {
let length = header_block.len() as u32;
let flags = if end_headers {
super::frame::FLAG_END_HEADERS
} else {
0x0
};
let mut out = frame_header(length, FRAME_TYPE_CONTINUATION, flags, 1);
out.extend_from_slice(header_block);
out
}
pub fn hpack_literal_indexed_name(name_index: u8, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x40 | (name_index & 0x3F));
encode_literal_string(&mut out, value);
out
}
pub fn hpack_literal_indexed_with_name(name: &str, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x40);
encode_literal_string(&mut out, name);
encode_literal_string(&mut out, value);
out
}
pub fn hpack_literal_no_indexing(name_index: u8, value: &str) -> Vec<u8> {
let mut out = Vec::new();
out.push(name_index & 0x0F);
encode_literal_string(&mut out, value);
out
}
pub fn hpack_indexed(index: u8) -> Vec<u8> {
vec![0x80 | (index & 0x7F)]
}
pub fn hpack_literal_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);
if payload.len() < 0x7F {
out.push(0x80 | payload.len() as u8);
} else {
out.push(0xFF);
let mut v = payload.len() as u64 - 0x7F;
while v >= 128 {
out.push(((v & 0x7F) as u8) | 0x80);
v >>= 7;
}
out.push(v as u8);
}
out.extend_from_slice(&payload);
out
}
pub fn encode_literal_string(out: &mut Vec<u8>, s: &str) {
let len = s.len() as u64;
if len < 0x7F {
out.push(len as u8);
} else {
out.push(0x7F);
let mut v = len - 0x7F;
while v >= 128 {
out.push(((v & 0x7F) as u8) | 0x80);
v >>= 7;
}
out.push(v as u8);
}
out.extend_from_slice(s.as_bytes());
}
pub fn hpack_literal_huffman_indexed_name_with_raw(
name_index: u8,
raw_value: &[u8],
) -> Vec<u8> {
let mut out = Vec::new();
out.push(0x40 | (name_index & 0x3F));
out.push(0x80 | (raw_value.len() as u8));
out.extend_from_slice(raw_value);
out
}
pub fn settings_then_headers(header_block: &[u8]) -> Vec<u8> {
let mut out = empty_settings_frame();
out.extend_from_slice(&headers_frame(header_block));
out
}
pub fn headers_frame_on_stream(header_block: &[u8], stream_id: u32) -> Vec<u8> {
let length = header_block.len() as u32;
let flags = super::frame::FLAG_END_HEADERS | 0x1; let mut out = frame_header(length, FRAME_TYPE_HEADERS, flags, stream_id);
out.extend_from_slice(header_block);
out
}
pub fn headers_frame_on_stream_no_end(header_block: &[u8], stream_id: u32) -> Vec<u8> {
let length = header_block.len() as u32;
let mut out = frame_header(length, FRAME_TYPE_HEADERS, 0x0, stream_id);
out.extend_from_slice(header_block);
out
}
pub fn continuation_frame_on_stream(
header_block: &[u8],
stream_id: u32,
end_headers: bool,
) -> Vec<u8> {
let length = header_block.len() as u32;
let flags = if end_headers {
super::frame::FLAG_END_HEADERS
} else {
0x0
};
let mut out = frame_header(length, FRAME_TYPE_CONTINUATION, flags, stream_id);
out.extend_from_slice(header_block);
out
}
pub fn data_frame_on_stream(payload: &[u8], stream_id: u32) -> Vec<u8> {
let length = payload.len() as u32;
let mut out = frame_header(length, super::frame::FRAME_TYPE_DATA, 0x0, stream_id);
out.extend_from_slice(payload);
out
}
}
#[cfg(test)]
mod tests {
use super::test_helpers::*;
use super::*;
#[test]
fn extracts_authority_from_static_table_index_1() {
let block = hpack_indexed(1);
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(
result, None,
"indexed-only :authority has no value; parser returns None"
);
}
#[test]
fn extracts_authority_from_literal_indexed() {
let block = hpack_literal_indexed_name(1, "api.example.com");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn extracts_authority_from_literal_with_incremental_indexing() {
let block = hpack_literal_indexed_with_name(":authority", "api.example.com");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn rejects_oversized_frame() {
let mut bytes = empty_settings_frame();
let bogus_header = frame_header(
16_385,
frame::FRAME_TYPE_HEADERS,
frame::FLAG_END_HEADERS,
1,
);
bytes.extend_from_slice(&bogus_header);
let err = extract_h2_authority(&bytes).unwrap_err();
assert!(matches!(
err,
H2ParseError::OversizedFrame {
declared: 16_385,
max: 16_384
}
));
}
#[test]
fn reassembles_headers_with_one_continuation() {
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let mid = block_full.len() / 2;
let (first_half, second_half) = block_full.split_at(mid);
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&continuation_fragmented_headers(first_half));
bytes.extend_from_slice(&continuation_frame(second_half, true));
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn rejects_non_headers_first_frame() {
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&frame_header(0, frame::FRAME_TYPE_DATA, 0, 1));
let err = extract_h2_authority(&bytes).unwrap_err();
assert!(matches!(
err,
H2ParseError::UnexpectedFirstFrame { frame_type: 0 }
));
}
#[test]
fn extracts_lowercased_authority() {
let block = hpack_literal_indexed_name(1, "API.Example.COM");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn strips_port_from_authority() {
let block = hpack_literal_indexed_name(1, "api.example.com:8443");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn returns_none_when_buffer_short() {
let bytes = empty_settings_frame();
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result, None);
let truncated = &bytes[..5];
let result = extract_h2_authority(truncated).unwrap();
assert_eq!(result, None);
}
#[test]
fn is_h2c_preface_recognises_canonical_preface() {
let mut bytes = HTTP2_PREFACE.to_vec();
bytes.extend_from_slice(&[0x00, 0x00, 0x00]);
assert!(is_h2c_preface(&bytes));
assert!(is_h2c_preface(HTTP2_PREFACE));
}
#[test]
fn is_h2c_preface_rejects_malformed() {
assert!(!is_h2c_preface(&HTTP2_PREFACE[..23]));
let mut bad = HTTP2_PREFACE.to_vec();
bad[5] = b'x';
assert!(!is_h2c_preface(&bad));
assert!(!is_h2c_preface(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n"));
assert!(!is_h2c_preface(&[0x16, 0x03, 0x01]));
}
#[test]
fn extracts_authority_from_padded_headers_frame() {
let block = hpack_literal_indexed_name(1, "api.example.com");
let pad_len: u8 = 4;
let mut padded_payload = Vec::new();
padded_payload.push(pad_len);
padded_payload.extend_from_slice(&block);
padded_payload.extend(std::iter::repeat_n(0u8, pad_len as usize));
let length = padded_payload.len() as u32;
let flags = frame::FLAG_END_HEADERS | frame::FLAG_PADDED;
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&frame_header(length, frame::FRAME_TYPE_HEADERS, flags, 1));
bytes.extend_from_slice(&padded_payload);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn rejects_padding_overflow() {
let block = hpack_literal_indexed_name(1, "api.example.com");
let mut padded_payload = Vec::new();
padded_payload.push(255);
padded_payload.extend_from_slice(&block);
let length = padded_payload.len() as u32;
let flags = frame::FLAG_END_HEADERS | frame::FLAG_PADDED;
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&frame_header(length, frame::FRAME_TYPE_HEADERS, flags, 1));
bytes.extend_from_slice(&padded_payload);
let err = extract_h2_authority(&bytes).unwrap_err();
assert_eq!(err, H2ParseError::PaddingOverflow);
}
#[test]
fn decodes_huffman_coded_literal() {
let block = hpack_literal_indexed_name_huffman(1, "api.example.com");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn decodes_dynamic_table_reference_with_stateful_decoder() {
let mut decoder = HpackDecoder::new();
let block1 = hpack_literal_indexed_name(1, "api.example.com");
let bytes1 = settings_then_headers(&block1);
let r1 = extract_h2_authority_with(&bytes1, &mut decoder)
.unwrap()
.unwrap();
assert_eq!(r1.value, "api.example.com");
let block2 = vec![0x80 | 62];
let bytes2 = settings_then_headers(&block2);
let r2 = extract_h2_authority_with(&bytes2, &mut decoder)
.unwrap()
.unwrap();
assert_eq!(r2.value, "api.example.com");
assert_eq!(r2.provenance, AuthorityProvenance::DynamicIndexed);
}
#[test]
fn extracts_authority_with_priority_flag() {
let block = hpack_literal_indexed_name(1, "api.example.com");
let priority_section = [0u8; 5];
let mut payload = Vec::new();
payload.extend_from_slice(&priority_section);
payload.extend_from_slice(&block);
let length = payload.len() as u32;
let flags = frame::FLAG_END_HEADERS | frame::FLAG_PRIORITY;
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&frame_header(length, frame::FRAME_TYPE_HEADERS, flags, 1));
bytes.extend_from_slice(&payload);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn strips_trailing_dot_from_authority() {
let block = hpack_literal_indexed_name(1, "api.example.com.");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn ipv6_literal_authority_keeps_brackets() {
let block = hpack_literal_indexed_name(1, "[::1]:443");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("[::1]"));
}
#[test]
fn extracts_authority_from_literal_without_indexing() {
let block = hpack_literal_no_indexing(1, "api.example.com");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn rejects_broken_huffman_literal() {
let block = hpack_literal_huffman_indexed_name_with_raw(1, &[0x00, 0x00, 0x00]);
let bytes = settings_then_headers(&block);
let err = extract_h2_authority(&bytes).unwrap_err();
assert!(matches!(
err,
H2ParseError::HuffmanInvalid | H2ParseError::HuffmanOversized { .. }
));
}
#[test]
fn reassembly_rejects_interleaved_data_frame() {
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let mid = block_full.len() / 2;
let (first_half, second_half) = block_full.split_at(mid);
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&continuation_fragmented_headers(first_half));
bytes.extend_from_slice(&frame_header(0, frame::FRAME_TYPE_DATA, 0, 1));
bytes.extend_from_slice(&continuation_frame(second_half, true));
let err = extract_h2_authority(&bytes).unwrap_err();
assert!(matches!(
err,
H2ParseError::InterleavedFrame { frame_type: 0 }
));
}
#[test]
fn reassembles_three_fragments_with_priority_and_padding() {
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let third = block_full.len() / 3;
let (a, rest) = block_full.split_at(third);
let (b, c) = rest.split_at(third);
let pad_len: u8 = 2;
let priority = [0u8; 5];
let mut headers_payload = Vec::new();
headers_payload.push(pad_len);
headers_payload.extend_from_slice(&priority);
headers_payload.extend_from_slice(a);
headers_payload.extend(std::iter::repeat_n(0u8, pad_len as usize));
let headers_frame_bytes = {
let length = headers_payload.len() as u32;
let flags = frame::FLAG_PADDED | frame::FLAG_PRIORITY;
let mut out = frame_header(length, frame::FRAME_TYPE_HEADERS, flags, 1);
out.extend_from_slice(&headers_payload);
out
};
let mut bytes = empty_settings_frame();
bytes.extend_from_slice(&headers_frame_bytes);
bytes.extend_from_slice(&continuation_frame(b, false));
bytes.extend_from_slice(&continuation_frame(c, true));
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
#[test]
fn reassembly_rejects_oversized_aggregate_block() {
let mut bytes = empty_settings_frame();
let chunk = vec![0xAAu8; 16_383];
bytes.extend_from_slice(&continuation_fragmented_headers(&chunk));
bytes.extend_from_slice(&continuation_frame(&chunk, false));
bytes.extend_from_slice(&continuation_frame(&chunk, false));
bytes.extend_from_slice(&continuation_frame(&chunk, false));
bytes.extend_from_slice(&continuation_frame(&chunk, true));
let err = extract_h2_authority(&bytes).unwrap_err();
assert!(matches!(
err,
H2ParseError::HpackOversizedHeaderBlock { .. }
));
}
#[test]
fn single_headers_with_end_headers_works_unchanged_via_reassembly_path() {
let block = hpack_literal_indexed_name(1, "api.example.com");
let bytes = settings_then_headers(&block);
let result = extract_h2_authority(&bytes).unwrap();
assert_eq!(result.as_deref(), Some("api.example.com"));
}
fn parse_first_frame(bytes: &[u8]) -> (frame::FrameHeader, Vec<u8>) {
let (header, payload, _rest) = parse_one_frame(bytes).unwrap().unwrap();
(header, payload.to_vec())
}
#[test]
fn ingest_single_headers_with_end_headers_returns_block() {
let mut r = H2StreamReassembler::new();
let block = hpack_literal_indexed_name(1, "api.example.com");
let frame_bytes = headers_frame(&block);
let (h, p) = parse_first_frame(&frame_bytes);
let out = r.ingest(&h, &p).unwrap();
let (sid, returned) = out.expect("expected immediate completion");
assert_eq!(sid, 1);
assert_eq!(returned, block);
assert_eq!(r.pending_count(), 0);
}
#[test]
fn ingest_two_headers_on_different_streams_returns_two_blocks() {
let mut r = H2StreamReassembler::new();
let block_a = hpack_literal_indexed_name(1, "api.example.com");
let block_b = hpack_literal_indexed_name(1, "api.other.com");
let f_a = headers_frame_on_stream(&block_a, 1);
let f_b = headers_frame_on_stream(&block_b, 3);
let (ha, pa) = parse_first_frame(&f_a);
let (hb, pb) = parse_first_frame(&f_b);
let out_a = r.ingest(&ha, &pa).unwrap().unwrap();
let out_b = r.ingest(&hb, &pb).unwrap().unwrap();
assert_eq!(out_a.0, 1);
assert_eq!(out_b.0, 3);
assert_eq!(out_a.1, block_a);
assert_eq!(out_b.1, block_b);
}
#[test]
fn ingest_continuation_on_correct_active_stream_reassembles() {
let mut r = H2StreamReassembler::new();
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let mid = block_full.len() / 2;
let (a, b) = block_full.split_at(mid);
let f1 = headers_frame_on_stream_no_end(a, 1);
let f2 = continuation_frame_on_stream(b, 1, true);
let (h1, p1) = parse_first_frame(&f1);
let (h2, p2) = parse_first_frame(&f2);
assert!(r.ingest(&h1, &p1).unwrap().is_none());
assert_eq!(r.pending_count(), 1);
let (sid, block) = r.ingest(&h2, &p2).unwrap().unwrap();
assert_eq!(sid, 1);
assert_eq!(block, block_full);
assert_eq!(r.pending_count(), 0);
}
#[test]
fn ingest_continuation_on_wrong_stream_returns_interleaved() {
let mut r = H2StreamReassembler::new();
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let mid = block_full.len() / 2;
let (a, b) = block_full.split_at(mid);
let f1 = headers_frame_on_stream_no_end(a, 1);
let f2 = continuation_frame_on_stream(b, 3, true);
let (h1, p1) = parse_first_frame(&f1);
let (h2, p2) = parse_first_frame(&f2);
r.ingest(&h1, &p1).unwrap();
let err = r.ingest(&h2, &p2).unwrap_err();
assert!(matches!(err, H2ParseError::InterleavedFrame { .. }));
}
#[test]
fn ingest_data_frame_passes_through_with_no_block_returned() {
let mut r = H2StreamReassembler::new();
let f = data_frame_on_stream(b"hello", 1);
let (h, p) = parse_first_frame(&f);
assert!(r.ingest(&h, &p).unwrap().is_none());
assert_eq!(r.pending_count(), 0);
}
#[test]
fn ingest_oversized_per_stream_block_returns_overflow() {
let mut r = H2StreamReassembler::new();
let chunk = vec![0u8; 16_383];
let f1 = headers_frame_on_stream_no_end(&chunk, 1);
let (h1, p1) = parse_first_frame(&f1);
r.ingest(&h1, &p1).unwrap();
for _ in 0..3 {
let f = continuation_frame_on_stream(&chunk, 1, false);
let (h, p) = parse_first_frame(&f);
r.ingest(&h, &p).unwrap();
}
let f = continuation_frame_on_stream(&chunk, 1, false);
let (h, p) = parse_first_frame(&f);
let err = r.ingest(&h, &p).unwrap_err();
assert!(matches!(
err,
H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::PerStreamBlock
}
));
}
#[test]
fn ingest_oversized_aggregate_returns_overflow() {
let mut r = H2StreamReassembler::new();
r.blocks.insert(1, vec![0u8; 60 * 1024]);
r.total_in_flight = 60 * 1024;
r.active_stream = Some(1);
let chunk = vec![0u8; 16_383];
let f = continuation_frame_on_stream(&chunk, 1, false);
let (h, p) = parse_first_frame(&f);
let err = r.ingest(&h, &p).unwrap_err();
assert!(matches!(
err,
H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::PerStreamBlock
}
));
let mut r2 = H2StreamReassembler::new();
r2.total_in_flight = REASSEMBLER_TOTAL_IN_FLIGHT_MAX - 100;
let block = vec![0u8; 200];
let f = headers_frame_on_stream_no_end(&block, 5);
let (h, p) = parse_first_frame(&f);
let err = r2.ingest(&h, &p).unwrap_err();
assert!(matches!(
err,
H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::TotalInFlight
}
));
}
#[test]
fn ingest_too_many_concurrent_streams_returns_overflow() {
let mut r = H2StreamReassembler::new();
for i in 1..=REASSEMBLER_MAX_CONCURRENT_STREAMS as u32 {
r.blocks.insert(i * 2 + 1, vec![]);
}
let block = hpack_literal_indexed_name(1, "api.example.com");
let f = headers_frame_on_stream(&block, 999);
let (h, p) = parse_first_frame(&f);
let err = r.ingest(&h, &p).unwrap_err();
assert!(matches!(
err,
H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::ConcurrentStreams
}
));
}
#[test]
fn feed_returns_decoded_authority_for_complete_headers_on_stream_1() {
let mut d = H2ConnectionDecoder::new();
let block = hpack_literal_indexed_name(1, "api.example.com");
let f = headers_frame(&block);
let (h, p) = parse_first_frame(&f);
let out = d.feed_frame(&h, &p).unwrap().unwrap();
assert_eq!(out.stream_id, 1);
assert_eq!(out.authority.as_deref(), Some("api.example.com"));
assert!(!out.via_dynamic_table);
assert!(!out.via_huffman);
}
#[test]
fn feed_returns_two_decoded_blocks_for_two_streams_interleaved_via_continuation() {
let mut d = H2ConnectionDecoder::new();
let block_a = hpack_literal_indexed_name(1, "api.example.com");
let block_b = hpack_literal_indexed_name(1, "api.other.com");
let fa = headers_frame_on_stream(&block_a, 1);
let (ha, pa) = parse_first_frame(&fa);
let out_a = d.feed_frame(&ha, &pa).unwrap().unwrap();
assert_eq!(out_a.stream_id, 1);
assert_eq!(out_a.authority.as_deref(), Some("api.example.com"));
let mid = block_b.len() / 2;
let (b1, b2) = block_b.split_at(mid);
let f1 = headers_frame_on_stream_no_end(b1, 3);
let f2 = continuation_frame_on_stream(b2, 3, true);
let (h1, p1) = parse_first_frame(&f1);
let (h2, p2) = parse_first_frame(&f2);
assert!(d.feed_frame(&h1, &p1).unwrap().is_none());
let out_b = d.feed_frame(&h2, &p2).unwrap().unwrap();
assert_eq!(out_b.stream_id, 3);
assert_eq!(out_b.authority.as_deref(), Some("api.other.com"));
}
#[test]
fn feed_returns_provenance_when_huffman_used() {
let mut d = H2ConnectionDecoder::new();
let block = hpack_literal_indexed_name_huffman(1, "api.example.com");
let f = headers_frame(&block);
let (h, p) = parse_first_frame(&f);
let out = d.feed_frame(&h, &p).unwrap().unwrap();
assert!(out.via_huffman);
}
#[test]
fn feed_returns_provenance_when_dynamic_table_used() {
let mut d = H2ConnectionDecoder::new();
let block1 = hpack_literal_indexed_name(1, "api.example.com");
let f1 = headers_frame_on_stream(&block1, 1);
let (h1, p1) = parse_first_frame(&f1);
let _ = d.feed_frame(&h1, &p1).unwrap();
let block2 = vec![0x80 | 62];
let f2 = headers_frame_on_stream(&block2, 3);
let (h2, p2) = parse_first_frame(&f2);
let out = d.feed_frame(&h2, &p2).unwrap().unwrap();
assert!(out.via_dynamic_table);
assert_eq!(out.authority.as_deref(), Some("api.example.com"));
}
#[test]
fn feed_returns_none_for_data_frame() {
let mut d = H2ConnectionDecoder::new();
let f = data_frame_on_stream(b"payload", 1);
let (h, p) = parse_first_frame(&f);
assert!(d.feed_frame(&h, &p).unwrap().is_none());
}
#[test]
fn feed_propagates_hpack_decoder_state_across_streams() {
let mut d = H2ConnectionDecoder::new();
let block1 = hpack_literal_indexed_name(1, "shared.example.com");
let f1 = headers_frame_on_stream(&block1, 1);
let (h1, p1) = parse_first_frame(&f1);
let r1 = d.feed_frame(&h1, &p1).unwrap().unwrap();
assert_eq!(r1.authority.as_deref(), Some("shared.example.com"));
let block2 = vec![0x80 | 62];
let f2 = headers_frame_on_stream(&block2, 5);
let (h2, p2) = parse_first_frame(&f2);
let r2 = d.feed_frame(&h2, &p2).unwrap().unwrap();
assert_eq!(r2.authority.as_deref(), Some("shared.example.com"));
assert_eq!(r2.stream_id, 5);
}
#[test]
fn feed_propagates_reassembler_overflow_per_stream() {
let mut d = H2ConnectionDecoder::new();
let chunk = vec![0u8; 16_383];
let f1 = headers_frame_on_stream_no_end(&chunk, 1);
let (h1, p1) = parse_first_frame(&f1);
d.feed_frame(&h1, &p1).unwrap();
for _ in 0..3 {
let f = continuation_frame_on_stream(&chunk, 1, false);
let (h, p) = parse_first_frame(&f);
d.feed_frame(&h, &p).unwrap();
}
let f = continuation_frame_on_stream(&chunk, 1, false);
let (h, p) = parse_first_frame(&f);
let err = d.feed_frame(&h, &p).unwrap_err();
assert!(matches!(
err,
H2ParseError::ReassemblerOverflow {
kind: ReassemblerOverflowKind::PerStreamBlock
}
));
}
#[test]
fn feed_handles_settings_frame_size_update_to_dynamic_table() {
let mut d = H2ConnectionDecoder::new();
let settings = empty_settings_frame();
let (sh, sp) = parse_first_frame(&settings);
assert!(d.feed_frame(&sh, &sp).unwrap().is_none());
let block: Vec<u8> = vec![
0x20, 0x80 | 2, ];
let f = headers_frame_on_stream(&block, 1);
let (h, p) = parse_first_frame(&f);
let out = d.feed_frame(&h, &p).unwrap().unwrap();
assert_eq!(out.stream_id, 1);
assert!(out.authority.is_none());
}
#[test]
fn feed_returns_none_authority_when_headers_block_lacks_pseudo_header() {
let mut d = H2ConnectionDecoder::new();
let block = vec![0x80 | 2];
let f = headers_frame_on_stream(&block, 1);
let (h, p) = parse_first_frame(&f);
let out = d.feed_frame(&h, &p).unwrap().unwrap();
assert_eq!(out.stream_id, 1);
assert!(out.authority.is_none());
}
#[test]
fn feed_returns_block_for_continuation_after_headers_without_end_headers() {
let mut d = H2ConnectionDecoder::new();
let block_full = hpack_literal_indexed_name(1, "api.example.com");
let mid = block_full.len() / 2;
let (a, b) = block_full.split_at(mid);
let f1 = headers_frame_on_stream_no_end(a, 1);
let f2 = continuation_frame_on_stream(b, 1, true);
let (h1, p1) = parse_first_frame(&f1);
let (h2, p2) = parse_first_frame(&f2);
assert!(d.feed_frame(&h1, &p1).unwrap().is_none());
let out = d.feed_frame(&h2, &p2).unwrap().unwrap();
assert_eq!(out.stream_id, 1);
assert_eq!(out.authority.as_deref(), Some("api.example.com"));
}
}