use crate::bytes::{Bytes, BytesMut};
use crate::net::quic_core::{decode_varint, encode_varint};
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::net::Ipv6Addr;
use super::h2::hpack::{
decode_huffman as hpack_decode_huffman, encode_huffman_to_buffer as hpack_encode_huffman,
huffman_encoded_size as hpack_huffman_encoded_size,
};
const H3_FRAME_DATA: u64 = 0x0;
const H3_FRAME_HEADERS: u64 = 0x1;
const H3_FRAME_CANCEL_PUSH: u64 = 0x3;
const H3_FRAME_SETTINGS: u64 = 0x4;
const H3_FRAME_PUSH_PROMISE: u64 = 0x5;
const H3_FRAME_GOAWAY: u64 = 0x7;
const H3_FRAME_MAX_PUSH_ID: u64 = 0xD;
const H3_FRAME_DATAGRAM: u64 = 0x30;
const H3_STREAM_TYPE_CONTROL: u64 = 0x00;
const H3_STREAM_TYPE_PUSH: u64 = 0x01;
const H3_STREAM_TYPE_QPACK_ENCODER: u64 = 0x02;
const H3_STREAM_TYPE_QPACK_DECODER: u64 = 0x03;
pub const H3_SETTING_QPACK_MAX_TABLE_CAPACITY: u64 = 0x01;
pub const H3_SETTING_MAX_FIELD_SECTION_SIZE: u64 = 0x06;
pub const H3_SETTING_QPACK_BLOCKED_STREAMS: u64 = 0x07;
pub const H3_SETTING_ENABLE_CONNECT_PROTOCOL: u64 = 0x08;
pub const H3_SETTING_H3_DATAGRAM: u64 = 0x33;
const QPACK_MAX_DECODED_HEADERS: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum H3NativeError {
UnexpectedEof,
InvalidFrame(&'static str),
FrameTooLarge {
payload_size: usize,
max_size: usize,
},
DuplicateSetting(u64),
InvalidSettingValue(u64),
ControlProtocol(&'static str),
StreamProtocol(&'static str),
QpackPolicy(&'static str),
InvalidRequestPseudoHeader(&'static str),
InvalidResponsePseudoHeader(&'static str),
ConcurrentStreamLimitExceeded {
active: u64,
limit: u64,
},
}
impl fmt::Display for H3NativeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEof => write!(f, "unexpected EOF"),
Self::InvalidFrame(msg) => write!(f, "invalid frame: {msg}"),
Self::FrameTooLarge {
payload_size,
max_size,
} => write!(
f,
"frame payload too large: {payload_size} bytes exceeds limit of {max_size} bytes"
),
Self::DuplicateSetting(id) => write!(f, "duplicate setting: 0x{id:x}"),
Self::InvalidSettingValue(id) => write!(f, "invalid setting value: 0x{id:x}"),
Self::ControlProtocol(msg) => write!(f, "control stream protocol violation: {msg}"),
Self::StreamProtocol(msg) => write!(f, "stream protocol violation: {msg}"),
Self::QpackPolicy(msg) => write!(f, "qpack policy violation: {msg}"),
Self::InvalidRequestPseudoHeader(msg) => {
write!(f, "invalid request pseudo-header set: {msg}")
}
Self::InvalidResponsePseudoHeader(msg) => {
write!(f, "invalid response pseudo-header set: {msg}")
}
Self::ConcurrentStreamLimitExceeded { active, limit } => write!(
f,
"concurrent request stream limit exceeded: {active} active, limit {limit}"
),
}
}
}
impl std::error::Error for H3NativeError {}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum H3QpackMode {
#[default]
StaticOnly,
DynamicTableAllowed,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum H3EndpointRole {
#[default]
Client,
Server,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct H3ConnectionConfig {
pub qpack_mode: H3QpackMode,
pub endpoint_role: H3EndpointRole,
pub max_frame_payload_size: usize,
pub max_concurrent_request_streams: Option<u64>,
}
impl Default for H3ConnectionConfig {
fn default() -> Self {
Self {
qpack_mode: H3QpackMode::StaticOnly,
endpoint_role: H3EndpointRole::Client,
max_frame_payload_size: 1024 * 1024,
max_concurrent_request_streams: None,
}
}
}
impl H3ConnectionConfig {
#[must_use]
pub fn with_dynamic_qpack(mut self) -> Self {
self.qpack_mode = H3QpackMode::DynamicTableAllowed;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum H3UniStreamType {
Control,
Push,
QpackEncoder,
QpackDecoder,
Unknown(u64),
}
impl H3UniStreamType {
#[must_use]
pub fn decode(stream_type: u64) -> Self {
match stream_type {
H3_STREAM_TYPE_CONTROL => Self::Control,
H3_STREAM_TYPE_PUSH => Self::Push,
H3_STREAM_TYPE_QPACK_ENCODER => Self::QpackEncoder,
H3_STREAM_TYPE_QPACK_DECODER => Self::QpackDecoder,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownSetting {
pub id: u64,
pub value: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct H3Settings {
pub qpack_max_table_capacity: Option<u64>,
pub max_field_section_size: Option<u64>,
pub qpack_blocked_streams: Option<u64>,
pub enable_connect_protocol: Option<bool>,
pub h3_datagram: Option<bool>,
pub unknown: Vec<UnknownSetting>,
}
impl H3Settings {
pub fn encode_payload(&self, out: &mut Vec<u8>) -> Result<(), H3NativeError> {
if let Some(v) = self.qpack_max_table_capacity {
encode_setting(out, H3_SETTING_QPACK_MAX_TABLE_CAPACITY, v)?;
}
if let Some(v) = self.max_field_section_size {
encode_setting(out, H3_SETTING_MAX_FIELD_SECTION_SIZE, v)?;
}
if let Some(v) = self.qpack_blocked_streams {
encode_setting(out, H3_SETTING_QPACK_BLOCKED_STREAMS, v)?;
}
if let Some(v) = self.enable_connect_protocol {
encode_setting(out, H3_SETTING_ENABLE_CONNECT_PROTOCOL, u64::from(v))?;
}
if let Some(v) = self.h3_datagram {
encode_setting(out, H3_SETTING_H3_DATAGRAM, u64::from(v))?;
}
for s in &self.unknown {
if is_http2_reserved_settings_id(s.id) {
return Err(H3NativeError::InvalidSettingValue(s.id));
}
encode_setting(out, s.id, s.value)?;
}
Ok(())
}
pub fn decode_payload(input: &[u8]) -> Result<Self, H3NativeError> {
let mut settings = Self::default();
let mut seen_ids = BTreeSet::new();
let mut pos = 0usize;
while pos < input.len() {
let (id, id_len) = decode_varint(input.get(pos..).ok_or(H3NativeError::UnexpectedEof)?)
.map_err(|_| H3NativeError::InvalidFrame("invalid setting id varint"))?;
pos += id_len;
let (value, val_len) =
decode_varint(input.get(pos..).ok_or(H3NativeError::UnexpectedEof)?)
.map_err(|_| H3NativeError::InvalidFrame("invalid setting value varint"))?;
pos += val_len;
if !seen_ids.insert(id) {
return Err(H3NativeError::DuplicateSetting(id));
}
match id {
id if is_http2_reserved_settings_id(id) => {
return Err(H3NativeError::InvalidSettingValue(id));
}
H3_SETTING_QPACK_MAX_TABLE_CAPACITY => {
settings.qpack_max_table_capacity = Some(value);
}
H3_SETTING_MAX_FIELD_SECTION_SIZE => {
settings.max_field_section_size = Some(value);
}
H3_SETTING_QPACK_BLOCKED_STREAMS => {
settings.qpack_blocked_streams = Some(value);
}
H3_SETTING_ENABLE_CONNECT_PROTOCOL => {
settings.enable_connect_protocol = Some(parse_bool_setting(id, value)?);
}
H3_SETTING_H3_DATAGRAM => {
settings.h3_datagram = Some(parse_bool_setting(id, value)?);
}
_ => settings.unknown.push(UnknownSetting { id, value }),
}
}
Ok(settings)
}
}
const fn is_http2_reserved_settings_id(id: u64) -> bool {
matches!(id, 0x00 | 0x02 | 0x03 | 0x04 | 0x05)
}
fn parse_bool_setting(id: u64, value: u64) -> Result<bool, H3NativeError> {
match value {
0 => Ok(false),
1 => Ok(true),
_ => Err(H3NativeError::InvalidSettingValue(id)),
}
}
fn encode_setting(out: &mut Vec<u8>, id: u64, value: u64) -> Result<(), H3NativeError> {
encode_varint(id, out).map_err(|_| H3NativeError::InvalidFrame("setting id out of range"))?;
encode_varint(value, out)
.map_err(|_| H3NativeError::InvalidFrame("setting value out of range"))?;
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum H3Frame {
Data(Vec<u8>),
Headers(Vec<u8>),
CancelPush(u64),
Settings(H3Settings),
PushPromise {
push_id: u64,
field_block: Vec<u8>,
},
Goaway(u64),
MaxPushId(u64),
Datagram {
quarter_stream_id: u64,
payload: Vec<u8>,
},
Unknown {
frame_type: u64,
payload: Vec<u8>,
},
}
impl H3Frame {
pub fn encode(&self, out: &mut Vec<u8>) -> Result<(), H3NativeError> {
let mut payload = Vec::new();
let frame_type = match self {
Self::Data(bytes) => {
payload.extend_from_slice(bytes);
H3_FRAME_DATA
}
Self::Headers(bytes) => {
payload.extend_from_slice(bytes);
H3_FRAME_HEADERS
}
Self::CancelPush(id) => {
encode_varint(*id, &mut payload)
.map_err(|_| H3NativeError::InvalidFrame("cancel_push id out of range"))?;
H3_FRAME_CANCEL_PUSH
}
Self::Settings(settings) => {
settings.encode_payload(&mut payload)?;
H3_FRAME_SETTINGS
}
Self::PushPromise {
push_id,
field_block,
} => {
encode_varint(*push_id, &mut payload)
.map_err(|_| H3NativeError::InvalidFrame("push_id out of range"))?;
payload.extend_from_slice(field_block);
H3_FRAME_PUSH_PROMISE
}
Self::Goaway(id) => {
encode_varint(*id, &mut payload)
.map_err(|_| H3NativeError::InvalidFrame("goaway id out of range"))?;
H3_FRAME_GOAWAY
}
Self::MaxPushId(id) => {
encode_varint(*id, &mut payload)
.map_err(|_| H3NativeError::InvalidFrame("max_push_id out of range"))?;
H3_FRAME_MAX_PUSH_ID
}
Self::Datagram {
quarter_stream_id,
payload: data,
} => {
encode_varint(*quarter_stream_id, &mut payload)
.map_err(|_| H3NativeError::InvalidFrame("quarter_stream_id out of range"))?;
payload.extend_from_slice(data);
H3_FRAME_DATAGRAM
}
Self::Unknown {
frame_type,
payload: body,
} => {
payload.extend_from_slice(body);
*frame_type
}
};
encode_varint(frame_type, out)
.map_err(|_| H3NativeError::InvalidFrame("frame type out of range"))?;
encode_varint(payload.len() as u64, out)
.map_err(|_| H3NativeError::InvalidFrame("frame length out of range"))?;
out.extend_from_slice(&payload);
Ok(())
}
pub fn decode(
input: &[u8],
config: &H3ConnectionConfig,
) -> Result<(Self, usize), H3NativeError> {
let (frame_type, type_len) =
decode_varint(input).map_err(|_| H3NativeError::InvalidFrame("frame type varint"))?;
let (len, len_len) = decode_varint(&input[type_len..])
.map_err(|_| H3NativeError::InvalidFrame("frame length varint"))?;
let len: usize = len
.try_into()
.map_err(|_| H3NativeError::InvalidFrame("frame length exceeds addressable range"))?;
if len > config.max_frame_payload_size {
return Err(H3NativeError::FrameTooLarge {
payload_size: len,
max_size: config.max_frame_payload_size,
});
}
let payload_start = type_len + len_len;
if frame_type == H3_FRAME_DATAGRAM {
let available = input.len().saturating_sub(payload_start);
let bounded_payload = &input[payload_start..payload_start + available.min(len)];
let (quarter_stream_id, n) = decode_varint(bounded_payload)
.map_err(|_| H3NativeError::InvalidFrame("quarter stream id varint"))?;
if available < len {
return Err(H3NativeError::InvalidFrame("insufficient frame payload"));
}
let payload = &input[payload_start..payload_start + len];
let consumed = payload_start + len;
return Ok((
Self::Datagram {
quarter_stream_id,
payload: payload[n..].to_vec(),
},
consumed,
));
}
if input.len().saturating_sub(payload_start) < len {
return Err(H3NativeError::UnexpectedEof);
}
let payload = &input[payload_start..payload_start + len];
let consumed = payload_start + len;
let frame = match frame_type {
H3_FRAME_DATA => Self::Data(payload.to_vec()),
H3_FRAME_HEADERS => Self::Headers(payload.to_vec()),
H3_FRAME_CANCEL_PUSH => {
let (id, n) = decode_varint(payload)
.map_err(|_| H3NativeError::InvalidFrame("cancel_push payload"))?;
if n != payload.len() {
return Err(H3NativeError::InvalidFrame("cancel_push trailing bytes"));
}
Self::CancelPush(id)
}
H3_FRAME_SETTINGS => Self::Settings(H3Settings::decode_payload(payload)?),
H3_FRAME_PUSH_PROMISE => {
let (push_id, n) = decode_varint(payload)
.map_err(|_| H3NativeError::InvalidFrame("push_promise push_id"))?;
let field_block = &payload[n..];
if field_block.is_empty() {
return Err(H3NativeError::InvalidFrame(
"push_promise empty field_block (RFC 9114 §7.2.5)",
));
}
Self::PushPromise {
push_id,
field_block: field_block.to_vec(),
}
}
H3_FRAME_GOAWAY => {
let (id, n) = decode_varint(payload)
.map_err(|_| H3NativeError::InvalidFrame("goaway payload"))?;
if n != payload.len() {
return Err(H3NativeError::InvalidFrame("goaway trailing bytes"));
}
Self::Goaway(id)
}
H3_FRAME_MAX_PUSH_ID => {
let (id, n) = decode_varint(payload)
.map_err(|_| H3NativeError::InvalidFrame("max_push_id payload"))?;
if n != payload.len() {
return Err(H3NativeError::InvalidFrame("max_push_id trailing bytes"));
}
Self::MaxPushId(id)
}
_ => Self::Unknown {
frame_type,
payload: payload.to_vec(),
},
};
Ok((frame, consumed))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct H3ControlState {
local_settings_sent: bool,
remote_settings_received: bool,
}
impl H3ControlState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn build_local_settings(&mut self, settings: H3Settings) -> Result<H3Frame, H3NativeError> {
if self.local_settings_sent {
return Err(H3NativeError::ControlProtocol(
"SETTINGS already sent on local control stream",
));
}
self.local_settings_sent = true;
Ok(H3Frame::Settings(settings))
}
pub fn on_remote_control_frame(&mut self, frame: &H3Frame) -> Result<(), H3NativeError> {
if self.remote_settings_received {
match frame {
H3Frame::Settings(_) => {
return Err(H3NativeError::ControlProtocol(
"duplicate SETTINGS on remote control stream",
));
}
H3Frame::Data(_)
| H3Frame::Headers(_)
| H3Frame::PushPromise { .. }
| H3Frame::Datagram { .. } => {
return Err(H3NativeError::ControlProtocol(
"frame type not allowed on control stream",
));
}
H3Frame::CancelPush(_)
| H3Frame::Goaway(_)
| H3Frame::MaxPushId(_)
| H3Frame::Unknown { .. } => {}
}
Ok(())
} else {
match frame {
H3Frame::Settings(_) => {
self.remote_settings_received = true;
Ok(())
}
_ => Err(H3NativeError::ControlProtocol(
"first remote control frame must be SETTINGS",
)),
}
}
}
}
pub fn validate_bidirectional_frame(frame: &H3Frame) -> Result<(), H3NativeError> {
match frame {
H3Frame::Data(_) | H3Frame::Headers(_) => Ok(()),
H3Frame::PushPromise { .. } => Ok(()),
H3Frame::Datagram { .. } => Ok(()),
H3Frame::Settings(_) => Err(H3NativeError::StreamProtocol(
"SETTINGS frame not allowed on bidirectional stream",
)),
H3Frame::CancelPush(_) => Err(H3NativeError::StreamProtocol(
"CANCEL_PUSH frame not allowed on bidirectional stream",
)),
H3Frame::Goaway(_) => Err(H3NativeError::StreamProtocol(
"GOAWAY frame not allowed on bidirectional stream",
)),
H3Frame::MaxPushId(_) => Err(H3NativeError::StreamProtocol(
"MAX_PUSH_ID frame not allowed on bidirectional stream",
)),
H3Frame::Unknown { .. } => Ok(()),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct H3PseudoHeaders {
pub method: Option<String>,
pub scheme: Option<String>,
pub authority: Option<String>,
pub path: Option<String>,
pub status: Option<u16>,
pub protocol: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct H3RequestHead {
pub pseudo: H3PseudoHeaders,
pub headers: Vec<(String, String)>,
}
impl H3RequestHead {
pub fn new(
pseudo: H3PseudoHeaders,
headers: Vec<(String, String)>,
) -> Result<Self, H3NativeError> {
validate_request_pseudo_headers(&pseudo)?;
for (name, value) in &headers {
validate_header_name(name)?;
if name.starts_with(':') {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"pseudo headers must not appear in regular header list",
));
}
validate_header_value(value)?;
}
Ok(Self { pseudo, headers })
}
pub fn new_with_settings(
pseudo: H3PseudoHeaders,
headers: Vec<(String, String)>,
enable_connect_protocol: bool,
) -> Result<Self, H3NativeError> {
validate_request_pseudo_headers_with_settings(&pseudo, enable_connect_protocol)?;
for (name, value) in &headers {
validate_header_name(name)?;
if name.starts_with(':') {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"pseudo headers must not appear in regular header list",
));
}
validate_header_value(value)?;
}
Ok(Self { pseudo, headers })
}
pub fn validate_connect_method(
&self,
enable_connect_protocol: bool,
) -> Result<(), H3NativeError> {
if self.pseudo.method.as_deref() != Some("CONNECT") {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"validate_connect_method called on non-CONNECT request",
));
}
validate_request_pseudo_headers_with_settings(&self.pseudo, enable_connect_protocol)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct H3ResponseHead {
pub status: u16,
pub headers: Vec<(String, String)>,
}
impl H3ResponseHead {
pub fn new(status: u16, headers: Vec<(String, String)>) -> Result<Self, H3NativeError> {
let pseudo = H3PseudoHeaders {
status: Some(status),
..H3PseudoHeaders::default()
};
validate_response_pseudo_headers(&pseudo)?;
for (name, value) in &headers {
validate_header_name(name)?;
if name.starts_with(':') {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers",
));
}
validate_header_value(value)?;
}
Ok(Self { status, headers })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QpackFieldPlan {
StaticIndex(u64),
DynamicIndex(u64),
Literal {
name: String,
value: String,
},
DynamicNameLiteral {
name_index: u64,
value: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QpackInstructionNameRef {
Static(u64),
Dynamic(u64),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QpackEncoderInstruction {
SetDynamicTableCapacity {
capacity: u64,
},
InsertWithNameReference {
name: QpackInstructionNameRef,
value: String,
},
InsertWithoutNameReference {
name: String,
value: String,
},
Duplicate {
index: u64,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum QpackDecoderInstruction {
HeaderAcknowledgement {
stream_id: u64,
},
StreamCancellation {
stream_id: u64,
},
InsertCountIncrement {
increment: u64,
},
}
#[must_use]
pub fn qpack_static_plan_for_request(head: &H3RequestHead) -> Vec<QpackFieldPlan> {
let mut out = Vec::new();
if let Some(method) = &head.pseudo.method {
if let Some(idx) = qpack_static_method_index(method) {
out.push(QpackFieldPlan::StaticIndex(idx));
} else {
out.push(QpackFieldPlan::Literal {
name: ":method".to_string(),
value: method.clone(),
});
}
}
if let Some(scheme) = &head.pseudo.scheme {
if let Some(idx) = qpack_static_scheme_index(scheme) {
out.push(QpackFieldPlan::StaticIndex(idx));
} else {
out.push(QpackFieldPlan::Literal {
name: ":scheme".to_string(),
value: scheme.clone(),
});
}
}
if let Some(path) = &head.pseudo.path {
if path == "/" {
out.push(QpackFieldPlan::StaticIndex(1));
} else {
out.push(QpackFieldPlan::Literal {
name: ":path".to_string(),
value: path.clone(),
});
}
}
if let Some(authority) = &head.pseudo.authority {
out.push(QpackFieldPlan::Literal {
name: ":authority".to_string(),
value: authority.clone(),
});
}
for (name, value) in &head.headers {
out.push(QpackFieldPlan::Literal {
name: name.clone(),
value: value.clone(),
});
}
out
}
#[must_use]
pub fn qpack_static_plan_for_response(head: &H3ResponseHead) -> Vec<QpackFieldPlan> {
let mut out = Vec::new();
if let Some(idx) = qpack_static_status_index(head.status) {
out.push(QpackFieldPlan::StaticIndex(idx));
} else {
out.push(QpackFieldPlan::Literal {
name: ":status".to_string(),
value: head.status.to_string(),
});
}
for (name, value) in &head.headers {
out.push(QpackFieldPlan::Literal {
name: name.clone(),
value: value.clone(),
});
}
out
}
pub fn qpack_encode_field_section(plan: &[QpackFieldPlan]) -> Result<Vec<u8>, H3NativeError> {
qpack_encode_field_section_with_context(plan, None)
}
pub fn qpack_encode_field_section_with_context(
plan: &[QpackFieldPlan],
qpack_context: Option<&QpackContext>,
) -> Result<Vec<u8>, H3NativeError> {
let mut out = Vec::new();
let required_insert_count = qpack_plan_required_insert_count(plan, qpack_context)?;
let encoded_insert_count = qpack_encode_required_insert_count(
required_insert_count,
qpack_context.map_or(0, |context| context.max_table_capacity),
)?;
qpack_encode_prefixed_int(&mut out, 0, 8, encoded_insert_count)?;
qpack_encode_prefixed_int(&mut out, 0, 7, 0)?;
let base = required_insert_count;
for field in plan {
match field {
QpackFieldPlan::StaticIndex(index) => {
if qpack_static_entry(*index).is_none() {
return Err(H3NativeError::InvalidFrame("unknown static qpack index"));
}
qpack_encode_prefixed_int(&mut out, 0b1100_0000, 6, *index)?;
}
QpackFieldPlan::DynamicIndex(index) => {
let context = qpack_context.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
if qpack_dynamic_entry(context.dynamic_table(), *index).is_none() {
return Err(H3NativeError::InvalidFrame("unknown dynamic qpack index"));
}
let relative = qpack_absolute_to_relative(base, *index)?;
qpack_encode_prefixed_int(&mut out, 0b1000_0000, 6, relative)?;
}
QpackFieldPlan::Literal { name, value } => {
qpack_encode_string(&mut out, 0b0010_0000, 3, name)?;
qpack_encode_string(&mut out, 0, 7, value)?;
}
QpackFieldPlan::DynamicNameLiteral { name_index, value } => {
let context = qpack_context.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
if qpack_dynamic_name(context.dynamic_table(), *name_index).is_none() {
return Err(H3NativeError::InvalidFrame(
"unknown dynamic qpack name index",
));
}
let relative = qpack_absolute_to_relative(base, *name_index)?;
qpack_encode_prefixed_int(&mut out, 0b0100_0000, 4, relative)?;
qpack_encode_string(&mut out, 0, 7, value)?;
}
}
}
Ok(out)
}
pub fn qpack_decode_field_section(
input: &[u8],
mode: H3QpackMode,
) -> Result<Vec<QpackFieldPlan>, H3NativeError> {
qpack_decode_field_section_with_context(input, mode, None)
}
fn qpack_decode_required_insert_count(
encoded_insert_count: u64,
total_inserts: u64,
max_table_capacity: usize,
) -> Result<u64, H3NativeError> {
if encoded_insert_count == 0 {
return Ok(0);
}
let max_entries = (max_table_capacity / 32) as u64;
if max_entries == 0 {
return Err(H3NativeError::QpackPolicy(
"required insert count requires dynamic table capacity",
));
}
let full_range = max_entries
.checked_mul(2)
.ok_or(H3NativeError::InvalidFrame(
"required insert count range overflow",
))?;
if encoded_insert_count > full_range {
return Err(H3NativeError::InvalidFrame(
"required insert count exceeds qpack full range",
));
}
let max_value = total_inserts
.checked_add(max_entries)
.ok_or(H3NativeError::InvalidFrame(
"required insert count exceeds addressable range",
))?;
let max_wrapped = (max_value / full_range) * full_range;
let mut required_insert_count = max_wrapped
.saturating_add(encoded_insert_count)
.saturating_sub(1);
if required_insert_count > max_value {
if required_insert_count <= full_range {
return Err(H3NativeError::InvalidFrame(
"required insert count decodes below zero",
));
}
required_insert_count -= full_range;
}
if required_insert_count == 0 {
return Err(H3NativeError::InvalidFrame(
"required insert count must decode to non-zero",
));
}
Ok(required_insert_count)
}
fn qpack_decode_base(
required_insert_count: u64,
sign: bool,
delta_base: u64,
) -> Result<u64, H3NativeError> {
if sign {
let signed_delta = delta_base
.checked_add(1)
.ok_or(H3NativeError::InvalidFrame(
"delta base exceeds required insert count",
))?;
required_insert_count
.checked_sub(signed_delta)
.ok_or(H3NativeError::InvalidFrame(
"delta base exceeds required insert count",
))
} else {
required_insert_count
.checked_add(delta_base)
.ok_or(H3NativeError::InvalidFrame(
"base exceeds addressable range",
))
}
}
fn qpack_encode_required_insert_count(
required_insert_count: u64,
max_table_capacity: usize,
) -> Result<u64, H3NativeError> {
if required_insert_count == 0 {
return Ok(0);
}
let max_entries = (max_table_capacity / 32) as u64;
if max_entries == 0 {
return Err(H3NativeError::QpackPolicy(
"required insert count requires dynamic table capacity",
));
}
let full_range = max_entries
.checked_mul(2)
.ok_or(H3NativeError::InvalidFrame(
"required insert count range overflow",
))?;
Ok((required_insert_count % full_range) + 1)
}
fn qpack_plan_required_insert_count(
plan: &[QpackFieldPlan],
qpack_context: Option<&QpackContext>,
) -> Result<u64, H3NativeError> {
let needs_dynamic = plan.iter().any(|field| {
matches!(
field,
QpackFieldPlan::DynamicIndex(_) | QpackFieldPlan::DynamicNameLiteral { .. }
)
});
if !needs_dynamic {
return Ok(0);
}
let context = qpack_context.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
Ok(context.dynamic_table().insertion_counter())
}
pub fn qpack_encode_encoder_instruction(
out: &mut Vec<u8>,
instruction: &QpackEncoderInstruction,
) -> Result<(), H3NativeError> {
match instruction {
QpackEncoderInstruction::SetDynamicTableCapacity { capacity } => {
qpack_encode_prefixed_int(out, 0b0010_0000, 5, *capacity)?;
}
QpackEncoderInstruction::InsertWithNameReference { name, value } => {
match name {
QpackInstructionNameRef::Static(index) => {
qpack_encode_prefixed_int(out, 0b1100_0000, 6, *index)?;
}
QpackInstructionNameRef::Dynamic(index) => {
qpack_encode_prefixed_int(out, 0b1000_0000, 6, *index)?;
}
}
qpack_encode_string(out, 0, 7, value)?;
}
QpackEncoderInstruction::InsertWithoutNameReference { name, value } => {
qpack_encode_string(out, 0b0100_0000, 5, name)?;
qpack_encode_string(out, 0, 7, value)?;
}
QpackEncoderInstruction::Duplicate { index } => {
qpack_encode_prefixed_int(out, 0, 5, *index)?;
}
}
Ok(())
}
pub fn qpack_decode_encoder_instruction(
input: &[u8],
) -> Result<(QpackEncoderInstruction, usize), H3NativeError> {
let first = *input.first().ok_or(H3NativeError::UnexpectedEof)?;
if (first & 0b1000_0000) != 0 {
let (index, index_extra) = qpack_decode_prefixed_int(first, 6, &input[1..])?;
let pos = 1 + index_extra;
let value_first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
let (value, value_extra) = qpack_decode_string(value_first, 7, &input[pos + 1..])?;
let name = if (first & 0b0100_0000) != 0 {
QpackInstructionNameRef::Static(index)
} else {
QpackInstructionNameRef::Dynamic(index)
};
return Ok((
QpackEncoderInstruction::InsertWithNameReference { name, value },
pos.saturating_add(1).saturating_add(value_extra),
));
}
if (first & 0b0100_0000) != 0 {
let (name, name_extra) = qpack_decode_string(first, 5, &input[1..])?;
let pos = 1 + name_extra;
let value_first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
let (value, value_extra) = qpack_decode_string(value_first, 7, &input[pos + 1..])?;
return Ok((
QpackEncoderInstruction::InsertWithoutNameReference { name, value },
pos.saturating_add(1).saturating_add(value_extra),
));
}
if (first & 0b0010_0000) != 0 {
let (capacity, extra) = qpack_decode_prefixed_int(first, 5, &input[1..])?;
return Ok((
QpackEncoderInstruction::SetDynamicTableCapacity { capacity },
1 + extra,
));
}
let (index, extra) = qpack_decode_prefixed_int(first, 5, &input[1..])?;
Ok((QpackEncoderInstruction::Duplicate { index }, 1 + extra))
}
pub fn qpack_encode_decoder_instruction(
out: &mut Vec<u8>,
instruction: &QpackDecoderInstruction,
) -> Result<(), H3NativeError> {
match instruction {
QpackDecoderInstruction::HeaderAcknowledgement { stream_id } => {
qpack_encode_prefixed_int(out, 0b1000_0000, 7, *stream_id)?;
}
QpackDecoderInstruction::StreamCancellation { stream_id } => {
qpack_encode_prefixed_int(out, 0b0100_0000, 6, *stream_id)?;
}
QpackDecoderInstruction::InsertCountIncrement { increment } => {
if *increment == 0 {
return Err(H3NativeError::InvalidFrame(
"qpack insert count increment must be non-zero",
));
}
qpack_encode_prefixed_int(out, 0, 6, *increment)?;
}
}
Ok(())
}
pub fn qpack_decode_decoder_instruction(
input: &[u8],
) -> Result<(QpackDecoderInstruction, usize), H3NativeError> {
let first = *input.first().ok_or(H3NativeError::UnexpectedEof)?;
if (first & 0b1000_0000) != 0 {
let (stream_id, extra) = qpack_decode_prefixed_int(first, 7, &input[1..])?;
return Ok((
QpackDecoderInstruction::HeaderAcknowledgement { stream_id },
1 + extra,
));
}
if (first & 0b0100_0000) != 0 {
let (stream_id, extra) = qpack_decode_prefixed_int(first, 6, &input[1..])?;
return Ok((
QpackDecoderInstruction::StreamCancellation { stream_id },
1 + extra,
));
}
let (increment, extra) = qpack_decode_prefixed_int(first, 6, &input[1..])?;
if increment == 0 {
return Err(H3NativeError::InvalidFrame(
"qpack insert count increment must be non-zero",
));
}
Ok((
QpackDecoderInstruction::InsertCountIncrement { increment },
1 + extra,
))
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct QpackDecoderFeedbackState {
known_received_count: u64,
acknowledged_streams: BTreeSet<u64>,
cancelled_streams: BTreeSet<u64>,
outstanding_references: BTreeMap<u64, Vec<u64>>,
first_error: Option<H3NativeError>,
}
impl QpackDecoderFeedbackState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn known_received_count(&self) -> u64 {
self.known_received_count
}
#[must_use]
pub fn acknowledged_stream_ids(&self) -> &BTreeSet<u64> {
&self.acknowledged_streams
}
#[must_use]
pub fn cancelled_stream_ids(&self) -> &BTreeSet<u64> {
&self.cancelled_streams
}
#[must_use]
pub fn outstanding_reference_count(&self) -> usize {
self.outstanding_references.values().map(Vec::len).sum()
}
#[must_use]
pub fn stream_outstanding_reference_count(&self, stream_id: u64) -> usize {
self.outstanding_references
.get(&stream_id)
.map_or(0, Vec::len)
}
#[must_use]
pub fn first_error(&self) -> Option<&H3NativeError> {
self.first_error.as_ref()
}
pub fn track_stream_references(
&mut self,
context: &mut QpackContext,
stream_id: u64,
references: &[u64],
) -> Result<(), H3NativeError> {
if self.acknowledged_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"qpack stream already acknowledged",
));
}
if self.cancelled_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"qpack stream already cancelled",
));
}
if self.outstanding_references.contains_key(&stream_id) {
return self.fail(H3NativeError::InvalidFrame("qpack stream already tracked"));
}
for insertion_id in references {
if context
.dynamic_table()
.get_by_insertion_id(*insertion_id)
.is_none()
{
return self.fail(H3NativeError::InvalidFrame(
"unknown dynamic qpack reference for stream",
));
}
}
for insertion_id in references {
let referenced = context.dynamic_table_mut().reference_entry(*insertion_id);
debug_assert!(referenced, "prechecked qpack reference must exist");
}
self.outstanding_references
.insert(stream_id, references.to_vec());
Ok(())
}
fn apply_header_acknowledgement(
&mut self,
context: &mut QpackContext,
stream_id: u64,
) -> Result<(), H3NativeError> {
if self.acknowledged_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"duplicate qpack header acknowledgement",
));
}
if self.cancelled_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"qpack acknowledgement after stream cancellation",
));
}
self.release_stream_references(context, stream_id)?;
self.acknowledged_streams.insert(stream_id);
Ok(())
}
fn apply_stream_cancellation(
&mut self,
context: &mut QpackContext,
stream_id: u64,
) -> Result<(), H3NativeError> {
if self.cancelled_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"duplicate qpack stream cancellation",
));
}
if self.acknowledged_streams.contains(&stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"qpack stream cancellation after acknowledgement",
));
}
self.release_stream_references(context, stream_id)?;
self.cancelled_streams.insert(stream_id);
Ok(())
}
fn release_stream_references(
&mut self,
context: &mut QpackContext,
stream_id: u64,
) -> Result<(), H3NativeError> {
let Some(references) = self.outstanding_references.get(&stream_id) else {
return self.fail(H3NativeError::InvalidFrame(
"unknown qpack decoder feedback stream",
));
};
let references = references.clone();
for insertion_id in &references {
if context
.dynamic_table()
.get_by_insertion_id(*insertion_id)
.is_none()
{
return self.fail(H3NativeError::InvalidFrame(
"tracked dynamic qpack reference missing",
));
}
}
self.outstanding_references.remove(&stream_id);
for insertion_id in references {
let released = context.dynamic_table_mut().unreference_entry(insertion_id);
debug_assert!(released, "prechecked qpack reference must still exist");
}
Ok(())
}
fn apply_insert_count_increment(&mut self, increment: u64) -> Result<(), H3NativeError> {
if increment == 0 {
return self.fail(H3NativeError::InvalidFrame(
"qpack decoder feedback increment must be non-zero",
));
}
let Some(next) = self.known_received_count.checked_add(increment) else {
return self.fail(H3NativeError::InvalidFrame(
"qpack known received count overflow",
));
};
self.known_received_count = next;
Ok(())
}
fn fail<T>(&mut self, error: H3NativeError) -> Result<T, H3NativeError> {
self.record_error(&error);
Err(error)
}
fn record_error(&mut self, error: &H3NativeError) {
if self.first_error.is_none() {
self.first_error = Some(error.clone());
}
}
}
pub fn qpack_apply_decoder_instruction(
feedback: &mut QpackDecoderFeedbackState,
context: &mut QpackContext,
mode: H3QpackMode,
instruction: &QpackDecoderInstruction,
) -> Result<(), H3NativeError> {
if mode != H3QpackMode::DynamicTableAllowed {
let error = H3NativeError::QpackPolicy("decoder feedback requires dynamic qpack mode");
feedback.record_error(&error);
return Err(error);
}
match instruction {
QpackDecoderInstruction::HeaderAcknowledgement { stream_id } => {
feedback.apply_header_acknowledgement(context, *stream_id)
}
QpackDecoderInstruction::StreamCancellation { stream_id } => {
feedback.apply_stream_cancellation(context, *stream_id)
}
QpackDecoderInstruction::InsertCountIncrement { increment } => {
feedback.apply_insert_count_increment(*increment)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QpackFieldSectionMetadata {
encoded_insert_count: u64,
required_insert_count: u64,
base: u64,
prefix_len: usize,
}
impl QpackFieldSectionMetadata {
#[must_use]
pub fn encoded_insert_count(&self) -> u64 {
self.encoded_insert_count
}
#[must_use]
pub fn required_insert_count(&self) -> u64 {
self.required_insert_count
}
#[must_use]
pub fn base(&self) -> u64 {
self.base
}
#[must_use]
pub fn prefix_len(&self) -> usize {
self.prefix_len
}
}
pub fn qpack_field_section_metadata(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
) -> Result<QpackFieldSectionMetadata, H3NativeError> {
let mut pos = 0usize;
let first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
pos += 1;
let (encoded_insert_count, ric_extra) = qpack_decode_prefixed_int(first, 8, &input[pos..])?;
pos += ric_extra;
let second = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
pos += 1;
let sign = (second & 0x80) != 0;
let (delta_base, db_extra) = qpack_decode_prefixed_int(second, 7, &input[pos..])?;
pos += db_extra;
match mode {
H3QpackMode::StaticOnly => {
if encoded_insert_count != 0 {
return Err(H3NativeError::QpackPolicy(
"required insert count must be zero in static-only mode",
));
}
if sign || delta_base != 0 {
return Err(H3NativeError::QpackPolicy(
"base must be zero in static-only mode",
));
}
Ok(QpackFieldSectionMetadata {
encoded_insert_count,
required_insert_count: 0,
base: 0,
prefix_len: pos,
})
}
H3QpackMode::DynamicTableAllowed => {
if encoded_insert_count > 65536 {
return Err(H3NativeError::QpackPolicy(
"required insert count exceeds reasonable limit",
));
}
if encoded_insert_count == 0 {
if sign || delta_base != 0 {
return Err(H3NativeError::InvalidFrame(
"base must be zero without required insert count",
));
}
return Ok(QpackFieldSectionMetadata {
encoded_insert_count,
required_insert_count: 0,
base: 0,
prefix_len: pos,
});
}
let context = qpack_context.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
let required_insert_count = qpack_decode_required_insert_count(
encoded_insert_count,
context.dynamic_table().insertion_counter(),
context.max_table_capacity,
)?;
let base = qpack_decode_base(required_insert_count, sign, delta_base)?;
Ok(QpackFieldSectionMetadata {
encoded_insert_count,
required_insert_count,
base,
prefix_len: pos,
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QpackBlockedStreamStatus {
Ready,
Blocked,
Cancelled,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QpackBlockedStreamRecord {
stream_id: u64,
required_insert_count: u64,
base: u64,
status: QpackBlockedStreamStatus,
blocked_reason: Option<&'static str>,
protected_references: Vec<u64>,
blocked_field_section: Option<Vec<u8>>,
first_failure: Option<H3NativeError>,
}
impl QpackBlockedStreamRecord {
fn new(
stream_id: u64,
metadata: &QpackFieldSectionMetadata,
status: QpackBlockedStreamStatus,
blocked_reason: Option<&'static str>,
protected_references: Vec<u64>,
) -> Self {
Self {
stream_id,
required_insert_count: metadata.required_insert_count(),
base: metadata.base(),
status,
blocked_reason,
protected_references,
blocked_field_section: None,
first_failure: None,
}
}
fn failed(
stream_id: u64,
metadata: Option<&QpackFieldSectionMetadata>,
error: H3NativeError,
) -> Self {
Self {
stream_id,
required_insert_count: metadata
.map_or(0, QpackFieldSectionMetadata::required_insert_count),
base: metadata.map_or(0, QpackFieldSectionMetadata::base),
status: QpackBlockedStreamStatus::Failed,
blocked_reason: None,
protected_references: Vec::new(),
blocked_field_section: None,
first_failure: Some(error),
}
}
fn record_failure(&mut self, error: &H3NativeError) {
if self.first_failure.is_none() {
self.first_failure = Some(error.clone());
}
if self.status != QpackBlockedStreamStatus::Cancelled {
self.status = QpackBlockedStreamStatus::Failed;
self.blocked_reason = None;
}
}
#[must_use]
pub fn stream_id(&self) -> u64 {
self.stream_id
}
#[must_use]
pub fn required_insert_count(&self) -> u64 {
self.required_insert_count
}
#[must_use]
pub fn base(&self) -> u64 {
self.base
}
#[must_use]
pub fn status(&self) -> QpackBlockedStreamStatus {
self.status
}
#[must_use]
pub fn blocked_reason(&self) -> Option<&'static str> {
self.blocked_reason
}
#[must_use]
pub fn protected_references(&self) -> &[u64] {
&self.protected_references
}
#[must_use]
pub fn first_failure(&self) -> Option<&H3NativeError> {
self.first_failure.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QpackBlockedStreamScheduler {
settings_blocked_streams: u64,
streams: BTreeMap<u64, QpackBlockedStreamRecord>,
first_failure: Option<H3NativeError>,
}
impl QpackBlockedStreamScheduler {
#[must_use]
pub fn new(settings_blocked_streams: u64) -> Self {
Self {
settings_blocked_streams,
streams: BTreeMap::new(),
first_failure: None,
}
}
#[must_use]
pub fn from_settings(settings: &H3Settings) -> Self {
Self::new(settings.qpack_blocked_streams.unwrap_or(0))
}
#[must_use]
pub fn settings_blocked_streams(&self) -> u64 {
self.settings_blocked_streams
}
#[must_use]
pub fn blocked_stream_count(&self) -> u64 {
self.streams
.values()
.filter(|record| record.status == QpackBlockedStreamStatus::Blocked)
.count() as u64
}
#[must_use]
pub fn record(&self, stream_id: u64) -> Option<&QpackBlockedStreamRecord> {
self.streams.get(&stream_id)
}
#[must_use]
pub fn first_failure(&self) -> Option<&H3NativeError> {
self.first_failure.as_ref()
}
pub fn submit_field_section(
&mut self,
context: &mut QpackContext,
feedback: &mut QpackDecoderFeedbackState,
mode: H3QpackMode,
stream_id: u64,
field_section: &[u8],
) -> Result<QpackBlockedStreamStatus, H3NativeError> {
if self.streams.contains_key(&stream_id) {
return self.fail(H3NativeError::StreamProtocol(
"qpack stream already scheduled",
));
}
let metadata = match qpack_field_section_metadata(field_section, mode, Some(context)) {
Ok(metadata) => metadata,
Err(error) => {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, None, error.clone()),
);
return self.fail(error);
}
};
let context_opt = (mode == H3QpackMode::DynamicTableAllowed).then_some(&*context);
let plan = match qpack_decode_field_section_with_context(field_section, mode, context_opt) {
Ok(plan) => plan,
Err(error) => {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
};
let references = qpack_plan_dynamic_references(&plan);
let will_block = metadata.required_insert_count() > feedback.known_received_count();
if will_block && self.settings_blocked_streams == 0 {
let error = H3NativeError::QpackPolicy("qpack blocked stream capacity is zero");
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
if will_block && self.blocked_stream_count() >= self.settings_blocked_streams {
let error = H3NativeError::QpackPolicy("qpack blocked stream capacity exceeded");
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
if mode == H3QpackMode::DynamicTableAllowed {
if let Err(error) = feedback.track_stream_references(context, stream_id, &references) {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
}
let status = if will_block {
QpackBlockedStreamStatus::Blocked
} else {
QpackBlockedStreamStatus::Ready
};
let blocked_reason =
will_block.then_some("required insert count exceeds known received count");
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::new(stream_id, &metadata, status, blocked_reason, references),
);
Ok(status)
}
pub fn submit_received_field_section(
&mut self,
context: &mut QpackContext,
feedback: &mut QpackDecoderFeedbackState,
mode: H3QpackMode,
stream_id: u64,
field_section: &[u8],
) -> Result<QpackBlockedStreamStatus, H3NativeError> {
if self.streams.contains_key(&stream_id) {
return self.fail(H3NativeError::StreamProtocol(
"qpack stream already scheduled",
));
}
let metadata = match qpack_field_section_metadata(field_section, mode, Some(context)) {
Ok(metadata) => metadata,
Err(error) => {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, None, error.clone()),
);
return self.fail(error);
}
};
if mode == H3QpackMode::DynamicTableAllowed
&& metadata.required_insert_count() > context.dynamic_table().insertion_counter()
{
if self.settings_blocked_streams == 0 {
let error = H3NativeError::QpackPolicy("qpack blocked stream capacity is zero");
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
if self.blocked_stream_count() >= self.settings_blocked_streams {
let error = H3NativeError::QpackPolicy("qpack blocked stream capacity exceeded");
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
let mut record = QpackBlockedStreamRecord::new(
stream_id,
&metadata,
QpackBlockedStreamStatus::Blocked,
Some("required insert count exceeds dynamic table state"),
Vec::new(),
);
record.blocked_field_section = Some(field_section.to_vec());
self.streams.insert(stream_id, record);
return Ok(QpackBlockedStreamStatus::Blocked);
}
let context_opt = (mode == H3QpackMode::DynamicTableAllowed).then_some(&*context);
let plan = match qpack_decode_field_section_with_context(field_section, mode, context_opt) {
Ok(plan) => plan,
Err(error) => {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
};
let references = qpack_plan_dynamic_references(&plan);
if mode == H3QpackMode::DynamicTableAllowed {
if let Err(error) = feedback.track_stream_references(context, stream_id, &references) {
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::failed(stream_id, Some(&metadata), error.clone()),
);
return self.fail(error);
}
}
self.streams.insert(
stream_id,
QpackBlockedStreamRecord::new(
stream_id,
&metadata,
QpackBlockedStreamStatus::Ready,
None,
references,
),
);
Ok(QpackBlockedStreamStatus::Ready)
}
pub fn apply_decoder_instruction(
&mut self,
feedback: &mut QpackDecoderFeedbackState,
context: &mut QpackContext,
mode: H3QpackMode,
instruction: &QpackDecoderInstruction,
) -> Result<Vec<u64>, H3NativeError> {
match instruction {
QpackDecoderInstruction::InsertCountIncrement { .. } => {
qpack_apply_decoder_instruction(feedback, context, mode, instruction)
.map_err(|error| self.record_global_error(error))?;
Ok(self.unblock_ready(feedback.known_received_count()))
}
QpackDecoderInstruction::HeaderAcknowledgement { stream_id } => {
if !self.streams.contains_key(stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"unknown qpack blocked stream acknowledgement",
));
}
if let Err(error) =
qpack_apply_decoder_instruction(feedback, context, mode, instruction)
{
self.record_stream_error(*stream_id, &error);
return Err(error);
}
if let Some(record) = self.streams.get_mut(stream_id) {
record.status = QpackBlockedStreamStatus::Ready;
record.blocked_reason = None;
record.protected_references.clear();
}
Ok(Vec::new())
}
QpackDecoderInstruction::StreamCancellation { stream_id } => {
if !self.streams.contains_key(stream_id) {
return self.fail(H3NativeError::InvalidFrame(
"unknown qpack blocked stream cancellation",
));
}
if let Err(error) =
qpack_apply_decoder_instruction(feedback, context, mode, instruction)
{
self.record_stream_error(*stream_id, &error);
return Err(error);
}
if let Some(record) = self.streams.get_mut(stream_id) {
record.status = QpackBlockedStreamStatus::Cancelled;
record.blocked_reason = None;
record.protected_references.clear();
}
Ok(Vec::new())
}
}
}
pub fn apply_encoder_instruction(
&mut self,
context: &mut QpackContext,
feedback: &mut QpackDecoderFeedbackState,
mode: H3QpackMode,
instruction: &QpackEncoderInstruction,
) -> Result<(Option<u64>, Vec<u64>), H3NativeError> {
let inserted = qpack_apply_encoder_instruction(context, mode, instruction)
.map_err(|error| self.record_global_error(error))?;
let mut unblocked = self.unblock_decodable(context, feedback, mode)?;
unblocked.extend(self.unblock_ready(feedback.known_received_count()));
Ok((inserted, unblocked))
}
pub fn cancel_stream(
&mut self,
feedback: &mut QpackDecoderFeedbackState,
context: &mut QpackContext,
mode: H3QpackMode,
stream_id: u64,
) -> Result<(), H3NativeError> {
self.apply_decoder_instruction(
feedback,
context,
mode,
&QpackDecoderInstruction::StreamCancellation { stream_id },
)
.map(|_| ())
}
fn unblock_ready(&mut self, known_received_count: u64) -> Vec<u64> {
let mut unblocked = Vec::new();
for record in self.streams.values_mut() {
if record.status == QpackBlockedStreamStatus::Blocked
&& record.required_insert_count <= known_received_count
{
record.status = QpackBlockedStreamStatus::Ready;
record.blocked_reason = None;
unblocked.push(record.stream_id);
}
}
unblocked
}
fn unblock_decodable(
&mut self,
context: &mut QpackContext,
feedback: &mut QpackDecoderFeedbackState,
mode: H3QpackMode,
) -> Result<Vec<u64>, H3NativeError> {
let ready_ids: Vec<u64> = self
.streams
.iter()
.filter(|(_, record)| {
record.status == QpackBlockedStreamStatus::Blocked
&& record.blocked_field_section.is_some()
&& record.required_insert_count <= context.dynamic_table().insertion_counter()
})
.map(|(stream_id, _)| *stream_id)
.collect();
let mut unblocked = Vec::new();
for stream_id in ready_ids {
let field_section = self
.streams
.get(&stream_id)
.and_then(|record| record.blocked_field_section.clone())
.ok_or(H3NativeError::InvalidFrame(
"qpack blocked field section missing",
))?;
let context_opt = (mode == H3QpackMode::DynamicTableAllowed).then_some(&*context);
let plan =
match qpack_decode_field_section_with_context(&field_section, mode, context_opt) {
Ok(plan) => plan,
Err(error) => {
self.record_stream_error(stream_id, &error);
return Err(error);
}
};
let references = qpack_plan_dynamic_references(&plan);
if mode == H3QpackMode::DynamicTableAllowed {
if let Err(error) =
feedback.track_stream_references(context, stream_id, &references)
{
self.record_stream_error(stream_id, &error);
return Err(error);
}
}
if let Some(record) = self.streams.get_mut(&stream_id) {
record.status = QpackBlockedStreamStatus::Ready;
record.blocked_reason = None;
record.protected_references = references;
record.blocked_field_section = None;
}
unblocked.push(stream_id);
}
Ok(unblocked)
}
fn record_stream_error(&mut self, stream_id: u64, error: &H3NativeError) {
self.record_error(error);
if let Some(record) = self.streams.get_mut(&stream_id) {
record.record_failure(error);
}
}
fn record_global_error(&mut self, error: H3NativeError) -> H3NativeError {
self.record_error(&error);
error
}
fn fail<T>(&mut self, error: H3NativeError) -> Result<T, H3NativeError> {
self.record_error(&error);
Err(error)
}
fn record_error(&mut self, error: &H3NativeError) {
if self.first_failure.is_none() {
self.first_failure = Some(error.clone());
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct QpackInstructionStreamOutcome {
instructions_processed: usize,
inserted_entry_ids: Vec<u64>,
unblocked_stream_ids: Vec<u64>,
}
impl QpackInstructionStreamOutcome {
fn record_encoder_result(&mut self, inserted: Option<u64>, mut unblocked: Vec<u64>) {
self.instructions_processed += 1;
if let Some(insertion_id) = inserted {
self.inserted_entry_ids.push(insertion_id);
}
self.unblocked_stream_ids.append(&mut unblocked);
}
fn record_decoder_result(&mut self, mut unblocked: Vec<u64>) {
self.instructions_processed += 1;
self.unblocked_stream_ids.append(&mut unblocked);
}
#[must_use]
pub fn instructions_processed(&self) -> usize {
self.instructions_processed
}
#[must_use]
pub fn inserted_entry_ids(&self) -> &[u64] {
&self.inserted_entry_ids
}
#[must_use]
pub fn unblocked_stream_ids(&self) -> &[u64] {
&self.unblocked_stream_ids
}
}
#[derive(Debug)]
pub struct QpackInstructionStreamState {
mode: H3QpackMode,
context: QpackContext,
decoder_feedback: QpackDecoderFeedbackState,
blocked_scheduler: QpackBlockedStreamScheduler,
encoder_stream_id: Option<u64>,
decoder_stream_id: Option<u64>,
first_failure: Option<H3NativeError>,
}
impl QpackInstructionStreamState {
pub fn new(
mode: H3QpackMode,
max_table_capacity: u64,
settings_blocked_streams: u64,
) -> Result<Self, H3NativeError> {
if mode == H3QpackMode::StaticOnly {
if max_table_capacity > 0 {
return Err(H3NativeError::QpackPolicy(
"dynamic qpack table disabled by policy",
));
}
if settings_blocked_streams > 0 {
return Err(H3NativeError::QpackPolicy(
"qpack blocked streams must be zero in static-only mode",
));
}
}
let max_table_capacity: usize = max_table_capacity.try_into().map_err(|_| {
H3NativeError::InvalidFrame("qpack dynamic table capacity exceeds addressable range")
})?;
Ok(Self {
mode,
context: QpackContext::new(max_table_capacity),
decoder_feedback: QpackDecoderFeedbackState::new(),
blocked_scheduler: QpackBlockedStreamScheduler::new(settings_blocked_streams),
encoder_stream_id: None,
decoder_stream_id: None,
first_failure: None,
})
}
pub fn from_settings(mode: H3QpackMode, settings: &H3Settings) -> Result<Self, H3NativeError> {
Self::new(
mode,
settings.qpack_max_table_capacity.unwrap_or(0),
settings.qpack_blocked_streams.unwrap_or(0),
)
}
#[must_use]
pub fn mode(&self) -> H3QpackMode {
self.mode
}
#[must_use]
pub fn context(&self) -> &QpackContext {
&self.context
}
#[must_use]
pub fn decoder_feedback(&self) -> &QpackDecoderFeedbackState {
&self.decoder_feedback
}
#[must_use]
pub fn blocked_scheduler(&self) -> &QpackBlockedStreamScheduler {
&self.blocked_scheduler
}
#[must_use]
pub fn encoder_stream_id(&self) -> Option<u64> {
self.encoder_stream_id
}
#[must_use]
pub fn decoder_stream_id(&self) -> Option<u64> {
self.decoder_stream_id
}
#[must_use]
pub fn known_received_count(&self) -> u64 {
self.decoder_feedback.known_received_count()
}
#[must_use]
pub fn blocked_stream_count(&self) -> u64 {
self.blocked_scheduler.blocked_stream_count()
}
#[must_use]
pub fn settings_blocked_streams(&self) -> u64 {
self.blocked_scheduler.settings_blocked_streams()
}
#[must_use]
pub fn first_failure(&self) -> Option<&H3NativeError> {
self.first_failure
.as_ref()
.or_else(|| self.blocked_scheduler.first_failure())
.or_else(|| self.decoder_feedback.first_error())
}
pub fn register_stream(
&mut self,
stream_id: u64,
kind: H3UniStreamType,
) -> Result<(), H3NativeError> {
if self.registered_stream_kind(stream_id).is_some() {
return self.fail(H3NativeError::StreamProtocol(
"qpack instruction stream id already registered",
));
}
match kind {
H3UniStreamType::QpackEncoder => {
if self.encoder_stream_id.is_some() {
return self.fail(H3NativeError::StreamProtocol(
"duplicate remote qpack encoder stream",
));
}
self.encoder_stream_id = Some(stream_id);
Ok(())
}
H3UniStreamType::QpackDecoder => {
if self.decoder_stream_id.is_some() {
return self.fail(H3NativeError::StreamProtocol(
"duplicate remote qpack decoder stream",
));
}
self.decoder_stream_id = Some(stream_id);
Ok(())
}
H3UniStreamType::Control | H3UniStreamType::Push | H3UniStreamType::Unknown(_) => self
.fail(H3NativeError::StreamProtocol(
"qpack instruction stream requires qpack stream type",
)),
}
}
pub fn register_from_connection(
&mut self,
connection: &H3ConnectionState,
stream_id: u64,
) -> Result<H3UniStreamType, H3NativeError> {
let kind =
connection
.remote_uni_stream_type(stream_id)
.ok_or(H3NativeError::StreamProtocol(
"unknown unidirectional stream",
))?;
self.register_stream(stream_id, kind)?;
Ok(kind)
}
pub fn ensure_stream_registered(
&mut self,
stream_id: u64,
kind: H3UniStreamType,
) -> Result<(), H3NativeError> {
match self.registered_stream_kind(stream_id) {
Some(actual) if actual == kind => Ok(()),
Some(_) => self.fail(H3NativeError::StreamProtocol(
"qpack instruction type does not match registered stream",
)),
None => self.register_stream(stream_id, kind),
}
}
pub fn feed_encoder_stream_bytes(
&mut self,
stream_id: u64,
bytes: &[u8],
) -> Result<QpackInstructionStreamOutcome, H3NativeError> {
self.ensure_stream_kind(stream_id, H3UniStreamType::QpackEncoder)?;
let mut pos = 0usize;
let mut outcome = QpackInstructionStreamOutcome::default();
while pos < bytes.len() {
let (instruction, consumed) = match qpack_decode_encoder_instruction(&bytes[pos..]) {
Ok(decoded) => decoded,
Err(error) => return self.fail(error),
};
let (inserted, unblocked) = match self.blocked_scheduler.apply_encoder_instruction(
&mut self.context,
&mut self.decoder_feedback,
self.mode,
&instruction,
) {
Ok(result) => result,
Err(error) => {
self.record_error(&error);
return Err(error);
}
};
outcome.record_encoder_result(inserted, unblocked);
pos += consumed;
}
Ok(outcome)
}
pub fn feed_decoder_stream_bytes(
&mut self,
stream_id: u64,
bytes: &[u8],
) -> Result<QpackInstructionStreamOutcome, H3NativeError> {
self.ensure_stream_kind(stream_id, H3UniStreamType::QpackDecoder)?;
let mut pos = 0usize;
let mut outcome = QpackInstructionStreamOutcome::default();
while pos < bytes.len() {
let (instruction, consumed) = match qpack_decode_decoder_instruction(&bytes[pos..]) {
Ok(decoded) => decoded,
Err(error) => return self.fail(error),
};
let unblocked = match self.blocked_scheduler.apply_decoder_instruction(
&mut self.decoder_feedback,
&mut self.context,
self.mode,
&instruction,
) {
Ok(result) => result,
Err(error) => {
self.record_error(&error);
return Err(error);
}
};
outcome.record_decoder_result(unblocked);
pos += consumed;
}
Ok(outcome)
}
pub fn feed_instruction_stream_bytes(
&mut self,
stream_id: u64,
kind: H3UniStreamType,
bytes: &[u8],
) -> Result<QpackInstructionStreamOutcome, H3NativeError> {
match kind {
H3UniStreamType::QpackEncoder => self.feed_encoder_stream_bytes(stream_id, bytes),
H3UniStreamType::QpackDecoder => self.feed_decoder_stream_bytes(stream_id, bytes),
H3UniStreamType::Control | H3UniStreamType::Push | H3UniStreamType::Unknown(_) => self
.fail(H3NativeError::StreamProtocol(
"qpack instruction stream requires qpack stream type",
)),
}
}
pub fn submit_field_section(
&mut self,
stream_id: u64,
field_section: &[u8],
) -> Result<QpackBlockedStreamStatus, H3NativeError> {
self.blocked_scheduler.submit_field_section(
&mut self.context,
&mut self.decoder_feedback,
self.mode,
stream_id,
field_section,
)
}
pub fn submit_received_field_section(
&mut self,
stream_id: u64,
field_section: &[u8],
) -> Result<QpackBlockedStreamStatus, H3NativeError> {
self.blocked_scheduler.submit_received_field_section(
&mut self.context,
&mut self.decoder_feedback,
self.mode,
stream_id,
field_section,
)
}
pub fn cancel_stream(&mut self, stream_id: u64) -> Result<(), H3NativeError> {
self.blocked_scheduler.cancel_stream(
&mut self.decoder_feedback,
&mut self.context,
self.mode,
stream_id,
)
}
fn ensure_stream_kind(
&mut self,
stream_id: u64,
expected: H3UniStreamType,
) -> Result<(), H3NativeError> {
match self.registered_stream_kind(stream_id) {
Some(actual) if actual == expected => Ok(()),
Some(_) => self.fail(H3NativeError::StreamProtocol(
"qpack instruction type does not match registered stream",
)),
None => self.fail(H3NativeError::StreamProtocol(
"unknown qpack instruction stream",
)),
}
}
fn registered_stream_kind(&self, stream_id: u64) -> Option<H3UniStreamType> {
if self.encoder_stream_id == Some(stream_id) {
return Some(H3UniStreamType::QpackEncoder);
}
if self.decoder_stream_id == Some(stream_id) {
return Some(H3UniStreamType::QpackDecoder);
}
None
}
fn fail<T>(&mut self, error: H3NativeError) -> Result<T, H3NativeError> {
self.record_error(&error);
Err(error)
}
fn record_error(&mut self, error: &H3NativeError) {
if self.first_failure.is_none() {
self.first_failure = Some(error.clone());
}
}
}
fn qpack_plan_dynamic_references(plan: &[QpackFieldPlan]) -> Vec<u64> {
let mut references = BTreeSet::new();
for field in plan {
match field {
QpackFieldPlan::DynamicIndex(index) => {
references.insert(*index);
}
QpackFieldPlan::DynamicNameLiteral { name_index, .. } => {
references.insert(*name_index);
}
QpackFieldPlan::StaticIndex(_) | QpackFieldPlan::Literal { .. } => {}
}
}
references.into_iter().collect()
}
pub fn qpack_apply_encoder_instruction(
context: &mut QpackContext,
mode: H3QpackMode,
instruction: &QpackEncoderInstruction,
) -> Result<Option<u64>, H3NativeError> {
if mode != H3QpackMode::DynamicTableAllowed {
return Err(H3NativeError::QpackPolicy(
"encoder instructions require dynamic qpack mode",
));
}
match instruction {
QpackEncoderInstruction::SetDynamicTableCapacity { capacity } => {
let capacity: usize = (*capacity).try_into().map_err(|_| {
H3NativeError::InvalidFrame(
"qpack dynamic table capacity exceeds addressable range",
)
})?;
context
.set_dynamic_table_capacity(capacity)
.map_err(qpack_capacity_error)?;
Ok(None)
}
QpackEncoderInstruction::InsertWithNameReference { name, value } => {
let name = match name {
QpackInstructionNameRef::Static(index) => qpack_static_name(*index)
.ok_or(H3NativeError::InvalidFrame(
"unknown static qpack name index",
))?
.to_string(),
QpackInstructionNameRef::Dynamic(index) => context
.dynamic_table()
.get_by_relative_index(*index)
.ok_or(H3NativeError::InvalidFrame(
"unknown dynamic qpack name index",
))?
.name()
.to_string(),
};
context
.insert_dynamic_entry(name, value.clone())
.map(Some)
.map_err(qpack_insert_error)
}
QpackEncoderInstruction::InsertWithoutNameReference { name, value } => context
.insert_dynamic_entry(name.clone(), value.clone())
.map(Some)
.map_err(qpack_insert_error),
QpackEncoderInstruction::Duplicate { index } => {
let entry = context
.dynamic_table()
.get_by_relative_index(*index)
.ok_or(H3NativeError::InvalidFrame(
"unknown dynamic qpack duplicate index",
))?;
let name = entry.name().to_string();
let value = entry.value().to_string();
context
.insert_dynamic_entry(name, value)
.map(Some)
.map_err(qpack_insert_error)
}
}
}
fn qpack_capacity_error(err: &'static str) -> H3NativeError {
match err {
"capacity exceeds peer limit" => {
H3NativeError::QpackPolicy("qpack dynamic table capacity exceeds peer limit")
}
"cannot reduce table capacity while entries are referenced" => H3NativeError::InvalidFrame(
"qpack dynamic table capacity shrink blocked by referenced entries",
),
_ => H3NativeError::InvalidFrame("qpack dynamic table capacity update failed"),
}
}
fn qpack_insert_error(err: &'static str) -> H3NativeError {
match err {
"entry larger than table capacity" => {
H3NativeError::InvalidFrame("qpack dynamic table entry exceeds capacity")
}
"cannot evict enough space (all entries referenced)" => {
H3NativeError::InvalidFrame("qpack dynamic table insert blocked by referenced entries")
}
_ => H3NativeError::InvalidFrame("qpack dynamic table insert failed"),
}
}
#[doc(hidden)]
pub fn fuzz_parse_status_code(value: &str) -> Result<u16, H3NativeError> {
parse_status_code(value)
}
#[doc(hidden)]
pub fn fuzz_qpack_decode_required_insert_count(
encoded_insert_count: u64,
total_inserts: u64,
max_table_capacity: usize,
) -> Result<u64, H3NativeError> {
qpack_decode_required_insert_count(encoded_insert_count, total_inserts, max_table_capacity)
}
#[doc(hidden)]
pub fn fuzz_qpack_decode_base(
required_insert_count: u64,
sign: bool,
delta_base: u64,
) -> Result<u64, H3NativeError> {
qpack_decode_base(required_insert_count, sign, delta_base)
}
fn qpack_relative_to_absolute(
base: u64,
relative_index: u64,
is_post_base: bool,
) -> Result<u64, H3NativeError> {
if is_post_base {
base.checked_add(relative_index)
.ok_or(H3NativeError::InvalidFrame(
"dynamic qpack post-base index overflow",
))
} else {
let plus_one = relative_index
.checked_add(1)
.ok_or(H3NativeError::InvalidFrame(
"dynamic qpack relative index +1 overflow (H3_QPACK_DECODER_STREAM_ERROR)",
))?;
base.checked_sub(plus_one)
.ok_or(H3NativeError::InvalidFrame(
"dynamic qpack relative index exceeds base (H3_QPACK_DECODER_STREAM_ERROR)",
))
}
}
fn qpack_absolute_to_relative(base: u64, absolute_index: u64) -> Result<u64, H3NativeError> {
let next = absolute_index
.checked_add(1)
.ok_or(H3NativeError::InvalidFrame(
"dynamic qpack absolute index overflow",
))?;
base.checked_sub(next).ok_or(H3NativeError::InvalidFrame(
"dynamic qpack absolute index exceeds base",
))
}
fn qpack_decode_field_section_with_context(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
) -> Result<Vec<QpackFieldPlan>, H3NativeError> {
let mut pos = 0usize;
let first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
pos += 1;
let (encoded_insert_count, ric_extra) = qpack_decode_prefixed_int(first, 8, &input[pos..])?;
pos += ric_extra;
let second = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
pos += 1;
let sign = (second & 0x80) != 0;
let (delta_base, db_extra) = qpack_decode_prefixed_int(second, 7, &input[pos..])?;
pos += db_extra;
let dynamic_base = match mode {
H3QpackMode::StaticOnly => {
if encoded_insert_count != 0 {
return Err(H3NativeError::QpackPolicy(
"required insert count must be zero in static-only mode",
));
}
if sign || delta_base != 0 {
return Err(H3NativeError::QpackPolicy(
"base must be zero in static-only mode",
));
}
None
}
H3QpackMode::DynamicTableAllowed => {
if encoded_insert_count > 65536 {
return Err(H3NativeError::QpackPolicy(
"required insert count exceeds reasonable limit",
));
}
if let Some(context) = qpack_context {
let total_inserts = context.dynamic_table().insertion_counter();
let required_insert_count = qpack_decode_required_insert_count(
encoded_insert_count,
total_inserts,
context.max_table_capacity,
)?;
if required_insert_count > total_inserts {
return Err(H3NativeError::QpackPolicy(
"required insert count exceeds dynamic table state",
));
}
let base = qpack_decode_base(required_insert_count, sign, delta_base)?;
if base > total_inserts {
return Err(H3NativeError::InvalidFrame(
"dynamic qpack base exceeds dynamic table state",
));
}
Some(base)
} else {
if encoded_insert_count != 0 || sign || delta_base != 0 {
return Err(H3NativeError::InvalidFrame(
"dynamic table context required",
));
}
None
}
}
};
let mut out = Vec::new();
while pos < input.len() {
let b = input[pos];
if (b & 0x80) != 0 {
let is_static = (b & 0x40) != 0;
let (index, extra) = qpack_decode_prefixed_int(b, 6, &input[pos + 1..])?;
pos += 1 + extra;
if !is_static && mode == H3QpackMode::StaticOnly {
return Err(H3NativeError::QpackPolicy(
"dynamic qpack index references not allowed in static-only mode",
));
}
if is_static {
if qpack_static_entry(index).is_none() {
return Err(H3NativeError::InvalidFrame("unknown static qpack index"));
}
out.push(QpackFieldPlan::StaticIndex(index));
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
} else {
let base = dynamic_base.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
let absolute_index = qpack_relative_to_absolute(base, index, false)?;
out.push(QpackFieldPlan::DynamicIndex(absolute_index));
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
}
continue;
}
if (b & 0x40) != 0 {
let is_static = (b & 0x10) != 0;
let (name_index, extra) = qpack_decode_prefixed_int(b, 4, &input[pos + 1..])?;
pos += 1 + extra;
if !is_static && mode == H3QpackMode::StaticOnly {
return Err(H3NativeError::QpackPolicy(
"dynamic qpack name references not allowed in static-only mode",
));
}
let value_first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
let (value, value_extra) = qpack_decode_string(value_first, 7, &input[pos + 1..])?;
pos += 1 + value_extra;
if is_static {
let name = qpack_static_name(name_index).ok_or(H3NativeError::InvalidFrame(
"unknown static qpack name index",
))?;
out.push(QpackFieldPlan::Literal {
name: name.to_string(),
value,
});
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
} else {
let base = dynamic_base.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
let absolute_name_index = qpack_relative_to_absolute(base, name_index, false)?;
out.push(QpackFieldPlan::DynamicNameLiteral {
name_index: absolute_name_index,
value,
});
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
}
continue;
}
if (b & 0x20) != 0 {
let (name, name_extra) = qpack_decode_string(b, 3, &input[pos + 1..])?;
pos += 1 + name_extra;
let value_first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
let (value, value_extra) = qpack_decode_string(value_first, 7, &input[pos + 1..])?;
pos += 1 + value_extra;
out.push(QpackFieldPlan::Literal { name, value });
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
continue;
}
if mode == H3QpackMode::StaticOnly {
return Err(H3NativeError::QpackPolicy(
"post-base/dynamic qpack line representations not allowed in static-only mode",
));
}
let base = dynamic_base.ok_or(H3NativeError::InvalidFrame(
"dynamic table context required",
))?;
if (b & 0x10) != 0 {
let (index, extra) = qpack_decode_prefixed_int(b, 4, &input[pos + 1..])?;
pos += 1 + extra;
let absolute_index = qpack_relative_to_absolute(base, index, true)?;
out.push(QpackFieldPlan::DynamicIndex(absolute_index));
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
continue;
}
let (name_index, extra) = qpack_decode_prefixed_int(b, 3, &input[pos + 1..])?;
pos += 1 + extra;
let value_first = *input.get(pos).ok_or(H3NativeError::UnexpectedEof)?;
let (value, value_extra) = qpack_decode_string(value_first, 7, &input[pos + 1..])?;
pos += 1 + value_extra;
let absolute_name_index = qpack_relative_to_absolute(base, name_index, true)?;
out.push(QpackFieldPlan::DynamicNameLiteral {
name_index: absolute_name_index,
value,
});
if out.len() > QPACK_MAX_DECODED_HEADERS {
return Err(H3NativeError::QpackPolicy(
"decoded header count exceeds safety limit",
));
}
}
Ok(out)
}
pub fn qpack_encode_request_field_section(head: &H3RequestHead) -> Result<Vec<u8>, H3NativeError> {
let plan = qpack_static_plan_for_request(head);
qpack_encode_field_section(&plan)
}
pub fn qpack_encode_response_field_section(
head: &H3ResponseHead,
) -> Result<Vec<u8>, H3NativeError> {
let plan = qpack_static_plan_for_response(head);
qpack_encode_field_section(&plan)
}
pub fn qpack_plan_to_header_fields(
plan: &[QpackFieldPlan],
qpack_context: Option<&QpackContext>,
) -> Result<Vec<(String, String)>, H3NativeError> {
let mut out = Vec::with_capacity(plan.len());
for field in plan {
match field {
QpackFieldPlan::StaticIndex(index) => {
let (name, value) = qpack_static_entry(*index)
.ok_or(H3NativeError::InvalidFrame("unknown static qpack index"))?;
out.push((name.to_string(), value.to_string()));
}
QpackFieldPlan::DynamicIndex(index) => {
if let Some(context) = qpack_context {
let (name, value) = qpack_dynamic_entry(context.dynamic_table(), *index)
.ok_or(H3NativeError::InvalidFrame("unknown dynamic qpack index"))?;
out.push((name.to_string(), value.to_string()));
} else {
return Err(H3NativeError::InvalidFrame(
"dynamic table context required",
));
}
}
QpackFieldPlan::DynamicNameLiteral { name_index, value } => {
if let Some(context) = qpack_context {
let name = qpack_dynamic_name(context.dynamic_table(), *name_index).ok_or(
H3NativeError::InvalidFrame("unknown dynamic qpack name index"),
)?;
out.push((name.to_string(), value.clone()));
} else {
return Err(H3NativeError::InvalidFrame(
"dynamic table context required",
));
}
}
QpackFieldPlan::Literal { name, value } => {
out.push((name.clone(), value.clone()));
}
}
}
Ok(out)
}
fn decoded_field_section_size(fields: &[(String, String)]) -> Result<u64, H3NativeError> {
fields.iter().try_fold(0u64, |acc, (name, value)| {
let field_size = name
.len()
.checked_add(value.len())
.and_then(|size| size.checked_add(32))
.ok_or(H3NativeError::QpackPolicy(
"decoded field section exceeds addressable range",
))?;
let field_size = u64::try_from(field_size).map_err(|_| {
H3NativeError::QpackPolicy("decoded field section exceeds addressable range")
})?;
acc.checked_add(field_size)
.ok_or(H3NativeError::QpackPolicy(
"decoded field section exceeds addressable range",
))
})
}
pub fn qpack_decode_request_field_section(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
) -> Result<H3RequestHead, H3NativeError> {
qpack_decode_request_field_section_with_limit(input, mode, qpack_context, None)
}
pub fn qpack_decode_request_field_section_with_limit(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
max_field_section_size: Option<u64>,
) -> Result<H3RequestHead, H3NativeError> {
let plan = qpack_decode_field_section_with_context(input, mode, qpack_context)?;
let fields = qpack_plan_to_header_fields(&plan, qpack_context)?;
if let Some(max_size) = max_field_section_size {
if decoded_field_section_size(&fields)? > max_size {
return Err(H3NativeError::QpackPolicy(
"decoded field section exceeds maximum size limit",
));
}
}
header_fields_to_request_head(&fields)
}
pub fn qpack_decode_response_field_section(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
) -> Result<H3ResponseHead, H3NativeError> {
qpack_decode_response_field_section_with_limit(input, mode, qpack_context, None)
}
pub fn qpack_decode_response_field_section_with_limit(
input: &[u8],
mode: H3QpackMode,
qpack_context: Option<&QpackContext>,
max_field_section_size: Option<u64>,
) -> Result<H3ResponseHead, H3NativeError> {
let plan = qpack_decode_field_section_with_context(input, mode, qpack_context)?;
let fields = qpack_plan_to_header_fields(&plan, qpack_context)?;
if let Some(max_size) = max_field_section_size {
if decoded_field_section_size(&fields)? > max_size {
return Err(H3NativeError::QpackPolicy(
"decoded field section exceeds maximum size limit",
));
}
}
header_fields_to_response_head(&fields)
}
const H3_FORBIDDEN_HEADER_NAMES: &[&str] = &[
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
];
fn validate_header_name(name: &str) -> Result<(), H3NativeError> {
if name.is_empty() {
return Err(H3NativeError::InvalidFrame("empty header field name"));
}
let bytes = name.as_bytes();
let start = if bytes[0] == b':' {
if bytes.len() == 1 {
return Err(H3NativeError::InvalidFrame("empty header field name"));
}
1
} else {
0
};
for &b in &bytes[start..] {
match 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'A'..=b'Z' => {
return Err(H3NativeError::InvalidFrame(
"header field name must be lowercase in HTTP/3",
));
}
_ => {
return Err(H3NativeError::InvalidFrame(
"header field name contains invalid character",
));
}
}
}
if H3_FORBIDDEN_HEADER_NAMES.contains(&name) {
return Err(H3NativeError::InvalidFrame(
"header field name forbidden in HTTP/3 (RFC 9114 §4.2)",
));
}
Ok(())
}
fn validate_header_value(value: &str) -> Result<(), H3NativeError> {
for &b in value.as_bytes() {
if b == 0 || b == b'\r' || b == b'\n' {
return Err(H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)",
));
}
}
Ok(())
}
fn validate_method_token(method: &str) -> Result<(), H3NativeError> {
if method.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader("empty :method"));
}
for &b in method.as_bytes() {
match b {
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'!'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~' => {}
_ => {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":method must be a valid HTTP token",
));
}
}
}
Ok(())
}
fn validate_scheme_syntax(scheme: &str) -> Result<(), H3NativeError> {
let Some((&first, rest)) = scheme.as_bytes().split_first() else {
return Err(H3NativeError::InvalidRequestPseudoHeader("empty :scheme"));
};
if !first.is_ascii_alphabetic() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":scheme must be a valid URI scheme",
));
}
for &b in rest {
match b {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'+' | b'-' | b'.' => {}
_ => {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":scheme must be a valid URI scheme",
));
}
}
}
Ok(())
}
fn validate_authority_form(authority: &str) -> Result<(), H3NativeError> {
if authority.as_bytes().iter().any(u8::is_ascii_whitespace) {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority must be RFC authority-form without whitespace",
));
}
if authority.contains('@') {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority must not include userinfo",
));
}
if authority.contains(['/', '?', '#']) {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority must not contain path, query, or fragment",
));
}
if authority.starts_with('[') {
let bracket_end = authority
.find(']')
.ok_or(H3NativeError::InvalidRequestPseudoHeader(
":authority has invalid IPv6 literal",
))?;
let literal = &authority[1..bracket_end];
if literal.parse::<Ipv6Addr>().is_err() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority has invalid IPv6 literal",
));
}
let rest = &authority[bracket_end + 1..];
if rest.is_empty() {
return Ok(());
}
let Some(port_str) = rest.strip_prefix(':') else {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority has invalid IPv6 literal",
));
};
if port_str.is_empty() || port_str.parse::<u16>().is_err() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority has invalid port",
));
}
return Ok(());
}
if authority.matches(':').count() > 1 {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority IPv6 literals must use [addr] form",
));
}
if let Some((host, port_str)) = authority.rsplit_once(':') {
if host.is_empty() || port_str.is_empty() || port_str.parse::<u16>().is_err() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":authority has invalid port",
));
}
}
Ok(())
}
fn validate_request_path(method: &str, path: &str) -> Result<(), H3NativeError> {
if path == "*" {
if method != "OPTIONS" {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"asterisk-form :path requires OPTIONS",
));
}
return Ok(());
}
if !path.starts_with('/') {
return Err(H3NativeError::InvalidRequestPseudoHeader(
":path must start with /",
));
}
Ok(())
}
fn parse_status_code(value: &str) -> Result<u16, H3NativeError> {
let bytes = value.as_bytes();
if bytes.len() != 3 || !bytes.iter().all(u8::is_ascii_digit) {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"invalid :status value",
));
}
value
.parse::<u16>()
.map_err(|_| H3NativeError::InvalidResponsePseudoHeader("invalid :status value"))
}
fn header_fields_to_request_head(
fields: &[(String, String)],
) -> Result<H3RequestHead, H3NativeError> {
let mut pseudo = H3PseudoHeaders::default();
let mut headers = Vec::new();
let mut saw_regular_headers = false;
for (name, value) in fields {
validate_header_name(name)?;
validate_header_value(value)?;
if name.starts_with(':') {
if saw_regular_headers {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"request pseudo headers must precede regular headers",
));
}
match name.as_str() {
":method" => {
if pseudo.method.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"duplicate :method",
));
}
pseudo.method = Some(value.clone());
}
":scheme" => {
if pseudo.scheme.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"duplicate :scheme",
));
}
pseudo.scheme = Some(value.clone());
}
":authority" => {
if pseudo.authority.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"duplicate :authority",
));
}
pseudo.authority = Some(value.clone());
}
":path" => {
if pseudo.path.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader("duplicate :path"));
}
pseudo.path = Some(value.clone());
}
":status" => {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"request must not include :status",
));
}
_ => {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"unknown request pseudo header",
));
}
}
} else {
saw_regular_headers = true;
headers.push((name.clone(), value.clone()));
}
}
H3RequestHead::new(pseudo, headers)
}
fn header_fields_to_response_head(
fields: &[(String, String)],
) -> Result<H3ResponseHead, H3NativeError> {
let mut status: Option<u16> = None;
let mut headers = Vec::new();
let mut saw_regular_headers = false;
for (name, value) in fields {
validate_header_name(name)?;
validate_header_value(value)?;
if name.starts_with(':') {
if saw_regular_headers {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"response pseudo headers must precede regular headers",
));
}
match name.as_str() {
":status" => {
if status.is_some() {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"duplicate :status",
));
}
let parsed = parse_status_code(value)?;
status = Some(parsed);
}
_ => {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers",
));
}
}
} else {
saw_regular_headers = true;
headers.push((name.clone(), value.clone()));
}
}
let status = status.ok_or(H3NativeError::InvalidResponsePseudoHeader(
"missing :status",
))?;
H3ResponseHead::new(status, headers)
}
fn qpack_encode_prefixed_int(
out: &mut Vec<u8>,
prefix_bits: u8,
prefix_len: u8,
mut value: u64,
) -> Result<(), H3NativeError> {
if !(1..=8).contains(&prefix_len) {
return Err(H3NativeError::InvalidFrame(
"invalid qpack integer prefix length",
));
}
let max_in_prefix = (1u64 << prefix_len) - 1;
if value < max_in_prefix {
out.push(prefix_bits | (value as u8));
return Ok(());
}
out.push(prefix_bits | (max_in_prefix as u8));
value -= max_in_prefix;
while value >= 128 {
out.push(((value as u8) & 0x7F) | 0x80);
value >>= 7;
}
out.push(value as u8);
Ok(())
}
fn qpack_decode_prefixed_int(
first: u8,
prefix_len: u8,
input: &[u8],
) -> Result<(u64, usize), H3NativeError> {
if !(1..=8).contains(&prefix_len) {
return Err(H3NativeError::InvalidFrame(
"invalid qpack integer prefix length",
));
}
let mask = ((1u16 << prefix_len) - 1) as u8;
let mut value = u64::from(first & mask);
let max_in_prefix = u64::from(mask);
if value < max_in_prefix {
return Ok((value, 0));
}
let mut shift = 0u32;
let mut consumed = 0usize;
loop {
let byte = *input.get(consumed).ok_or(H3NativeError::UnexpectedEof)?;
consumed += 1;
let part = u64::from(byte & 0x7F);
let shifted = part
.checked_shl(shift)
.ok_or(H3NativeError::InvalidFrame("qpack integer overflow"))?;
value = value
.checked_add(shifted)
.ok_or(H3NativeError::InvalidFrame("qpack integer overflow"))?;
if (byte & 0x80) == 0 {
return Ok((value, consumed));
}
shift = shift.saturating_add(7);
if shift > 56 {
return Err(H3NativeError::InvalidFrame("qpack integer overflow"));
}
}
}
fn qpack_encode_string(
out: &mut Vec<u8>,
prefix_bits: u8,
prefix_len: u8,
value: &str,
) -> Result<(), H3NativeError> {
let bytes = value.as_bytes();
let huffman_len = hpack_huffman_encoded_size(bytes);
if huffman_len < bytes.len() {
qpack_encode_prefixed_int(
out,
prefix_bits | (1u8 << prefix_len),
prefix_len,
huffman_len as u64,
)?;
let mut encoded = BytesMut::with_capacity(huffman_len);
hpack_encode_huffman(&mut encoded, bytes);
out.extend_from_slice(&encoded);
} else {
qpack_encode_prefixed_int(out, prefix_bits, prefix_len, bytes.len() as u64)?;
out.extend_from_slice(bytes);
}
Ok(())
}
fn qpack_decode_string(
first: u8,
prefix_len: u8,
input: &[u8],
) -> Result<(String, usize), H3NativeError> {
if prefix_len >= 8 {
return Err(H3NativeError::InvalidFrame(
"qpack string prefix length must be less than 8",
));
}
let huffman_bit = 1u8 << prefix_len;
let (len, extra) = qpack_decode_prefixed_int(first, prefix_len, input)?;
let len: usize = len.try_into().map_err(|_| {
H3NativeError::InvalidFrame("qpack string length exceeds addressable range")
})?;
if input.len().saturating_sub(extra) < len {
return Err(H3NativeError::UnexpectedEof);
}
let bytes = &input[extra..extra + len];
let value = if (first & huffman_bit) != 0 {
let encoded = Bytes::copy_from_slice(bytes);
hpack_decode_huffman(&encoded)
.map_err(|_| H3NativeError::InvalidFrame("invalid qpack huffman string"))?
} else {
std::str::from_utf8(bytes)
.map_err(|_| H3NativeError::InvalidFrame("qpack string is not valid utf-8"))?
.to_string()
};
Ok((value, extra + len))
}
fn qpack_static_name(index: u64) -> Option<&'static str> {
qpack_static_entry(index).map(|(name, _)| name)
}
fn qpack_static_entry(index: u64) -> Option<(&'static str, &'static str)> {
match index {
0 => Some((":authority", "")),
1 => Some((":path", "/")),
2 => Some(("age", "0")),
3 => Some(("content-disposition", "")),
4 => Some(("content-length", "0")),
5 => Some(("cookie", "")),
6 => Some(("date", "")),
7 => Some(("etag", "")),
8 => Some(("if-modified-since", "")),
9 => Some(("if-none-match", "")),
10 => Some(("last-modified", "")),
11 => Some(("link", "")),
12 => Some(("location", "")),
13 => Some(("referer", "")),
14 => Some(("set-cookie", "")),
15 => Some((":method", "CONNECT")),
16 => Some((":method", "DELETE")),
17 => Some((":method", "GET")),
18 => Some((":method", "HEAD")),
19 => Some((":method", "OPTIONS")),
20 => Some((":method", "POST")),
21 => Some((":method", "PUT")),
22 => Some((":scheme", "http")),
23 => Some((":scheme", "https")),
24 => Some((":status", "103")),
25 => Some((":status", "200")),
26 => Some((":status", "304")),
27 => Some((":status", "404")),
28 => Some((":status", "503")),
29 => Some(("accept", "*/*")),
30 => Some(("accept", "application/dns-message")),
31 => Some(("accept-encoding", "gzip, deflate, br")),
32 => Some(("accept-ranges", "bytes")),
33 => Some(("access-control-allow-headers", "cache-control")),
34 => Some(("access-control-allow-headers", "content-type")),
35 => Some(("access-control-allow-origin", "*")),
36 => Some(("cache-control", "max-age=0")),
37 => Some(("cache-control", "max-age=2592000")),
38 => Some(("cache-control", "max-age=604800")),
39 => Some(("cache-control", "no-cache")),
40 => Some(("cache-control", "no-store")),
41 => Some(("cache-control", "public, max-age=31536000")),
42 => Some(("content-encoding", "br")),
43 => Some(("content-encoding", "gzip")),
44 => Some(("content-type", "application/dns-message")),
45 => Some(("content-type", "application/javascript")),
46 => Some(("content-type", "application/json")),
47 => Some(("content-type", "application/x-www-form-urlencoded")),
48 => Some(("content-type", "image/gif")),
49 => Some(("content-type", "image/jpeg")),
50 => Some(("content-type", "image/png")),
51 => Some(("content-type", "text/css")),
52 => Some(("content-type", "text/html; charset=utf-8")),
53 => Some(("content-type", "text/plain")),
54 => Some(("content-type", "text/plain;charset=utf-8")),
55 => Some(("range", "bytes=0-")),
56 => Some(("strict-transport-security", "max-age=31536000")),
57 => Some((
"strict-transport-security",
"max-age=31536000; includesubdomains",
)),
58 => Some((
"strict-transport-security",
"max-age=31536000; includesubdomains; preload",
)),
59 => Some(("vary", "accept-encoding")),
60 => Some(("vary", "origin")),
61 => Some(("x-content-type-options", "nosniff")),
62 => Some(("x-xss-protection", "1; mode=block")),
63 => Some((":status", "100")),
64 => Some((":status", "204")),
65 => Some((":status", "206")),
66 => Some((":status", "302")),
67 => Some((":status", "400")),
68 => Some((":status", "403")),
69 => Some((":status", "421")),
70 => Some((":status", "425")),
71 => Some((":status", "500")),
72 => Some(("accept-language", "")),
73 => Some(("access-control-allow-credentials", "FALSE")),
74 => Some(("access-control-allow-credentials", "TRUE")),
75 => Some(("access-control-allow-headers", "*")),
76 => Some(("access-control-allow-methods", "get")),
77 => Some(("access-control-allow-methods", "get, post, options")),
78 => Some(("access-control-allow-methods", "options")),
79 => Some(("access-control-expose-headers", "content-length")),
80 => Some(("access-control-request-headers", "content-type")),
81 => Some(("access-control-request-method", "get")),
82 => Some(("access-control-request-method", "post")),
83 => Some(("alt-svc", "clear")),
84 => Some(("authorization", "")),
85 => Some((
"content-security-policy",
"script-src 'none'; object-src 'none'; base-uri 'none'",
)),
86 => Some(("early-data", "1")),
87 => Some(("expect-ct", "")),
88 => Some(("forwarded", "")),
89 => Some(("if-range", "")),
90 => Some(("origin", "")),
91 => Some(("purpose", "prefetch")),
92 => Some(("server", "")),
93 => Some(("timing-allow-origin", "*")),
94 => Some(("upgrade-insecure-requests", "1")),
95 => Some(("user-agent", "")),
96 => Some(("x-forwarded-for", "")),
97 => Some(("x-frame-options", "deny")),
98 => Some(("x-frame-options", "sameorigin")),
_ => None,
}
}
fn qpack_static_method_index(method: &str) -> Option<u64> {
match method {
"CONNECT" => Some(15),
"DELETE" => Some(16),
"GET" => Some(17),
"HEAD" => Some(18),
"OPTIONS" => Some(19),
"POST" => Some(20),
"PUT" => Some(21),
_ => None,
}
}
fn qpack_static_scheme_index(scheme: &str) -> Option<u64> {
match scheme {
"http" => Some(22),
"https" => Some(23),
_ => None,
}
}
fn qpack_static_status_index(status: u16) -> Option<u64> {
match status {
103 => Some(24),
200 => Some(25),
304 => Some(26),
404 => Some(27),
503 => Some(28),
100 => Some(63),
204 => Some(64),
206 => Some(65),
302 => Some(66),
400 => Some(67),
403 => Some(68),
421 => Some(69),
425 => Some(70),
500 => Some(71),
_ => None,
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct H3RequestStreamState {
header_blocks_seen: u8,
saw_data: bool,
end_stream: bool,
}
impl H3RequestStreamState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn on_frame(&mut self, frame: &H3Frame) -> Result<(), H3NativeError> {
if self.end_stream {
return Err(H3NativeError::ControlProtocol(
"request stream already finished",
));
}
match frame {
H3Frame::Headers(_) => {
if self.header_blocks_seen == 0 {
self.header_blocks_seen = 1;
return Ok(());
}
if self.header_blocks_seen == 1 {
self.header_blocks_seen = 2;
return Ok(());
}
Err(H3NativeError::ControlProtocol(
"invalid HEADERS ordering on request stream",
))
}
H3Frame::Data(_) => {
if self.header_blocks_seen == 0 {
return Err(H3NativeError::ControlProtocol(
"DATA before initial HEADERS on request stream",
));
}
if self.header_blocks_seen > 1 {
return Err(H3NativeError::ControlProtocol(
"DATA not allowed after trailing HEADERS",
));
}
self.saw_data = true;
Ok(())
}
H3Frame::Datagram { .. } => {
if self.header_blocks_seen == 0 {
return Err(H3NativeError::ControlProtocol(
"DATAGRAM before initial HEADERS on request stream",
));
}
Ok(())
}
H3Frame::PushPromise { .. } | H3Frame::Unknown { .. } => Ok(()),
H3Frame::Settings(_)
| H3Frame::CancelPush(_)
| H3Frame::Goaway(_)
| H3Frame::MaxPushId(_) => Err(H3NativeError::ControlProtocol(
"control frames are not valid on request streams",
)),
}
}
pub fn mark_end_stream(&mut self) -> Result<(), H3NativeError> {
if self.header_blocks_seen == 0 {
return Err(H3NativeError::ControlProtocol(
"request stream ended before initial HEADERS",
));
}
self.end_stream = true;
Ok(())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct H3PushStreamState {
push_id: Option<u64>,
response: H3RequestStreamState,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct H3ConnectionState {
config: H3ConnectionConfig,
control: H3ControlState,
request_streams: BTreeMap<u64, H3RequestStreamState>,
finished_request_streams: BTreeSet<u64>,
max_contiguous_finished_request_stream_id: Option<u64>,
push_streams: BTreeMap<u64, H3PushStreamState>,
used_push_ids: BTreeSet<u64>,
uni_stream_types: BTreeMap<u64, H3UniStreamType>,
control_stream_id: Option<u64>,
qpack_encoder_stream_id: Option<u64>,
qpack_decoder_stream_id: Option<u64>,
goaway_id: Option<u64>,
}
impl H3ConnectionState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn new_client() -> Self {
Self::new()
}
#[must_use]
pub fn new_server() -> Self {
Self::with_config(H3ConnectionConfig {
endpoint_role: H3EndpointRole::Server,
..H3ConnectionConfig::default()
})
}
#[must_use]
pub fn with_config(config: H3ConnectionConfig) -> Self {
Self {
config,
control: H3ControlState::default(),
request_streams: BTreeMap::new(),
finished_request_streams: BTreeSet::new(),
max_contiguous_finished_request_stream_id: None,
push_streams: BTreeMap::new(),
used_push_ids: BTreeSet::new(),
uni_stream_types: BTreeMap::new(),
control_stream_id: None,
qpack_encoder_stream_id: None,
qpack_decoder_stream_id: None,
goaway_id: None,
}
}
fn is_request_stream_finished(&self, stream_id: u64) -> bool {
if let Some(max_contig) = self.max_contiguous_finished_request_stream_id {
if stream_id <= max_contig {
return true;
}
}
self.finished_request_streams.contains(&stream_id)
}
pub fn on_control_frame(&mut self, frame: &H3Frame) -> Result<(), H3NativeError> {
if let H3Frame::Settings(settings) = frame {
self.validate_qpack_settings(settings)?;
}
self.control.on_remote_control_frame(frame)?;
if self.config.endpoint_role == H3EndpointRole::Client
&& matches!(frame, H3Frame::MaxPushId(_))
{
return Err(H3NativeError::ControlProtocol(
"client must not receive MAX_PUSH_ID",
));
}
if let H3Frame::Goaway(id) = frame {
if self.config.endpoint_role == H3EndpointRole::Client
&& !is_client_initiated_bidirectional_stream_id(*id)
{
return Err(H3NativeError::ControlProtocol(
"GOAWAY id must be a client-initiated bidirectional stream id",
));
}
if self.goaway_id.is_some_and(|prev| *id > prev) {
return Err(H3NativeError::ControlProtocol(
"GOAWAY id must not increase",
));
}
self.goaway_id = Some(*id);
}
Ok(())
}
pub fn on_request_stream_frame(
&mut self,
stream_id: u64,
frame: &H3Frame,
) -> Result<(), H3NativeError> {
if !is_client_initiated_bidirectional_stream_id(stream_id) {
return Err(H3NativeError::StreamProtocol(
"request stream id must be client-initiated bidirectional",
));
}
if self.uni_stream_types.contains_key(&stream_id) {
return Err(H3NativeError::StreamProtocol(
"request stream id is registered as unidirectional",
));
}
if self.is_request_stream_finished(stream_id) {
return Err(H3NativeError::ControlProtocol(
"request stream already finished",
));
}
if self.config.endpoint_role == H3EndpointRole::Client
&& let Some(goaway_id) = self.goaway_id
&& stream_id >= goaway_id
{
return Err(H3NativeError::ControlProtocol(
"request stream id rejected after GOAWAY",
));
}
let request_stream_exists = self.request_streams.contains_key(&stream_id);
if matches!(frame, H3Frame::Unknown { .. }) && !request_stream_exists {
return Ok(());
}
if let Some(limit) = self.config.max_concurrent_request_streams
&& !request_stream_exists
&& self.request_streams.len() as u64 >= limit
{
return Err(H3NativeError::ConcurrentStreamLimitExceeded {
active: self.request_streams.len() as u64,
limit,
});
}
if let Some(state) = self.request_streams.get_mut(&stream_id) {
return state.on_frame(frame);
}
let mut state = H3RequestStreamState::new();
state.on_frame(frame)?;
self.request_streams.insert(stream_id, state);
Ok(())
}
#[must_use]
pub fn active_request_stream_count(&self) -> u64 {
self.request_streams.len() as u64
}
pub fn set_max_concurrent_request_streams(&mut self, limit: Option<u64>) {
self.config.max_concurrent_request_streams = limit;
}
pub fn finish_request_stream(&mut self, stream_id: u64) -> Result<(), H3NativeError> {
if self.is_request_stream_finished(stream_id) {
return Err(H3NativeError::ControlProtocol(
"request stream already finished",
));
}
let state =
self.request_streams
.get_mut(&stream_id)
.ok_or(H3NativeError::ControlProtocol(
"unknown request stream on finish",
))?;
state.mark_end_stream()?;
self.request_streams.remove(&stream_id);
self.finished_request_streams.insert(stream_id);
let mut next_expected = self
.max_contiguous_finished_request_stream_id
.map_or(0, |id| id + 4);
while self.finished_request_streams.remove(&next_expected) {
self.max_contiguous_finished_request_stream_id = Some(next_expected);
next_expected += 4;
}
Ok(())
}
pub fn on_push_stream_header(
&mut self,
stream_id: u64,
push_id: u64,
) -> Result<(), H3NativeError> {
match self.uni_stream_types.get(&stream_id) {
Some(H3UniStreamType::Push) => {}
Some(_) => {
return Err(H3NativeError::StreamProtocol(
"push stream header requires a push stream",
));
}
None => {
return Err(H3NativeError::StreamProtocol(
"unknown unidirectional stream",
));
}
}
let state = self
.push_streams
.get_mut(&stream_id)
.ok_or(H3NativeError::StreamProtocol("unknown push stream"))?;
if state.push_id.is_some() {
return Err(H3NativeError::StreamProtocol(
"push stream header already received",
));
}
if !self.used_push_ids.insert(push_id) {
return Err(H3NativeError::StreamProtocol(
"duplicate push id in push stream header",
));
}
state.push_id = Some(push_id);
Ok(())
}
pub fn on_remote_uni_stream_type(
&mut self,
stream_id: u64,
stream_type: u64,
) -> Result<H3UniStreamType, H3NativeError> {
if !is_unidirectional_stream_id(stream_id) {
return Err(H3NativeError::StreamProtocol(
"unidirectional stream type requires unidirectional stream id",
));
}
if !is_peer_initiated_unidirectional_stream_id(stream_id, self.config.endpoint_role) {
return Err(H3NativeError::StreamProtocol(
"unidirectional stream type requires peer-initiated unidirectional stream id",
));
}
let kind = H3UniStreamType::decode(stream_type);
if self.uni_stream_types.contains_key(&stream_id) {
return Err(H3NativeError::StreamProtocol(
"unidirectional stream type already set",
));
}
match kind {
H3UniStreamType::Control => {
if self.control_stream_id.is_some() {
return Err(H3NativeError::ControlProtocol(
"duplicate remote control stream",
));
}
self.control_stream_id = Some(stream_id);
}
H3UniStreamType::QpackEncoder => {
if self.qpack_encoder_stream_id.is_some() {
return Err(H3NativeError::StreamProtocol(
"duplicate remote qpack encoder stream",
));
}
self.qpack_encoder_stream_id = Some(stream_id);
}
H3UniStreamType::QpackDecoder => {
if self.qpack_decoder_stream_id.is_some() {
return Err(H3NativeError::StreamProtocol(
"duplicate remote qpack decoder stream",
));
}
self.qpack_decoder_stream_id = Some(stream_id);
}
H3UniStreamType::Push => {
if self.config.endpoint_role != H3EndpointRole::Client {
return Err(H3NativeError::StreamProtocol(
"server endpoint must not receive push streams",
));
}
self.push_streams.entry(stream_id).or_default();
}
H3UniStreamType::Unknown(_) => {
}
}
self.uni_stream_types.insert(stream_id, kind);
Ok(kind)
}
pub fn on_uni_stream_frame(
&mut self,
stream_id: u64,
frame: &H3Frame,
) -> Result<(), H3NativeError> {
let kind =
self.uni_stream_types
.get(&stream_id)
.copied()
.ok_or(H3NativeError::StreamProtocol(
"unknown unidirectional stream",
))?;
match kind {
H3UniStreamType::Control => self.on_control_frame(frame),
H3UniStreamType::Push => {
let state = self.push_streams.entry(stream_id).or_default();
if state.push_id.is_none() {
return Err(H3NativeError::StreamProtocol("push stream missing push id"));
}
state.response.on_frame(frame)
}
H3UniStreamType::QpackEncoder | H3UniStreamType::QpackDecoder => Err(
H3NativeError::StreamProtocol("qpack streams carry instructions, not h3 frames"),
),
H3UniStreamType::Unknown(_) => {
Ok(())
}
}
}
#[must_use]
pub fn remote_uni_stream_type(&self, stream_id: u64) -> Option<H3UniStreamType> {
self.uni_stream_types.get(&stream_id).copied()
}
#[must_use]
pub fn qpack_encoder_stream_id(&self) -> Option<u64> {
self.qpack_encoder_stream_id
}
#[must_use]
pub fn qpack_decoder_stream_id(&self) -> Option<u64> {
self.qpack_decoder_stream_id
}
pub fn register_qpack_instruction_stream(
&self,
qpack: &mut QpackInstructionStreamState,
stream_id: u64,
) -> Result<H3UniStreamType, H3NativeError> {
qpack.register_from_connection(self, stream_id)
}
pub fn feed_qpack_instruction_stream_bytes(
&self,
qpack: &mut QpackInstructionStreamState,
stream_id: u64,
bytes: &[u8],
) -> Result<QpackInstructionStreamOutcome, H3NativeError> {
let kind = self
.remote_uni_stream_type(stream_id)
.ok_or(H3NativeError::StreamProtocol(
"unknown unidirectional stream",
))?;
qpack.ensure_stream_registered(stream_id, kind)?;
qpack.feed_instruction_stream_bytes(stream_id, kind, bytes)
}
fn validate_qpack_settings(&self, settings: &H3Settings) -> Result<(), H3NativeError> {
if self.config.qpack_mode == H3QpackMode::DynamicTableAllowed {
return Ok(());
}
if settings.qpack_max_table_capacity.unwrap_or(0) > 0 {
return Err(H3NativeError::QpackPolicy(
"dynamic qpack table disabled by policy",
));
}
if settings.qpack_blocked_streams.unwrap_or(0) > 0 {
return Err(H3NativeError::QpackPolicy(
"qpack blocked streams must be zero in static-only mode",
));
}
Ok(())
}
#[must_use]
pub fn goaway_id(&self) -> Option<u64> {
self.goaway_id
}
#[must_use]
pub fn qpack_mode(&self) -> H3QpackMode {
self.config.qpack_mode
}
#[must_use]
pub fn endpoint_role(&self) -> H3EndpointRole {
self.config.endpoint_role
}
}
fn is_unidirectional_stream_id(stream_id: u64) -> bool {
(stream_id & 0x2) != 0
}
fn is_client_initiated_bidirectional_stream_id(stream_id: u64) -> bool {
stream_id.trailing_zeros() >= 2
}
fn is_client_initiated_unidirectional_stream_id(stream_id: u64) -> bool {
(stream_id & 0x3) == 0x2
}
fn is_server_initiated_unidirectional_stream_id(stream_id: u64) -> bool {
(stream_id & 0x3) == 0x3
}
fn is_peer_initiated_unidirectional_stream_id(
stream_id: u64,
endpoint_role: H3EndpointRole,
) -> bool {
match endpoint_role {
H3EndpointRole::Client => is_server_initiated_unidirectional_stream_id(stream_id),
H3EndpointRole::Server => is_client_initiated_unidirectional_stream_id(stream_id),
}
}
pub fn validate_request_pseudo_headers(headers: &H3PseudoHeaders) -> Result<(), H3NativeError> {
validate_request_pseudo_headers_with_settings(headers, false)
}
pub fn validate_request_pseudo_headers_with_settings(
headers: &H3PseudoHeaders,
enable_connect_protocol: bool,
) -> Result<(), H3NativeError> {
let method = headers
.method
.as_deref()
.ok_or(H3NativeError::InvalidRequestPseudoHeader("missing :method"))?;
validate_header_value(method)?;
validate_method_token(method)?;
if headers.status.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"request must not include :status",
));
}
if method == "CONNECT" {
let authority =
headers
.authority
.as_deref()
.ok_or(H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request missing :authority",
))?;
validate_header_value(authority)?;
if authority.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request missing :authority",
));
}
validate_authority_form(authority)?;
if enable_connect_protocol {
if let Some(protocol) = &headers.protocol {
validate_header_value(protocol)?;
if protocol.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"extended CONNECT request :protocol must not be empty",
));
}
} else {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"extended CONNECT request missing :protocol",
));
}
if let Some(scheme) = &headers.scheme {
validate_header_value(scheme)?;
validate_scheme_syntax(scheme)?;
}
if let Some(path) = &headers.path {
validate_header_value(path)?;
validate_request_path("CONNECT", path)?;
}
} else {
if headers.scheme.is_some() || headers.path.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request must not include :scheme or :path",
));
}
if headers.protocol.is_some() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request must not include :protocol (extended CONNECT not enabled)",
));
}
}
return Ok(());
}
let scheme = headers
.scheme
.as_deref()
.ok_or(H3NativeError::InvalidRequestPseudoHeader("missing :scheme"))?;
validate_header_value(scheme)?;
if scheme.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader("empty :scheme"));
}
validate_scheme_syntax(scheme)?;
let path = headers
.path
.as_deref()
.ok_or(H3NativeError::InvalidRequestPseudoHeader("missing :path"))?;
validate_header_value(path)?;
if path.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader("empty :path"));
}
validate_request_path(method, path)?;
if let Some(authority) = headers.authority.as_deref() {
validate_header_value(authority)?;
if authority.is_empty() {
return Err(H3NativeError::InvalidRequestPseudoHeader(
"empty :authority",
));
}
validate_authority_form(authority)?;
}
Ok(())
}
pub fn validate_response_pseudo_headers(headers: &H3PseudoHeaders) -> Result<(), H3NativeError> {
let status = headers
.status
.ok_or(H3NativeError::InvalidResponsePseudoHeader(
"missing :status",
))?;
if !(100..=999).contains(&status) {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"status must be in 100..=999",
));
}
if status == 101 {
return Err(H3NativeError::InvalidResponsePseudoHeader(
"HTTP/3 does not support 101 Switching Protocols",
));
}
if headers.method.is_some()
|| headers.scheme.is_some()
|| headers.authority.is_some()
|| headers.path.is_some()
{
return Err(H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers",
));
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct QpackDynamicEntry {
name: String,
value: String,
size: usize,
reference_count: usize,
insertion_order: u64,
}
impl QpackDynamicEntry {
fn new(name: String, value: String, insertion_order: u64) -> Self {
let size = name.len().saturating_add(value.len()).saturating_add(32);
Self {
name,
value,
size,
reference_count: 0,
insertion_order,
}
}
fn add_reference(&mut self) {
self.reference_count = self.reference_count.saturating_add(1);
}
fn remove_reference(&mut self) {
self.reference_count = self.reference_count.saturating_sub(1);
}
fn is_referenced(&self) -> bool {
self.reference_count > 0
}
pub fn name(&self) -> &str {
&self.name
}
pub fn value(&self) -> &str {
&self.value
}
pub fn insertion_id(&self) -> u64 {
self.insertion_order
}
}
#[derive(Debug)]
pub struct QpackDynamicTable {
entries: Vec<QpackDynamicEntry>,
max_capacity: usize,
current_size: usize,
insertion_counter: u64,
evicted_count: usize,
}
impl QpackDynamicTable {
pub fn new(max_capacity: usize) -> Self {
Self {
entries: Vec::new(),
max_capacity,
current_size: 0,
insertion_counter: 0,
evicted_count: 0,
}
}
pub fn insert(&mut self, name: String, value: String) -> Result<u64, &'static str> {
let entry = QpackDynamicEntry::new(name, value, self.insertion_counter);
let entry_size = entry.size;
if entry_size > self.max_capacity {
return Err("entry larger than table capacity");
}
while self.current_size.saturating_add(entry_size) > self.max_capacity {
if !self.evict_lru_unreferenced() {
return Err("cannot evict enough space (all entries referenced)");
}
}
let insertion_id = self.insertion_counter;
self.entries.push(entry);
self.current_size = self.current_size.saturating_add(entry_size);
self.insertion_counter += 1;
Ok(insertion_id)
}
pub fn set_capacity(&mut self, max_capacity: usize) -> Result<(), &'static str> {
if self.current_size <= max_capacity {
self.max_capacity = max_capacity;
return Ok(());
}
let mut candidates = Vec::new();
let mut freed = 0usize;
for (index, entry) in self.entries.iter().enumerate() {
if entry.is_referenced() {
continue;
}
candidates.push(index);
freed += entry.size;
if self.current_size - freed <= max_capacity {
break;
}
}
if self.current_size - freed > max_capacity {
return Err("cannot reduce table capacity while entries are referenced");
}
self.max_capacity = max_capacity;
for index in candidates.into_iter().rev() {
let evicted = self.entries.remove(index);
self.current_size -= evicted.size;
self.evicted_count += 1;
}
Ok(())
}
fn evict_lru_unreferenced(&mut self) -> bool {
let mut lru_index = None;
let mut lru_insertion_order = u64::MAX;
for (i, entry) in self.entries.iter().enumerate() {
if !entry.is_referenced() && entry.insertion_order < lru_insertion_order {
lru_insertion_order = entry.insertion_order;
lru_index = Some(i);
}
}
if let Some(index) = lru_index {
let evicted = self.entries.remove(index);
self.current_size -= evicted.size;
self.evicted_count += 1;
true
} else {
false
}
}
pub fn reference_entry(&mut self, insertion_id: u64) -> bool {
if let Some(entry) = self
.entries
.iter_mut()
.find(|e| e.insertion_order == insertion_id)
{
entry.add_reference();
true
} else {
false
}
}
pub fn unreference_entry(&mut self, insertion_id: u64) -> bool {
if let Some(entry) = self
.entries
.iter_mut()
.find(|e| e.insertion_order == insertion_id)
{
entry.remove_reference();
true
} else {
false
}
}
pub fn get_by_absolute_index(&self, absolute_index: u64) -> Option<&QpackDynamicEntry> {
self.get_by_insertion_id(absolute_index)
}
pub fn get_by_insertion_id(&self, insertion_id: u64) -> Option<&QpackDynamicEntry> {
if insertion_id >= self.insertion_counter {
return None;
}
self.entries
.iter()
.find(|entry| entry.insertion_order == insertion_id)
}
pub fn get_by_relative_index(&self, relative_index: u64) -> Option<&QpackDynamicEntry> {
let insertion_id = self
.insertion_counter
.checked_sub(1)?
.checked_sub(relative_index)?;
self.get_by_insertion_id(insertion_id)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn size(&self) -> usize {
self.current_size
}
pub fn capacity(&self) -> usize {
self.max_capacity
}
pub fn insertion_counter(&self) -> u64 {
self.insertion_counter
}
pub fn evicted_count(&self) -> usize {
self.evicted_count
}
}
impl Default for QpackDynamicTable {
fn default() -> Self {
Self::new(4096) }
}
pub fn qpack_dynamic_entry(table: &QpackDynamicTable, absolute_index: u64) -> Option<(&str, &str)> {
table
.get_by_absolute_index(absolute_index)
.map(|entry| (entry.name(), entry.value()))
}
pub fn qpack_dynamic_name(table: &QpackDynamicTable, absolute_index: u64) -> Option<&str> {
table
.get_by_absolute_index(absolute_index)
.map(|entry| entry.name())
}
#[derive(Debug)]
pub struct QpackContext {
dynamic_table: QpackDynamicTable,
max_table_capacity: usize,
}
impl QpackContext {
pub fn new(max_table_capacity: usize) -> Self {
Self {
dynamic_table: QpackDynamicTable::new(max_table_capacity),
max_table_capacity,
}
}
pub fn dynamic_table(&self) -> &QpackDynamicTable {
&self.dynamic_table
}
pub fn dynamic_table_mut(&mut self) -> &mut QpackDynamicTable {
&mut self.dynamic_table
}
pub fn max_table_capacity(&self) -> usize {
self.max_table_capacity
}
pub fn set_dynamic_table_capacity(&mut self, capacity: usize) -> Result<(), &'static str> {
if capacity > self.max_table_capacity {
return Err("capacity exceeds peer limit");
}
self.dynamic_table.set_capacity(capacity)
}
pub fn insert_dynamic_entry(
&mut self,
name: String,
value: String,
) -> Result<u64, &'static str> {
self.dynamic_table.insert(name, value)
}
}
impl Default for QpackContext {
fn default() -> Self {
Self::new(4096)
}
}
#[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::*;
fn qpack_entry_by_insertion_id(
table: &QpackDynamicTable,
insertion_id: u64,
) -> Option<(&str, &str)> {
table
.get_by_insertion_id(insertion_id)
.map(|entry| (entry.name(), entry.value()))
}
fn test_config() -> H3ConnectionConfig {
H3ConnectionConfig::default()
}
#[test]
fn settings_roundtrip_and_unknown_preservation() {
let settings = H3Settings {
qpack_max_table_capacity: Some(4096),
max_field_section_size: Some(16384),
qpack_blocked_streams: Some(16),
enable_connect_protocol: Some(true),
h3_datagram: Some(false),
unknown: vec![UnknownSetting {
id: 0xfeed,
value: 7,
}],
};
let mut payload = Vec::new();
settings.encode_payload(&mut payload).expect("encode");
let decoded = H3Settings::decode_payload(&payload).expect("decode");
assert_eq!(decoded, settings);
}
#[test]
fn settings_reject_duplicate_ids() {
let mut payload = Vec::new();
encode_setting(&mut payload, H3_SETTING_MAX_FIELD_SECTION_SIZE, 100).expect("first");
encode_setting(&mut payload, H3_SETTING_MAX_FIELD_SECTION_SIZE, 200).expect("second");
let err = H3Settings::decode_payload(&payload).expect_err("must fail");
assert_eq!(
err,
H3NativeError::DuplicateSetting(H3_SETTING_MAX_FIELD_SECTION_SIZE)
);
}
#[test]
fn settings_decode_large_unique_unknown_setting_set() {
let mut payload = Vec::new();
for id in 0x40u64..0x440 {
encode_setting(&mut payload, id, id ^ 0x55).expect("encode unknown setting");
}
let decoded = H3Settings::decode_payload(&payload).expect("decode large settings set");
assert_eq!(decoded.unknown.len(), 1024);
assert_eq!(
decoded.unknown.first(),
Some(&UnknownSetting {
id: 0x40,
value: 0x15
})
);
assert_eq!(
decoded.unknown.last(),
Some(&UnknownSetting {
id: 0x43f,
value: 0x46a,
})
);
}
#[test]
fn settings_reject_invalid_boolean_values() {
let mut payload = Vec::new();
encode_setting(&mut payload, H3_SETTING_ENABLE_CONNECT_PROTOCOL, 2).expect("encode");
let err = H3Settings::decode_payload(&payload).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidSettingValue(H3_SETTING_ENABLE_CONNECT_PROTOCOL)
);
}
#[test]
fn frame_roundtrip() {
let frame = H3Frame::PushPromise {
push_id: 9,
field_block: vec![1, 2, 3, 4],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn control_stream_requires_settings_first() {
let mut state = H3ControlState::new();
let err = state
.on_remote_control_frame(&H3Frame::Goaway(3))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
}
#[test]
fn control_stream_rejects_cancel_push_first() {
let mut state = H3ControlState::new();
let err = state
.on_remote_control_frame(&H3Frame::CancelPush(123))
.expect_err("CANCEL_PUSH as first frame must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
}
#[test]
fn control_stream_rejects_max_push_id_first() {
let mut state = H3ControlState::new();
let err = state
.on_remote_control_frame(&H3Frame::MaxPushId(456))
.expect_err("MAX_PUSH_ID as first frame must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
}
#[test]
fn control_stream_rejects_unknown_frame_first() {
let mut state = H3ControlState::new();
let unknown_frame = H3Frame::Unknown {
frame_type: 0xBADF00D,
payload: vec![1, 2, 3],
};
let err = state
.on_remote_control_frame(&unknown_frame)
.expect_err("unknown frame as first frame must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
}
#[test]
fn control_stream_rejects_data_headers_first() {
let mut state = H3ControlState::new();
let err = state
.on_remote_control_frame(&H3Frame::Data(vec![1, 2, 3]))
.expect_err("DATA as first frame must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
let mut state2 = H3ControlState::new();
let err = state2
.on_remote_control_frame(&H3Frame::Headers(vec![4, 5, 6]))
.expect_err("HEADERS as first frame must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("first remote control frame must be SETTINGS")
);
}
#[test]
fn control_stream_accepts_settings_first_then_rejects_duplicate() {
let mut state = H3ControlState::new();
state
.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("first SETTINGS should be accepted");
let err = state
.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect_err("duplicate SETTINGS must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("duplicate SETTINGS on remote control stream")
);
state
.on_remote_control_frame(&H3Frame::Goaway(789))
.expect("GOAWAY after SETTINGS should be accepted");
state
.on_remote_control_frame(&H3Frame::CancelPush(101))
.expect("CANCEL_PUSH after SETTINGS should be accepted");
state
.on_remote_control_frame(&H3Frame::MaxPushId(202))
.expect("MAX_PUSH_ID after SETTINGS should be accepted");
}
#[test]
fn pseudo_header_validation() {
let req = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
validate_request_pseudo_headers(&req).expect("valid request");
let resp = H3PseudoHeaders {
status: Some(200),
..H3PseudoHeaders::default()
};
validate_response_pseudo_headers(&resp).expect("valid response");
let connect = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("upstream.example:443".to_string()),
..H3PseudoHeaders::default()
};
validate_request_pseudo_headers(&connect).expect("valid connect request");
}
#[test]
fn pseudo_header_validation_rejects_invalid_connect_and_status() {
let bad_connect = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
scheme: Some("https".to_string()),
authority: Some("upstream.example:443".to_string()),
path: Some("/".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers(&bad_connect).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request must not include :scheme or :path"
)
);
let missing_authority_connect = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
..H3PseudoHeaders::default()
};
let err =
validate_request_pseudo_headers(&missing_authority_connect).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("CONNECT request missing :authority")
);
let bad_resp = H3PseudoHeaders {
status: Some(99),
..H3PseudoHeaders::default()
};
let err = validate_response_pseudo_headers(&bad_resp).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader("status must be in 100..=999")
);
}
#[test]
fn extended_connect_protocol_validation() {
let extended_connect = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
scheme: Some("https".to_string()),
authority: Some("upstream.example:443".to_string()),
path: Some("/websocket".to_string()),
protocol: Some("websocket".to_string()),
..H3PseudoHeaders::default()
};
validate_request_pseudo_headers_with_settings(&extended_connect, true)
.expect("valid extended connect request");
let missing_protocol = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
scheme: Some("https".to_string()),
authority: Some("upstream.example:443".to_string()),
path: Some("/websocket".to_string()),
protocol: None,
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers_with_settings(&missing_protocol, true)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("extended CONNECT request missing :protocol")
);
let empty_protocol = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("upstream.example:443".to_string()),
protocol: Some(String::new()),
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers_with_settings(&empty_protocol, true)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"extended CONNECT request :protocol must not be empty"
)
);
let standard_connect_with_protocol = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("upstream.example:443".to_string()),
protocol: Some("websocket".to_string()),
..H3PseudoHeaders::default()
};
let err =
validate_request_pseudo_headers_with_settings(&standard_connect_with_protocol, false)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request must not include :protocol (extended CONNECT not enabled)"
)
);
}
#[test]
fn h3_request_head_validate_connect_method() {
let extended_connect_head = H3RequestHead::new_with_settings(
H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("upstream.example:443".to_string()),
protocol: Some("websocket".to_string()),
..H3PseudoHeaders::default()
},
vec![],
true,
)
.expect("valid extended connect");
extended_connect_head
.validate_connect_method(true)
.expect("extended CONNECT validation should succeed");
let err = extended_connect_head
.validate_connect_method(false)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"CONNECT request must not include :protocol (extended CONNECT not enabled)"
)
);
let get_head = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
..H3PseudoHeaders::default()
},
vec![],
)
.expect("valid GET");
let err = get_head
.validate_connect_method(false)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"validate_connect_method called on non-CONNECT request"
)
);
}
#[test]
fn request_stream_state_enforces_headers_then_data() {
let mut st = H3RequestStreamState::new();
let err = st
.on_frame(&H3Frame::Data(vec![1, 2, 3]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA before initial HEADERS on request stream")
);
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("headers");
st.on_frame(&H3Frame::Data(vec![1])).expect("data");
st.on_frame(&H3Frame::Headers(vec![0x81]))
.expect("trailers headers");
let err = st.on_frame(&H3Frame::Data(vec![2])).expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA not allowed after trailing HEADERS")
);
}
#[test]
fn request_stream_rejects_non_data_headers_frames() {
let mut st = H3RequestStreamState::new();
let err = st
.on_frame(&H3Frame::Settings(H3Settings::default()))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("control frames are not valid on request streams")
);
}
#[test]
fn request_stream_accepts_datagram_after_headers() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("headers");
st.on_frame(&H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![1, 2, 3],
})
.expect("datagram-after-headers");
st.on_frame(&H3Frame::Data(vec![10])).expect("data ok");
st.on_frame(&H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![4, 5, 6],
})
.expect("datagram-between-headers-and-trailers");
st.on_frame(&H3Frame::Headers(vec![0x81]))
.expect("trailers headers");
st.on_frame(&H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![7, 8, 9],
})
.expect("datagram-after-trailers");
}
#[test]
fn request_stream_rejects_datagram_before_headers() {
let mut st = H3RequestStreamState::new();
let err = st
.on_frame(&H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![1, 2, 3],
})
.expect_err("must fail before HEADERS");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATAGRAM before initial HEADERS on request stream")
);
}
#[test]
fn request_stream_datagram_does_not_advance_state_machine() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("headers");
st.on_frame(&H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![1],
})
.expect("datagram");
st.on_frame(&H3Frame::Data(vec![2]))
.expect("data after datagram");
st.on_frame(&H3Frame::Headers(vec![0x81]))
.expect("trailers after data after datagram");
}
#[test]
fn request_stream_ignores_unknown_frames_without_advancing_state_machine() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Unknown {
frame_type: 0xDEAD_BEEF,
payload: vec![1, 2, 3],
})
.expect("unknown frames must be ignored before headers");
let err = st
.on_frame(&H3Frame::Data(vec![1]))
.expect_err("unknown frame must not count as initial headers");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA before initial HEADERS on request stream")
);
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("headers");
st.on_frame(&H3Frame::Unknown {
frame_type: 0x40,
payload: vec![4, 5],
})
.expect("unknown frames must be ignored after headers");
st.on_frame(&H3Frame::Data(vec![2]))
.expect("DATA after ignored unknown frame");
}
#[test]
fn request_stream_accepts_push_promise_without_advancing_state_machine() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::PushPromise {
push_id: 1,
field_block: vec![0x80],
})
.expect("PUSH_PROMISE is a request-stream sidecar");
let err = st
.on_frame(&H3Frame::Data(vec![1]))
.expect_err("PUSH_PROMISE must not count as initial headers");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA before initial HEADERS on request stream")
);
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("headers");
st.on_frame(&H3Frame::PushPromise {
push_id: 2,
field_block: vec![0x81],
})
.expect("PUSH_PROMISE after headers");
st.on_frame(&H3Frame::Data(vec![2]))
.expect("DATA after push promise");
}
#[test]
fn control_stream_rejects_data_after_settings() {
let mut state = H3ControlState::new();
state
.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = state
.on_remote_control_frame(&H3Frame::Data(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("frame type not allowed on control stream")
);
}
#[test]
fn client_role_applies_goaway_to_new_request_ids() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_control_frame(&H3Frame::Goaway(12)).expect("goaway");
assert_eq!(c.goaway_id(), Some(12));
c.on_request_stream_frame(8, &H3Frame::Headers(vec![1]))
.expect("allowed");
let err = c
.on_request_stream_frame(12, &H3Frame::Headers(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream id rejected after GOAWAY")
);
}
#[test]
fn server_role_accepts_push_id_goaway_without_blocking_request_streams() {
let mut c = H3ConnectionState::new_server();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_control_frame(&H3Frame::Goaway(10))
.expect("server role accepts client push-id goaway");
assert_eq!(c.goaway_id(), Some(10));
c.on_request_stream_frame(12, &H3Frame::Headers(vec![1]))
.expect("server role must keep accepting request stream ids");
}
#[test]
fn connection_state_rejects_increasing_goaway_id() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_control_frame(&H3Frame::Goaway(12)).expect("first");
let err = c
.on_control_frame(&H3Frame::Goaway(16))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("GOAWAY id must not increase")
);
}
#[test]
fn request_stream_rejects_unidirectional_stream_id() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = c
.on_request_stream_frame(2, &H3Frame::Headers(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"request stream id must be client-initiated bidirectional"
)
);
}
#[test]
fn connection_ignores_unknown_request_frame_without_opening_stream() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_request_stream_frame(
0,
&H3Frame::Unknown {
frame_type: 0x21,
payload: vec![0xAA],
},
)
.expect("unknown frame on new request stream must be ignored");
assert_eq!(c.active_request_stream_count(), 0);
c.on_request_stream_frame(0, &H3Frame::Headers(vec![0x80]))
.expect("headers still open the stream after ignored unknown");
assert_eq!(c.active_request_stream_count(), 1);
}
#[test]
fn connection_does_not_open_request_stream_after_invalid_first_frame() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = c
.on_request_stream_frame(0, &H3Frame::Data(vec![0xAA]))
.expect_err("DATA before HEADERS must be rejected");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA before initial HEADERS on request stream")
);
assert_eq!(c.active_request_stream_count(), 0);
c.on_request_stream_frame(0, &H3Frame::Headers(vec![0x80]))
.expect("valid first HEADERS should still open the stream");
assert_eq!(c.active_request_stream_count(), 1);
}
#[test]
fn request_stream_rejects_server_initiated_bidirectional_stream_id() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = c
.on_request_stream_frame(1, &H3Frame::Headers(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"request stream id must be client-initiated bidirectional"
)
);
}
#[test]
fn static_only_qpack_policy_rejects_dynamic_settings() {
let mut c = H3ConnectionState::new();
let settings = H3Settings {
qpack_max_table_capacity: Some(1024),
..H3Settings::default()
};
let err = c
.on_control_frame(&H3Frame::Settings(settings))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::QpackPolicy("dynamic qpack table disabled by policy")
);
}
#[test]
fn duplicate_remote_control_uni_stream_rejected() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(3, H3_STREAM_TYPE_CONTROL)
.expect("first control");
let err = c
.on_remote_uni_stream_type(7, H3_STREAM_TYPE_CONTROL)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("duplicate remote control stream")
);
c.on_uni_stream_frame(3, &H3Frame::Settings(H3Settings::default()))
.expect("original control stream remains active");
let err = c
.on_uni_stream_frame(7, &H3Frame::Settings(H3Settings::default()))
.expect_err("new duplicate stream must not become active");
assert_eq!(
err,
H3NativeError::StreamProtocol("unknown unidirectional stream")
);
}
#[test]
fn uni_stream_type_rejects_bidirectional_stream_id() {
let mut c = H3ConnectionState::new();
let err = c
.on_remote_uni_stream_type(0, H3_STREAM_TYPE_CONTROL)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"unidirectional stream type requires unidirectional stream id"
)
);
}
#[test]
fn push_uni_stream_uses_headers_data_ordering() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(11, H3_STREAM_TYPE_PUSH)
.expect("push type");
let err = c
.on_uni_stream_frame(11, &H3Frame::Headers(vec![0x80]))
.expect_err("must fail before push header");
assert_eq!(
err,
H3NativeError::StreamProtocol("push stream missing push id")
);
c.on_push_stream_header(11, 7).expect("push header");
let err = c
.on_uni_stream_frame(11, &H3Frame::Data(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("DATA before initial HEADERS on request stream")
);
c.on_uni_stream_frame(11, &H3Frame::Headers(vec![0x80]))
.expect("headers");
c.on_uni_stream_frame(11, &H3Frame::Data(vec![1, 2]))
.expect("data");
}
#[test]
fn push_stream_duplicate_header_and_push_id_rejected() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(11, H3_STREAM_TYPE_PUSH)
.expect("first push stream");
c.on_push_stream_header(11, 7).expect("push header");
let err = c
.on_push_stream_header(11, 8)
.expect_err("second push header must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("push stream header already received")
);
c.on_remote_uni_stream_type(15, H3_STREAM_TYPE_PUSH)
.expect("second push stream");
let err = c
.on_push_stream_header(15, 7)
.expect_err("duplicate push id must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("duplicate push id in push stream header")
);
}
#[test]
fn qpack_streams_reject_h3_frame_mapping() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(15, H3_STREAM_TYPE_QPACK_ENCODER)
.expect("qpack encoder");
let err = c
.on_uni_stream_frame(15, &H3Frame::Data(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("qpack streams carry instructions, not h3 frames")
);
}
#[test]
fn h3_native_error_display_all_variants() {
let cases: Vec<(H3NativeError, &str)> = vec![
(H3NativeError::UnexpectedEof, "unexpected EOF"),
(H3NativeError::InvalidFrame("bad"), "invalid frame: bad"),
(
H3NativeError::DuplicateSetting(0x6),
"duplicate setting: 0x6",
),
(
H3NativeError::InvalidSettingValue(0x8),
"invalid setting value: 0x8",
),
(
H3NativeError::ControlProtocol("dup"),
"control stream protocol violation: dup",
),
(
H3NativeError::StreamProtocol("bad stream"),
"stream protocol violation: bad stream",
),
(
H3NativeError::QpackPolicy("no dyn"),
"qpack policy violation: no dyn",
),
(
H3NativeError::InvalidRequestPseudoHeader("missing"),
"invalid request pseudo-header set: missing",
),
(
H3NativeError::InvalidResponsePseudoHeader("bad status"),
"invalid response pseudo-header set: bad status",
),
];
for (err, expected) in &cases {
assert_eq!(format!("{err}"), *expected, "{err:?}");
}
}
#[test]
fn h3_native_error_debug_clone_eq() {
let a = H3NativeError::UnexpectedEof;
let b = a.clone();
assert_eq!(a, b);
let dbg = format!("{a:?}");
assert!(dbg.contains("UnexpectedEof"), "{dbg}");
}
#[test]
fn h3_native_error_is_std_error() {
let err = H3NativeError::UnexpectedEof;
let _: &dyn std::error::Error = &err;
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn h3_qpack_mode_default_debug_copy() {
let mode: H3QpackMode = H3QpackMode::default();
assert_eq!(mode, H3QpackMode::StaticOnly);
let copied = mode; let cloned = mode;
assert_eq!(copied, cloned);
let dbg = format!("{mode:?}");
assert!(dbg.contains("StaticOnly"), "{dbg}");
}
#[test]
fn h3_qpack_mode_inequality() {
assert_ne!(H3QpackMode::StaticOnly, H3QpackMode::DynamicTableAllowed);
}
#[test]
fn h3_connection_config_default_debug_copy() {
let config = H3ConnectionConfig::default();
assert_eq!(config.qpack_mode, H3QpackMode::StaticOnly);
assert_eq!(config.endpoint_role, H3EndpointRole::Client);
let copied = config; let cloned = config;
assert_eq!(copied, cloned);
let dbg = format!("{config:?}");
assert!(dbg.contains("H3ConnectionConfig"), "{dbg}");
}
#[test]
fn h3_endpoint_role_default_debug_copy() {
let role = H3EndpointRole::default();
assert_eq!(role, H3EndpointRole::Client);
let copied = role; let cloned = role;
assert_eq!(copied, cloned);
let dbg = format!("{role:?}");
assert!(dbg.contains("Client"), "{dbg}");
}
#[test]
fn h3_uni_stream_type_debug_copy_eq() {
let t = H3UniStreamType::Control;
let copied = t; let cloned = t;
assert_eq!(copied, cloned);
assert_ne!(H3UniStreamType::Control, H3UniStreamType::Push);
assert_ne!(H3UniStreamType::QpackEncoder, H3UniStreamType::QpackDecoder);
let dbg = format!("{t:?}");
assert!(dbg.contains("Control"), "{dbg}");
}
#[test]
fn h3_uni_stream_type_decode_all_known() {
assert_eq!(H3UniStreamType::decode(0x00), H3UniStreamType::Control);
assert_eq!(H3UniStreamType::decode(0x01), H3UniStreamType::Push);
assert_eq!(H3UniStreamType::decode(0x02), H3UniStreamType::QpackEncoder);
assert_eq!(H3UniStreamType::decode(0x03), H3UniStreamType::QpackDecoder);
}
#[test]
fn h3_uni_stream_type_decode_unknown_accepted() {
let kind = H3UniStreamType::decode(0xFF);
assert_eq!(kind, H3UniStreamType::Unknown(0xFF));
}
#[test]
fn unknown_setting_debug_clone_eq() {
let a = UnknownSetting {
id: 0xAA,
value: 42,
};
let b = a.clone();
assert_eq!(a, b);
let dbg = format!("{a:?}");
assert!(dbg.contains("UnknownSetting"), "{dbg}");
}
#[test]
fn h3_settings_default_debug_clone() {
let s = H3Settings::default();
assert!(s.qpack_max_table_capacity.is_none());
assert!(s.unknown.is_empty());
let dbg = format!("{s:?}");
assert!(dbg.contains("H3Settings"), "{dbg}");
let cloned = s.clone();
assert_eq!(cloned, s);
}
#[test]
fn h3_settings_empty_roundtrip() {
let s = H3Settings::default();
let mut payload = Vec::new();
s.encode_payload(&mut payload).expect("encode");
assert!(payload.is_empty());
let decoded = H3Settings::decode_payload(&payload).expect("decode");
assert_eq!(decoded, s);
}
#[test]
fn h3_frame_debug_clone_all_variants() {
let variants: Vec<H3Frame> = vec![
H3Frame::Data(vec![1, 2]),
H3Frame::Headers(vec![3, 4]),
H3Frame::CancelPush(5),
H3Frame::Settings(H3Settings::default()),
H3Frame::PushPromise {
push_id: 6,
field_block: vec![7],
},
H3Frame::Goaway(8),
H3Frame::MaxPushId(9),
H3Frame::Unknown {
frame_type: 0xFF,
payload: vec![10],
},
];
for frame in &variants {
let dbg = format!("{frame:?}");
assert!(!dbg.is_empty());
let cloned = frame.clone();
assert_eq!(cloned, *frame);
}
}
#[test]
fn h3_control_state_default_debug_clone() {
let s = H3ControlState::new();
let dbg = format!("{s:?}");
assert!(dbg.contains("H3ControlState"), "{dbg}");
let cloned = s.clone();
assert_eq!(cloned, s);
}
#[test]
fn h3_control_state_duplicate_local_settings() {
let mut s = H3ControlState::new();
s.build_local_settings(H3Settings::default())
.expect("first ok");
let err = s
.build_local_settings(H3Settings::default())
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("SETTINGS already sent on local control stream")
);
}
#[test]
fn h3_control_state_duplicate_remote_settings() {
let mut s = H3ControlState::new();
s.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("first ok");
let err = s
.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("duplicate SETTINGS on remote control stream")
);
}
#[test]
fn h3_pseudo_headers_default_debug_clone() {
let ph = H3PseudoHeaders::default();
assert!(ph.method.is_none());
assert!(ph.scheme.is_none());
assert!(ph.authority.is_none());
assert!(ph.path.is_none());
assert!(ph.status.is_none());
let dbg = format!("{ph:?}");
assert!(dbg.contains("H3PseudoHeaders"), "{dbg}");
let cloned = ph.clone();
assert_eq!(cloned, ph);
}
#[test]
fn h3_request_head_debug_clone_eq() {
let head = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
},
vec![],
)
.expect("valid");
let dbg = format!("{head:?}");
assert!(dbg.contains("H3RequestHead"), "{dbg}");
let cloned = head.clone();
assert_eq!(cloned, head);
}
#[test]
fn h3_response_head_debug_clone_eq() {
let head = H3ResponseHead::new(200, vec![]).expect("valid");
let dbg = format!("{head:?}");
assert!(dbg.contains("H3ResponseHead"), "{dbg}");
assert_eq!(head.status, 200);
let cloned = head.clone();
assert_eq!(cloned, head);
}
#[test]
fn h3_response_head_invalid_status() {
let err = H3ResponseHead::new(50, vec![]).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader("status must be in 100..=999")
);
}
#[test]
fn response_pseudo_headers_reject_authority() {
let headers = H3PseudoHeaders {
status: Some(200),
authority: Some("example.com".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_response_pseudo_headers(&headers).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers"
)
);
}
#[test]
fn response_pseudo_headers_reject_101_switching_protocols() {
let headers = H3PseudoHeaders {
status: Some(101),
..H3PseudoHeaders::default()
};
let err = validate_response_pseudo_headers(&headers).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"HTTP/3 does not support 101 Switching Protocols"
)
);
}
#[test]
fn qpack_field_plan_debug_clone_eq() {
let idx = QpackFieldPlan::StaticIndex(17);
let lit = QpackFieldPlan::Literal {
name: "x".to_string(),
value: "y".to_string(),
};
assert_ne!(idx, lit);
let dbg = format!("{idx:?}");
assert!(dbg.contains("StaticIndex"), "{dbg}");
let cloned = lit.clone();
assert_eq!(cloned, lit);
}
#[test]
fn qpack_static_plans_use_known_indices() {
let req = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
},
vec![("accept".to_string(), "*/*".to_string())],
)
.expect("request");
let req_plan = qpack_static_plan_for_request(&req);
assert!(req_plan.contains(&QpackFieldPlan::StaticIndex(17)));
assert!(req_plan.contains(&QpackFieldPlan::StaticIndex(23)));
assert!(req_plan.contains(&QpackFieldPlan::StaticIndex(1)));
let resp = H3ResponseHead::new(200, vec![("server".to_string(), "asupersync".to_string())])
.expect("response");
let resp_plan = qpack_static_plan_for_response(&resp);
assert_eq!(resp_plan.first(), Some(&QpackFieldPlan::StaticIndex(25)));
}
#[test]
fn frame_roundtrip_data() {
let frame = H3Frame::Data(vec![0xCA, 0xFE]);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_headers() {
let frame = H3Frame::Headers(vec![0x80, 0x81, 0x82]);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_cancel_push() {
let frame = H3Frame::CancelPush(42);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_goaway() {
let frame = H3Frame::Goaway(1000);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_max_push_id() {
let frame = H3Frame::MaxPushId(255);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_unknown() {
let frame = H3Frame::Unknown {
frame_type: 0x1F,
payload: vec![0xDE, 0xAD],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_roundtrip_settings() {
let settings = H3Settings {
qpack_max_table_capacity: Some(4096),
max_field_section_size: Some(8192),
qpack_blocked_streams: None,
enable_connect_protocol: Some(true),
h3_datagram: None,
unknown: vec![],
};
let frame = H3Frame::Settings(settings);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn frame_decode_empty_input_error() {
let err = H3Frame::decode(&[], &test_config()).expect_err("must fail on empty input");
assert_eq!(err, H3NativeError::InvalidFrame("frame type varint"));
}
#[test]
fn frame_decode_truncated_payload_unexpected_eof() {
let frame = H3Frame::Data(vec![1, 2, 3, 4]);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let truncated = &buf[..buf.len().saturating_sub(2)];
let err =
H3Frame::decode(truncated, &test_config()).expect_err("must fail on truncated payload");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn frame_decode_cancel_push_trailing_bytes_invalid_frame() {
let mut payload = Vec::new();
encode_varint(7, &mut payload).expect("varint");
payload.push(0xFF);
let mut buf = Vec::new();
encode_varint(H3_FRAME_CANCEL_PUSH, &mut buf).expect("type");
encode_varint(payload.len() as u64, &mut buf).expect("len");
buf.extend_from_slice(&payload);
let err = H3Frame::decode(&buf, &test_config()).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame("cancel_push trailing bytes")
);
}
#[test]
fn frame_decode_goaway_trailing_bytes_invalid_frame() {
let mut payload = Vec::new();
encode_varint(50, &mut payload).expect("varint");
payload.push(0xAA);
let mut buf = Vec::new();
encode_varint(H3_FRAME_GOAWAY, &mut buf).expect("type");
encode_varint(payload.len() as u64, &mut buf).expect("len");
buf.extend_from_slice(&payload);
let err = H3Frame::decode(&buf, &test_config()).expect_err("must fail");
assert_eq!(err, H3NativeError::InvalidFrame("goaway trailing bytes"));
}
#[test]
fn frame_decode_max_push_id_trailing_bytes_invalid_frame() {
let mut payload = Vec::new();
encode_varint(99, &mut payload).expect("varint");
payload.push(0xBB);
let mut buf = Vec::new();
encode_varint(H3_FRAME_MAX_PUSH_ID, &mut buf).expect("type");
encode_varint(payload.len() as u64, &mut buf).expect("len");
buf.extend_from_slice(&payload);
let err = H3Frame::decode(&buf, &test_config()).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame("max_push_id trailing bytes")
);
}
#[test]
fn frame_decode_push_promise_empty_field_block_rejected() {
let mut payload = Vec::new();
encode_varint(7, &mut payload).expect("push_id varint");
let mut buf = Vec::new();
encode_varint(H3_FRAME_PUSH_PROMISE, &mut buf).expect("frame type");
encode_varint(payload.len() as u64, &mut buf).expect("frame length");
buf.extend_from_slice(&payload);
let err = H3Frame::decode(&buf, &test_config()).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame("push_promise empty field_block (RFC 9114 §7.2.5)")
);
}
#[test]
fn frame_decode_push_promise_with_field_block_ok() {
let mut payload = Vec::new();
encode_varint(42, &mut payload).expect("push_id varint");
payload.extend_from_slice(&[0x01, 0x02, 0x03]);
let mut buf = Vec::new();
encode_varint(H3_FRAME_PUSH_PROMISE, &mut buf).expect("frame type");
encode_varint(payload.len() as u64, &mut buf).expect("frame length");
buf.extend_from_slice(&payload);
let (frame, _consumed) = H3Frame::decode(&buf, &test_config()).expect("must decode");
match frame {
H3Frame::PushPromise {
push_id,
field_block,
} => {
assert_eq!(push_id, 42);
assert_eq!(field_block, vec![0x01, 0x02, 0x03]);
}
other => panic!("expected PushPromise, got {other:?}"),
}
}
#[test]
fn validate_header_name_rejects_rfc9114_forbidden_names() {
for forbidden in [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
] {
let err = validate_header_name(forbidden).expect_err(forbidden);
match err {
H3NativeError::InvalidFrame(msg) => assert!(
msg.contains("forbidden"),
"wrong reject reason for {forbidden}: {msg}"
),
other => panic!("expected InvalidFrame, got {other:?}"),
}
}
}
#[test]
fn validate_header_name_accepts_ordinary_names() {
for ok in [
"content-type",
"content-length",
"x-custom-header",
"te", ":authority",
":method",
":path",
] {
validate_header_name(ok).unwrap_or_else(|e| panic!("rejected {ok}: {e:?}"));
}
}
#[test]
fn qpack_relative_to_absolute_pre_base_overflow_rejected() {
let err = qpack_relative_to_absolute(100, u64::MAX, false)
.expect_err("must reject u64::MAX relative_index");
match err {
H3NativeError::InvalidFrame(msg) => assert!(
msg.contains("H3_QPACK_DECODER_STREAM_ERROR"),
"wrong reject reason: {msg}"
),
other => panic!("expected InvalidFrame, got {other:?}"),
}
}
#[test]
fn qpack_relative_to_absolute_pre_base_happy_path() {
let abs = qpack_relative_to_absolute(10, 3, false).expect("valid pre-base");
assert_eq!(abs, 6); let abs0 = qpack_relative_to_absolute(1, 0, false).expect("base=1, index=0");
assert_eq!(abs0, 0);
}
#[test]
fn qpack_relative_to_absolute_pre_base_index_geq_base_rejected() {
assert!(qpack_relative_to_absolute(5, 5, false).is_err());
assert!(qpack_relative_to_absolute(5, 10, false).is_err());
assert_eq!(
qpack_relative_to_absolute(5, 4, false).expect("boundary valid"),
0
);
}
#[test]
fn request_stream_trailers_without_data_valid() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Headers(vec![0x80]))
.expect("first HEADERS");
st.on_frame(&H3Frame::Headers(vec![0x81]))
.expect("trailers without DATA must succeed per RFC 9114");
}
#[test]
fn request_stream_mark_end_stream_after_headers_only() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Headers(vec![0x80]))
.expect("first HEADERS");
st.mark_end_stream().expect("valid headers-only end");
}
#[test]
fn request_stream_mark_end_stream_before_headers_error() {
let mut st = H3RequestStreamState::new();
let err = st.mark_end_stream().expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream ended before initial HEADERS")
);
}
#[test]
fn request_stream_on_frame_after_end_stream_error() {
let mut st = H3RequestStreamState::new();
st.on_frame(&H3Frame::Headers(vec![0x80])).expect("HEADERS");
st.mark_end_stream().expect("end");
let err = st.on_frame(&H3Frame::Data(vec![1])).expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream already finished")
);
}
#[test]
fn finish_request_stream_unknown_stream_id_error() {
let mut c = H3ConnectionState::new();
let err = c.finish_request_stream(999).expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("unknown request stream on finish")
);
}
#[test]
fn finished_request_stream_rejects_late_frames() {
let mut c = H3ConnectionState::new();
c.on_request_stream_frame(0, &H3Frame::Headers(vec![0x80]))
.expect("headers");
c.finish_request_stream(0).expect("finish");
let err = c
.on_request_stream_frame(0, &H3Frame::Headers(vec![0x81]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream already finished")
);
}
#[test]
fn finish_request_stream_twice_reports_finished() {
let mut c = H3ConnectionState::new();
c.on_request_stream_frame(0, &H3Frame::Headers(vec![0x80]))
.expect("headers");
c.finish_request_stream(0).expect("finish");
let err = c.finish_request_stream(0).expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream already finished")
);
}
#[test]
fn duplicate_qpack_encoder_stream_error() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(3, H3_STREAM_TYPE_QPACK_ENCODER)
.expect("first encoder");
let err = c
.on_remote_uni_stream_type(7, H3_STREAM_TYPE_QPACK_ENCODER)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("duplicate remote qpack encoder stream")
);
}
#[test]
fn duplicate_qpack_decoder_stream_error() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(3, H3_STREAM_TYPE_QPACK_DECODER)
.expect("first decoder");
let err = c
.on_remote_uni_stream_type(7, H3_STREAM_TYPE_QPACK_DECODER)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("duplicate remote qpack decoder stream")
);
}
#[test]
fn uni_stream_type_already_set_for_same_id_error() {
let mut c = H3ConnectionState::new();
c.on_remote_uni_stream_type(3, H3_STREAM_TYPE_CONTROL)
.expect("first set");
let err = c
.on_remote_uni_stream_type(3, H3_STREAM_TYPE_PUSH)
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::StreamProtocol("unidirectional stream type already set")
);
}
#[test]
fn goaway_decreasing_is_allowed() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_control_frame(&H3Frame::Goaway(100))
.expect("first goaway=100");
assert_eq!(c.goaway_id(), Some(100));
c.on_control_frame(&H3Frame::Goaway(96))
.expect("second goaway=96");
assert_eq!(c.goaway_id(), Some(96));
}
#[test]
fn client_role_rejects_non_request_goaway_id() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = c
.on_control_frame(&H3Frame::Goaway(10))
.expect_err("must reject invalid goaway id");
assert_eq!(
err,
H3NativeError::ControlProtocol(
"GOAWAY id must be a client-initiated bidirectional stream id"
)
);
}
#[test]
fn client_role_rejects_max_push_id_control_frame() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = c
.on_control_frame(&H3Frame::MaxPushId(10))
.expect_err("client must reject MAX_PUSH_ID from server");
assert_eq!(
err,
H3NativeError::ControlProtocol("client must not receive MAX_PUSH_ID")
);
}
#[test]
fn client_role_goaway_zero_blocks_all_request_streams() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_control_frame(&H3Frame::Goaway(0)).expect("goaway=0");
assert_eq!(c.goaway_id(), Some(0));
let err = c
.on_request_stream_frame(0, &H3Frame::Headers(vec![1]))
.expect_err("must fail");
assert_eq!(
err,
H3NativeError::ControlProtocol("request stream id rejected after GOAWAY")
);
}
#[test]
fn dynamic_table_allowed_accepts_nonzero_capacity() {
let config = H3ConnectionConfig {
qpack_mode: H3QpackMode::DynamicTableAllowed,
..H3ConnectionConfig::default()
};
let mut c = H3ConnectionState::with_config(config);
let settings = H3Settings {
qpack_max_table_capacity: Some(4096),
qpack_blocked_streams: Some(100),
..H3Settings::default()
};
c.on_control_frame(&H3Frame::Settings(settings))
.expect("dynamic table settings accepted");
}
#[test]
fn client_role_rejects_locally_initiated_remote_uni_stream_id() {
let mut c = H3ConnectionState::new();
let err = c
.on_remote_uni_stream_type(2, H3_STREAM_TYPE_CONTROL)
.expect_err("client must reject locally initiated uni stream id");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"unidirectional stream type requires peer-initiated unidirectional stream id"
)
);
}
#[test]
fn server_role_rejects_server_initiated_remote_uni_stream_id() {
let mut c = H3ConnectionState::new_server();
let err = c
.on_remote_uni_stream_type(3, H3_STREAM_TYPE_CONTROL)
.expect_err("server must reject its own uni stream ids as remote");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"unidirectional stream type requires peer-initiated unidirectional stream id"
)
);
}
#[test]
fn server_role_rejects_push_streams() {
let mut c = H3ConnectionState::new_server();
let err = c
.on_remote_uni_stream_type(2, H3_STREAM_TYPE_PUSH)
.expect_err("server must reject client push streams");
assert_eq!(
err,
H3NativeError::StreamProtocol("server endpoint must not receive push streams")
);
}
#[test]
fn qpack_static_plan_request_non_static_method_produces_literal() {
let req = H3RequestHead::new(
H3PseudoHeaders {
method: Some("PATCH".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/resource".to_string()),
status: None,
protocol: None,
},
vec![],
)
.expect("valid request");
let plan = qpack_static_plan_for_request(&req);
assert_eq!(
plan[0],
QpackFieldPlan::Literal {
name: ":method".to_string(),
value: "PATCH".to_string(),
}
);
}
#[test]
fn qpack_static_plan_response_non_indexed_status_produces_literal() {
let resp = H3ResponseHead::new(201, vec![]).expect("valid response");
let plan = qpack_static_plan_for_response(&resp);
assert_eq!(
plan[0],
QpackFieldPlan::Literal {
name: ":status".to_string(),
value: "201".to_string(),
}
);
}
#[test]
fn qpack_wire_roundtrip_static_and_literal_field_lines() {
let plan = vec![
QpackFieldPlan::StaticIndex(17), QpackFieldPlan::StaticIndex(23), QpackFieldPlan::StaticIndex(1), QpackFieldPlan::Literal {
name: ":authority".to_string(),
value: "example.com".to_string(),
},
QpackFieldPlan::Literal {
name: "accept".to_string(),
value: "application/json".to_string(),
},
];
let encoded = qpack_encode_field_section(&plan).expect("encode");
let decoded =
qpack_decode_field_section(&encoded, H3QpackMode::StaticOnly).expect("decode");
assert_eq!(decoded, plan);
let headers = qpack_plan_to_header_fields(&decoded, None).expect("expand headers");
assert_eq!(headers[0], (":method".to_string(), "GET".to_string()));
assert_eq!(headers[1], (":scheme".to_string(), "https".to_string()));
assert_eq!(headers[2], (":path".to_string(), "/".to_string()));
assert_eq!(
headers[3],
(":authority".to_string(), "example.com".to_string())
);
assert_eq!(
headers[4],
("accept".to_string(), "application/json".to_string())
);
}
#[test]
fn qpack_wire_request_and_response_helpers_roundtrip() {
let request = H3RequestHead::new(
H3PseudoHeaders {
method: Some("POST".to_string()),
scheme: Some("https".to_string()),
authority: Some("api.example.com".to_string()),
path: Some("/upload".to_string()),
status: None,
protocol: None,
},
vec![("content-type".to_string(), "application/json".to_string())],
)
.expect("request");
let request_plan = qpack_static_plan_for_request(&request);
let request_wire = qpack_encode_request_field_section(&request).expect("request encode");
let request_decoded = qpack_decode_field_section(&request_wire, H3QpackMode::StaticOnly)
.expect("request decode");
assert_eq!(request_decoded, request_plan);
let response = H3ResponseHead::new(
200,
vec![("content-type".to_string(), "text/plain".to_string())],
)
.expect("response");
let response_plan = qpack_static_plan_for_response(&response);
let response_wire =
qpack_encode_response_field_section(&response).expect("response encode");
let response_decoded = qpack_decode_field_section(&response_wire, H3QpackMode::StaticOnly)
.expect("response decode");
assert_eq!(response_decoded, response_plan);
}
#[test]
fn qpack_wire_decode_request_head_helper_roundtrip() {
let request = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("api.example.com".to_string()),
path: Some("/v1/items".to_string()),
status: None,
protocol: None,
},
vec![("accept".to_string(), "application/json".to_string())],
)
.expect("request");
let wire = qpack_encode_request_field_section(&request).expect("encode");
let decoded = qpack_decode_request_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect("decode");
assert_eq!(decoded, request);
}
#[test]
fn qpack_wire_decode_response_head_helper_roundtrip() {
let response = H3ResponseHead::new(
200,
vec![
("content-type".to_string(), "text/plain".to_string()),
("server".to_string(), "asupersync".to_string()),
],
)
.expect("response");
let wire = qpack_encode_response_field_section(&response).expect("encode");
let decoded = qpack_decode_response_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect("decode");
assert_eq!(decoded, response);
}
#[test]
fn qpack_request_decode_rejects_pseudo_after_regular_header() {
let plan = vec![
QpackFieldPlan::Literal {
name: "accept".to_string(),
value: "*/*".to_string(),
},
QpackFieldPlan::StaticIndex(17), QpackFieldPlan::StaticIndex(23), QpackFieldPlan::StaticIndex(1), ];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_request_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect_err("fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"request pseudo headers must precede regular headers",
)
);
}
#[test]
fn qpack_request_decode_rejects_duplicate_method() {
let plan = vec![
QpackFieldPlan::StaticIndex(17), QpackFieldPlan::Literal {
name: ":method".to_string(),
value: "POST".to_string(),
},
QpackFieldPlan::StaticIndex(23), QpackFieldPlan::StaticIndex(1), ];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_request_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect_err("fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("duplicate :method")
);
}
#[test]
fn qpack_response_decode_rejects_invalid_status_value() {
let plan = vec![QpackFieldPlan::Literal {
name: ":status".to_string(),
value: "ok".to_string(),
}];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_response_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect_err("fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader("invalid :status value")
);
}
#[test]
fn response_decode_rejects_zero_padded_status_value() {
let fields = vec![(":status".to_string(), "020".to_string())];
let err = header_fields_to_response_head(&fields).expect_err("must reject zero-padded");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader("status must be in 100..=999")
);
}
#[test]
fn qpack_response_decode_rejects_non_three_digit_status_value() {
let plan = vec![QpackFieldPlan::Literal {
name: ":status".to_string(),
value: "0200".to_string(),
}];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_response_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect_err("fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader("invalid :status value")
);
}
#[test]
fn qpack_response_decode_rejects_request_pseudo_header() {
let plan = vec![
QpackFieldPlan::StaticIndex(25), QpackFieldPlan::Literal {
name: ":method".to_string(),
value: "GET".to_string(),
},
];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_response_field_section(&wire, H3QpackMode::StaticOnly, None)
.expect_err("fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers",
)
);
}
#[test]
fn qpack_wire_static_only_rejects_required_insert_count() {
let wire = [0x01u8, 0x00, 0xD1];
let err = qpack_decode_field_section(&wire, H3QpackMode::StaticOnly).expect_err("reject");
assert_eq!(
err,
H3NativeError::QpackPolicy("required insert count must be zero in static-only mode")
);
}
#[test]
fn qpack_dynamic_decode_rejects_required_insert_count_beyond_table_state() {
let mut context = QpackContext::new(4096);
context
.insert_dynamic_entry("x-one".to_string(), "value-1".to_string())
.expect("insert entry");
let wire = [0x03u8, 0x00, 0x80];
let err = qpack_decode_field_section_with_context(
&wire,
H3QpackMode::DynamicTableAllowed,
Some(&context),
)
.expect_err("required insert count should block decode");
assert_eq!(
err,
H3NativeError::QpackPolicy("required insert count exceeds dynamic table state")
);
}
#[test]
fn qpack_dynamic_decode_resolves_relative_index_with_nonzero_ric() {
let mut context = QpackContext::new(4096);
context
.insert_dynamic_entry("x-old".to_string(), "old".to_string())
.expect("insert old entry");
context
.insert_dynamic_entry("x-middle".to_string(), "middle".to_string())
.expect("insert middle entry");
context
.insert_dynamic_entry("x-new".to_string(), "new".to_string())
.expect("insert new entry");
let wire = [0x03u8, 0x00, 0x80];
let plan = qpack_decode_field_section_with_context(
&wire,
H3QpackMode::DynamicTableAllowed,
Some(&context),
)
.expect("decode dynamic field section");
assert_eq!(plan, vec![QpackFieldPlan::DynamicIndex(1)]);
let fields = qpack_plan_to_header_fields(&plan, Some(&context)).expect("resolve fields");
assert_eq!(fields, vec![("x-middle".to_string(), "middle".to_string())]);
}
#[test]
fn qpack_required_insert_count_encoder_matches_decoder_boundaries() {
for required_insert_count in [1, 2, 3, 127, 128, 129, 255, 256, 257] {
let encoded =
qpack_encode_required_insert_count(required_insert_count, 4096).expect("encode");
let decoded = qpack_decode_required_insert_count(encoded, required_insert_count, 4096)
.expect("decode");
assert_eq!(
decoded, required_insert_count,
"required insert count {required_insert_count} encoded as {encoded}"
);
}
}
#[test]
fn qpack_dynamic_lookup_preserves_evicted_absolute_gaps() {
let mut table = QpackDynamicTable::new(76);
let old = table
.insert("old".to_string(), "1".to_string())
.expect("insert old");
let middle = table
.insert("middle".to_string(), "2".to_string())
.expect("insert middle");
assert!(table.reference_entry(old));
let new = table
.insert("new".to_string(), "3".to_string())
.expect("insert new");
assert_eq!(old, 0);
assert_eq!(middle, 1);
assert_eq!(new, 2);
assert_eq!(table.evicted_count(), 1);
assert_eq!(qpack_dynamic_entry(&table, old), Some(("old", "1")));
assert_eq!(qpack_dynamic_entry(&table, middle), None);
assert_eq!(qpack_dynamic_entry(&table, new), Some(("new", "3")));
}
#[test]
fn qpack_wire_decodes_huffman_strings_in_static_mode() {
let mut encoded_value = BytesMut::new();
hpack_encode_huffman(&mut encoded_value, b"www.example.com");
let mut wire = vec![0x00u8, 0x00];
qpack_encode_prefixed_int(&mut wire, 0x50, 4, 0).expect("encode name ref");
qpack_encode_prefixed_int(&mut wire, 0x80, 7, encoded_value.len() as u64)
.expect("encode huffman string len");
wire.extend_from_slice(&encoded_value);
let plan = qpack_decode_field_section(&wire, H3QpackMode::StaticOnly).expect("decode");
assert_eq!(
plan,
vec![QpackFieldPlan::Literal {
name: ":authority".to_string(),
value: "www.example.com".to_string(),
}]
);
}
#[test]
fn qpack_dynamic_wire_encode_roundtrips_with_context() {
let mut context = QpackContext::new(4096);
context
.insert_dynamic_entry("x-old".to_string(), "old".to_string())
.expect("insert old entry");
context
.insert_dynamic_entry("x-middle".to_string(), "middle".to_string())
.expect("insert middle entry");
context
.insert_dynamic_entry("x-new".to_string(), "new".to_string())
.expect("insert new entry");
let plan = vec![
QpackFieldPlan::DynamicIndex(1),
QpackFieldPlan::DynamicNameLiteral {
name_index: 2,
value: "tail".to_string(),
},
];
let wire =
qpack_encode_field_section_with_context(&plan, Some(&context)).expect("encode wire");
let decoded = qpack_decode_field_section_with_context(
&wire,
H3QpackMode::DynamicTableAllowed,
Some(&context),
)
.expect("decode wire");
assert_eq!(decoded, plan);
let headers = qpack_plan_to_header_fields(&decoded, Some(&context)).expect("resolve");
assert_eq!(
headers,
vec![
("x-middle".to_string(), "middle".to_string()),
("x-new".to_string(), "tail".to_string()),
]
);
}
#[test]
fn qpack_dynamic_decode_resolves_post_base_lines() {
let mut context = QpackContext::new(4096);
context
.insert_dynamic_entry("x-old".to_string(), "old".to_string())
.expect("insert old entry");
context
.insert_dynamic_entry("x-middle".to_string(), "middle".to_string())
.expect("insert middle entry");
context
.insert_dynamic_entry("x-new".to_string(), "new".to_string())
.expect("insert new entry");
let wire = [0x04u8, 0x80, 0x10, 0x00, 0x04, b't', b'a', b'i', b'l'];
let plan = qpack_decode_field_section_with_context(
&wire,
H3QpackMode::DynamicTableAllowed,
Some(&context),
)
.expect("decode post-base wire");
assert_eq!(
plan,
vec![
QpackFieldPlan::DynamicIndex(2),
QpackFieldPlan::DynamicNameLiteral {
name_index: 2,
value: "tail".to_string(),
},
]
);
}
#[test]
fn qpack_decode_rejects_field_sections_over_header_count_limit() {
let plan: Vec<_> = (0..=QPACK_MAX_DECODED_HEADERS)
.map(|_| QpackFieldPlan::Literal {
name: "x-test".to_string(),
value: String::new(),
})
.collect();
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_field_section(&wire, H3QpackMode::StaticOnly).expect_err("reject");
assert_eq!(
err,
H3NativeError::QpackPolicy("decoded header count exceeds safety limit")
);
}
#[test]
fn qpack_request_decode_with_limit_rejects_oversized_field_section() {
let plan = vec![QpackFieldPlan::Literal {
name: "x-test".to_string(),
value: "abcdef".to_string(),
}];
let wire = qpack_encode_field_section(&plan).expect("encode");
let err = qpack_decode_request_field_section_with_limit(
&wire,
H3QpackMode::StaticOnly,
None,
Some(40),
)
.expect_err("reject oversized field section");
assert_eq!(
err,
H3NativeError::QpackPolicy("decoded field section exceeds maximum size limit")
);
}
#[test]
fn qpack_plan_to_header_fields_rejects_unknown_static_index() {
let err = qpack_plan_to_header_fields(&[QpackFieldPlan::StaticIndex(999)], None)
.expect_err("unknown static index");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown static qpack index")
);
}
#[test]
fn qpack_wire_decode_rejects_unknown_static_index() {
let wire = [0x00u8, 0x00, 0xFF, 0x24];
let err = qpack_decode_field_section(&wire, H3QpackMode::StaticOnly).expect_err("reject");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown static qpack index")
);
}
#[test]
fn qpack_wire_encode_rejects_unknown_static_index() {
let err = qpack_encode_field_section(&[QpackFieldPlan::StaticIndex(999)])
.expect_err("unknown static index");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown static qpack index")
);
}
#[test]
fn qpack_prefixed_int_rejects_high_shift_truncation() {
let mut wire = vec![0xFFu8]; wire.extend(std::iter::repeat_n(0x80, 9)); wire.push(0x02); let result = qpack_decode_prefixed_int(0xFF, 8, &wire[1..]);
assert!(
result.is_err(),
"must reject integer that would silently truncate at high shifts"
);
}
#[test]
fn qpack_encoder_instruction_roundtrips_all_variants() {
let cases = [
QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 4096 },
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(1),
value: "/index.html".to_string(),
},
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(2),
value: "dynamic-value".to_string(),
},
QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-custom".to_string(),
value: "literal-value".to_string(),
},
QpackEncoderInstruction::Duplicate { index: 3 },
];
for instruction in cases {
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
let (decoded, consumed) =
qpack_decode_encoder_instruction(&wire).expect("decode encoder instruction");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
}
#[test]
fn qpack_encoder_instruction_uses_expected_wire_prefixes() {
let cases = [
(
QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 0 },
0b0010_0000,
),
(
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(0),
value: String::new(),
},
0b1100_0000,
),
(
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(0),
value: String::new(),
},
0b1000_0000,
),
(
QpackEncoderInstruction::InsertWithoutNameReference {
name: String::new(),
value: String::new(),
},
0b0100_0000,
),
(QpackEncoderInstruction::Duplicate { index: 0 }, 0b0000_0000),
];
for (instruction, expected_first) in cases {
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
assert_eq!(wire[0], expected_first);
}
}
#[test]
fn qpack_decoder_instruction_roundtrips_all_variants() {
let cases = [
QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 0 },
QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 4 },
QpackDecoderInstruction::StreamCancellation { stream_id: 8 },
QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
QpackDecoderInstruction::InsertCountIncrement { increment: 128 },
];
for instruction in cases {
let mut wire = Vec::new();
qpack_encode_decoder_instruction(&mut wire, &instruction).expect("encode");
let (decoded, consumed) =
qpack_decode_decoder_instruction(&wire).expect("decode decoder instruction");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
}
#[test]
fn qpack_decoder_instruction_uses_expected_wire_prefixes() {
let cases = [
(
QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 0 },
0b1000_0000,
),
(
QpackDecoderInstruction::StreamCancellation { stream_id: 0 },
0b0100_0000,
),
(
QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
0b0000_0001,
),
];
for (instruction, expected_first) in cases {
let mut wire = Vec::new();
qpack_encode_decoder_instruction(&mut wire, &instruction).expect("encode");
assert_eq!(wire[0], expected_first);
}
}
#[test]
fn qpack_instruction_prefixed_integer_boundaries() {
for capacity in [30, 31, 32, 1337, u16::MAX as u64] {
let instruction = QpackEncoderInstruction::SetDynamicTableCapacity { capacity };
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
let (decoded, consumed) =
qpack_decode_encoder_instruction(&wire).expect("decode capacity");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
for stream_id in [126, 127, 128, 16_384] {
let instruction = QpackDecoderInstruction::HeaderAcknowledgement { stream_id };
let mut wire = Vec::new();
qpack_encode_decoder_instruction(&mut wire, &instruction).expect("encode");
let (decoded, consumed) =
qpack_decode_decoder_instruction(&wire).expect("decode stream id");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
}
#[test]
fn qpack_instruction_string_edges_roundtrip() {
let cases = [
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(0),
value: String::new(),
},
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(0),
value: "www.example.com".to_string(),
},
QpackEncoderInstruction::InsertWithoutNameReference {
name: String::new(),
value: String::new(),
},
QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-long-name".repeat(40),
value: "large-bounded-value".repeat(40),
},
];
for instruction in cases {
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
let (decoded, consumed) =
qpack_decode_encoder_instruction(&wire).expect("decode string instruction");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
}
#[test]
fn qpack_instruction_value_string_uses_huffman_when_smaller() {
let instruction = QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(0),
value: "www.example.com".to_string(),
};
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
assert_ne!(wire[1] & 0b1000_0000, 0);
let (decoded, consumed) =
qpack_decode_encoder_instruction(&wire).expect("decode huffman value");
assert_eq!(decoded, instruction);
assert_eq!(consumed, wire.len());
}
#[test]
fn qpack_instruction_decode_reports_consumed_prefix_only() {
let instruction = QpackEncoderInstruction::Duplicate { index: 31 };
let mut wire = Vec::new();
qpack_encode_encoder_instruction(&mut wire, &instruction).expect("encode");
let expected_instruction_len = wire.len();
wire.extend_from_slice(&[0xAA, 0xBB]);
let (decoded, consumed) =
qpack_decode_encoder_instruction(&wire).expect("decode with trailing bytes");
assert_eq!(decoded, instruction);
assert_eq!(consumed, expected_instruction_len);
}
#[test]
fn qpack_decoder_instruction_decode_reports_consumed_prefix_only() {
let instruction = QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 127 };
let mut wire = Vec::new();
qpack_encode_decoder_instruction(&mut wire, &instruction).expect("encode");
let expected_instruction_len = wire.len();
wire.extend_from_slice(&[0xCC, 0xDD]);
let (decoded, consumed) =
qpack_decode_decoder_instruction(&wire).expect("decode with trailing bytes");
assert_eq!(decoded, instruction);
assert_eq!(consumed, expected_instruction_len);
}
#[test]
fn qpack_encoder_instruction_rejects_truncated_capacity_integer() {
let err = qpack_decode_encoder_instruction(&[0x3F]).expect_err("truncated capacity");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn qpack_encoder_instruction_rejects_overflow_capacity_integer() {
let mut wire = vec![0x3Fu8];
wire.extend(std::iter::repeat_n(0x80, 9));
wire.push(0x02);
let err = qpack_decode_encoder_instruction(&wire).expect_err("overflow capacity");
assert_eq!(err, H3NativeError::InvalidFrame("qpack integer overflow"));
}
#[test]
fn qpack_encoder_instruction_rejects_truncated_value_string() {
let err = qpack_decode_encoder_instruction(&[0xC0]).expect_err("missing value");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn qpack_encoder_instruction_rejects_truncated_value_length_integer() {
let err =
qpack_decode_encoder_instruction(&[0xC0, 0x7F]).expect_err("truncated value length");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn qpack_encoder_instruction_rejects_truncated_literal_name() {
let err =
qpack_decode_encoder_instruction(&[0x43, b'x']).expect_err("truncated literal name");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn qpack_decoder_instruction_rejects_truncated_stream_id_integer() {
let err =
qpack_decode_decoder_instruction(&[0xFF]).expect_err("truncated stream id integer");
assert_eq!(err, H3NativeError::UnexpectedEof);
}
#[test]
fn qpack_decoder_instruction_rejects_zero_insert_count_increment() {
let err = qpack_encode_decoder_instruction(
&mut Vec::new(),
&QpackDecoderInstruction::InsertCountIncrement { increment: 0 },
)
.expect_err("zero increment encode must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack insert count increment must be non-zero")
);
let err =
qpack_decode_decoder_instruction(&[0x00]).expect_err("zero increment decode must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack insert count increment must be non-zero")
);
}
#[test]
fn qpack_decoder_feedback_ack_releases_references_and_tracks_fields() {
let mut context = QpackContext::new(128);
let first = context
.insert_dynamic_entry("one".to_string(), "1".to_string())
.expect("insert first");
let second = context
.insert_dynamic_entry("two".to_string(), "2".to_string())
.expect("insert second");
let mut feedback = QpackDecoderFeedbackState::new();
feedback
.track_stream_references(&mut context, 4, &[first, second])
.expect("track references");
assert_eq!(feedback.known_received_count(), 0);
assert_eq!(feedback.stream_outstanding_reference_count(4), 2);
assert_eq!(feedback.outstanding_reference_count(), 2);
assert_eq!(feedback.first_error(), None);
assert_eq!(
context
.set_dynamic_table_capacity(0)
.expect_err("referenced entries must block shrink"),
"cannot reduce table capacity while entries are referenced"
);
qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 4 },
)
.expect("ack releases references");
assert!(feedback.acknowledged_stream_ids().contains(&4));
assert!(!feedback.cancelled_stream_ids().contains(&4));
assert_eq!(feedback.stream_outstanding_reference_count(4), 0);
assert_eq!(feedback.outstanding_reference_count(), 0);
assert_eq!(feedback.first_error(), None);
context
.set_dynamic_table_capacity(0)
.expect("released references allow shrink");
}
#[test]
fn qpack_decoder_feedback_ack_rejects_unknown_duplicate_and_cancelled() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
let err = qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 4 },
)
.expect_err("unknown stream");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown qpack decoder feedback stream")
);
assert_eq!(feedback.first_error(), Some(&err));
let mut duplicate = QpackDecoderFeedbackState::new();
duplicate
.track_stream_references(&mut context, 8, &[])
.expect("track empty references");
qpack_apply_decoder_instruction(
&mut duplicate,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 8 },
)
.expect("first ack");
let err = qpack_apply_decoder_instruction(
&mut duplicate,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 8 },
)
.expect_err("duplicate ack");
assert_eq!(
err,
H3NativeError::InvalidFrame("duplicate qpack header acknowledgement")
);
assert_eq!(duplicate.first_error(), Some(&err));
let mut cancelled = QpackDecoderFeedbackState::new();
cancelled
.track_stream_references(&mut context, 12, &[])
.expect("track stream");
qpack_apply_decoder_instruction(
&mut cancelled,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::StreamCancellation { stream_id: 12 },
)
.expect("cancel");
let err = qpack_apply_decoder_instruction(
&mut cancelled,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 12 },
)
.expect_err("ack after cancel");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack acknowledgement after stream cancellation")
);
}
#[test]
fn qpack_decoder_feedback_cancellation_releases_references_and_rejects_edges() {
let mut context = QpackContext::new(128);
let mut unknown = QpackDecoderFeedbackState::new();
let err = qpack_apply_decoder_instruction(
&mut unknown,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::StreamCancellation { stream_id: 14 },
)
.expect_err("unknown cancellation stream");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown qpack decoder feedback stream")
);
assert_eq!(unknown.first_error(), Some(&err));
let insertion_id = context
.insert_dynamic_entry("ref".to_string(), "value".to_string())
.expect("insert reference");
let mut feedback = QpackDecoderFeedbackState::new();
feedback
.track_stream_references(&mut context, 16, &[insertion_id])
.expect("track references");
qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::StreamCancellation { stream_id: 16 },
)
.expect("cancel releases references");
assert!(feedback.cancelled_stream_ids().contains(&16));
assert!(!feedback.acknowledged_stream_ids().contains(&16));
assert_eq!(feedback.outstanding_reference_count(), 0);
context
.set_dynamic_table_capacity(0)
.expect("cancellation released references");
let err = qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::StreamCancellation { stream_id: 16 },
)
.expect_err("duplicate cancellation");
assert_eq!(
err,
H3NativeError::InvalidFrame("duplicate qpack stream cancellation")
);
let mut acked = QpackDecoderFeedbackState::new();
acked
.track_stream_references(&mut context, 20, &[])
.expect("track acked stream");
qpack_apply_decoder_instruction(
&mut acked,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 20 },
)
.expect("ack");
let err = qpack_apply_decoder_instruction(
&mut acked,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::StreamCancellation { stream_id: 20 },
)
.expect_err("cancel after ack");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack stream cancellation after acknowledgement")
);
}
#[test]
fn qpack_decoder_feedback_insert_count_increment_boundaries() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("increment one");
assert_eq!(feedback.known_received_count(), 1);
qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 2 },
)
.expect("increment many");
assert_eq!(feedback.known_received_count(), 3);
qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement {
increment: u64::MAX - 3,
},
)
.expect("reach max boundary");
assert_eq!(feedback.known_received_count(), u64::MAX);
let err = qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect_err("overflow");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack known received count overflow")
);
assert_eq!(feedback.known_received_count(), u64::MAX);
assert_eq!(feedback.first_error(), Some(&err));
let mut zero = QpackDecoderFeedbackState::new();
let err = qpack_apply_decoder_instruction(
&mut zero,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 0 },
)
.expect_err("zero increment");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack decoder feedback increment must be non-zero")
);
assert_eq!(zero.known_received_count(), 0);
assert_eq!(zero.first_error(), Some(&err));
}
#[test]
fn qpack_decoder_feedback_static_only_rejects_before_mutation() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
feedback
.track_stream_references(&mut context, 24, &[])
.expect("track stream");
let before = feedback.clone();
let err = qpack_apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::StaticOnly,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 24 },
)
.expect_err("static-only rejects decoder feedback");
assert_eq!(
err,
H3NativeError::QpackPolicy("decoder feedback requires dynamic qpack mode")
);
assert_eq!(feedback.first_error(), Some(&err));
assert_eq!(
feedback.known_received_count(),
before.known_received_count()
);
assert_eq!(
feedback.acknowledged_stream_ids(),
before.acknowledged_stream_ids()
);
assert_eq!(
feedback.cancelled_stream_ids(),
before.cancelled_stream_ids()
);
assert_eq!(
feedback.outstanding_reference_count(),
before.outstanding_reference_count()
);
}
#[test]
fn qpack_decoder_feedback_track_rejects_duplicate_terminal_and_unknown_reference() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
let err = feedback
.track_stream_references(&mut context, 28, &[0])
.expect_err("unknown reference");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown dynamic qpack reference for stream")
);
assert_eq!(feedback.first_error(), Some(&err));
let mut duplicate = QpackDecoderFeedbackState::new();
duplicate
.track_stream_references(&mut context, 32, &[])
.expect("track once");
let err = duplicate
.track_stream_references(&mut context, 32, &[])
.expect_err("duplicate track");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack stream already tracked")
);
let mut terminal = QpackDecoderFeedbackState::new();
terminal
.track_stream_references(&mut context, 36, &[])
.expect("track terminal");
qpack_apply_decoder_instruction(
&mut terminal,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 36 },
)
.expect("ack terminal");
let err = terminal
.track_stream_references(&mut context, 36, &[])
.expect_err("cannot retrack acked stream");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack stream already acknowledged")
);
}
fn qpack_dynamic_wire(
context: &QpackContext,
insertion_id: u64,
) -> Result<Vec<u8>, H3NativeError> {
qpack_encode_field_section_with_context(
&[QpackFieldPlan::DynamicIndex(insertion_id)],
Some(context),
)
}
#[test]
fn qpack_blocked_scheduler_ready_tracks_and_ack_releases_references() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-ready".to_string(), "ready".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("advance known received count");
let status = scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect("schedule ready stream");
assert_eq!(status, QpackBlockedStreamStatus::Ready);
assert_eq!(scheduler.blocked_stream_count(), 0);
let record = scheduler.record(4).expect("record");
assert_eq!(record.required_insert_count(), 1);
assert_eq!(record.base(), 1);
assert_eq!(record.protected_references(), &[insertion_id]);
assert_eq!(
context
.set_dynamic_table_capacity(0)
.expect_err("ready stream still protects referenced entry"),
"cannot reduce table capacity while entries are referenced"
);
scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 4 },
)
.expect("ack releases references");
let record = scheduler.record(4).expect("record after ack");
assert_eq!(record.status(), QpackBlockedStreamStatus::Ready);
assert!(record.protected_references().is_empty());
assert!(feedback.acknowledged_stream_ids().contains(&4));
context
.set_dynamic_table_capacity(0)
.expect("ack released protected entry");
}
#[test]
fn qpack_blocked_scheduler_capacity_zero_and_static_only_fail_closed() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-blocked".to_string(), "blocked".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(0);
let err = scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect_err("zero capacity rejects blocked field section");
assert_eq!(
err,
H3NativeError::QpackPolicy("qpack blocked stream capacity is zero")
);
assert_eq!(scheduler.blocked_stream_count(), 0);
let record = scheduler.record(4).expect("failed record");
assert_eq!(record.status(), QpackBlockedStreamStatus::Failed);
assert_eq!(record.first_failure(), Some(&err));
assert_eq!(scheduler.first_failure(), Some(&err));
assert_eq!(feedback.outstanding_reference_count(), 0);
let mut static_scheduler = QpackBlockedStreamScheduler::new(1);
let err = static_scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::StaticOnly,
8,
&wire,
)
.expect_err("static-only must reject dynamic RIC");
assert_eq!(
err,
H3NativeError::QpackPolicy("required insert count must be zero in static-only mode")
);
assert_eq!(
static_scheduler.record(8).expect("static failure").status(),
QpackBlockedStreamStatus::Failed
);
}
#[test]
fn qpack_blocked_scheduler_blocks_until_insert_count_increment_releases_capacity() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-blocked".to_string(), "blocked".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
assert_eq!(
scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect("first blocked stream"),
QpackBlockedStreamStatus::Blocked
);
assert_eq!(scheduler.blocked_stream_count(), 1);
assert_eq!(
scheduler.record(4).expect("record").blocked_reason(),
Some("required insert count exceeds known received count")
);
let err = scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
8,
&wire,
)
.expect_err("second blocked stream exceeds limit");
assert_eq!(
err,
H3NativeError::QpackPolicy("qpack blocked stream capacity exceeded")
);
assert_eq!(scheduler.blocked_stream_count(), 1);
let unblocked = scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("known received count unblocks stream");
assert_eq!(unblocked, vec![4]);
assert_eq!(scheduler.blocked_stream_count(), 0);
assert_eq!(
scheduler.record(4).expect("record").status(),
QpackBlockedStreamStatus::Ready
);
assert_eq!(
scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
12,
&wire,
)
.expect("capacity released after unblock"),
QpackBlockedStreamStatus::Ready
);
}
#[test]
fn qpack_blocked_scheduler_unblocks_multiple_streams_from_decoder_feedback() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-multi".to_string(), "multi".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(2);
for stream_id in [4, 8] {
let status = scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
stream_id,
&wire,
)
.expect("schedule blocked stream");
assert_eq!(status, QpackBlockedStreamStatus::Blocked);
}
assert_eq!(scheduler.blocked_stream_count(), 2);
assert_eq!(feedback.outstanding_reference_count(), 2);
let unblocked = scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("unblock both streams");
assert_eq!(unblocked, vec![4, 8]);
assert_eq!(scheduler.blocked_stream_count(), 0);
assert_eq!(
scheduler.record(4).expect("stream 4").status(),
QpackBlockedStreamStatus::Ready
);
assert_eq!(
scheduler.record(8).expect("stream 8").status(),
QpackBlockedStreamStatus::Ready
);
assert_eq!(feedback.outstanding_reference_count(), 2);
}
#[test]
fn qpack_blocked_scheduler_encoder_instruction_unblocks_received_field_section() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
let mut wire = Vec::new();
let encoded_ric = qpack_encode_required_insert_count(1, 128).expect("encode RIC");
qpack_encode_prefixed_int(&mut wire, 0, 8, encoded_ric).expect("RIC prefix");
qpack_encode_prefixed_int(&mut wire, 0, 7, 0).expect("base prefix");
qpack_encode_prefixed_int(&mut wire, 0b1000_0000, 6, 0).expect("dynamic relative index 0");
let status = scheduler
.submit_received_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect("received field section blocks before insert");
assert_eq!(status, QpackBlockedStreamStatus::Blocked);
assert_eq!(scheduler.blocked_stream_count(), 1);
assert!(
scheduler
.record(4)
.expect("blocked record")
.protected_references()
.is_empty()
);
let (inserted, unblocked) = scheduler
.apply_encoder_instruction(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-received".to_string(),
value: "value".to_string(),
},
)
.expect("encoder insert unblocks received field section");
assert_eq!(inserted, Some(0));
assert_eq!(unblocked, vec![4]);
assert_eq!(scheduler.blocked_stream_count(), 0);
let record = scheduler.record(4).expect("ready record");
assert_eq!(record.status(), QpackBlockedStreamStatus::Ready);
assert_eq!(record.protected_references(), &[0]);
assert_eq!(feedback.outstanding_reference_count(), 1);
}
#[test]
fn qpack_blocked_scheduler_cancellation_releases_blocked_and_ready_references() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-cancel".to_string(), "cancel".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect("blocked stream");
scheduler
.cancel_stream(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
4,
)
.expect("cancel blocked stream");
assert_eq!(scheduler.blocked_stream_count(), 0);
assert_eq!(
scheduler.record(4).expect("cancelled").status(),
QpackBlockedStreamStatus::Cancelled
);
assert!(
scheduler
.record(4)
.expect("cancelled")
.protected_references()
.is_empty()
);
assert!(feedback.cancelled_stream_ids().contains(&4));
context
.set_dynamic_table_capacity(0)
.expect("blocked cancellation released reference");
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-ready-cancel".to_string(), "cancel".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
8,
&wire,
)
.expect("blocked stream");
scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("unblock stream");
assert_eq!(
context
.set_dynamic_table_capacity(0)
.expect_err("ready stream is still protected before terminal feedback"),
"cannot reduce table capacity while entries are referenced"
);
scheduler
.cancel_stream(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
8,
)
.expect("cancel ready stream");
assert_eq!(
scheduler.record(8).expect("cancelled").status(),
QpackBlockedStreamStatus::Cancelled
);
context
.set_dynamic_table_capacity(0)
.expect("ready cancellation released reference");
}
#[test]
fn qpack_blocked_scheduler_required_insert_count_boundaries_and_failures() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-boundary".to_string(), "boundary".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut equal_feedback = QpackDecoderFeedbackState::new();
qpack_apply_decoder_instruction(
&mut equal_feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("known equals RIC");
let mut equal = QpackBlockedStreamScheduler::new(1);
assert_eq!(
equal
.submit_field_section(
&mut context,
&mut equal_feedback,
H3QpackMode::DynamicTableAllowed,
4,
&wire,
)
.expect("equality is ready"),
QpackBlockedStreamStatus::Ready
);
let mut less_feedback = QpackDecoderFeedbackState::new();
let mut less = QpackBlockedStreamScheduler::new(1);
assert_eq!(
less.submit_field_section(
&mut context,
&mut less_feedback,
H3QpackMode::DynamicTableAllowed,
8,
&wire,
)
.expect("less-than blocks"),
QpackBlockedStreamStatus::Blocked
);
let mut greater_feedback = QpackDecoderFeedbackState::new();
qpack_apply_decoder_instruction(
&mut greater_feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 2 },
)
.expect("known greater than RIC");
let mut greater = QpackBlockedStreamScheduler::new(1);
assert_eq!(
greater
.submit_field_section(
&mut context,
&mut greater_feedback,
H3QpackMode::DynamicTableAllowed,
12,
&wire,
)
.expect("greater-than is ready"),
QpackBlockedStreamStatus::Ready
);
let mut overflow_wire = Vec::new();
qpack_encode_prefixed_int(&mut overflow_wire, 0, 8, 2).expect("encoded RIC");
qpack_encode_prefixed_int(&mut overflow_wire, 0, 7, u64::MAX).expect("overflowing base");
let mut overflow_feedback = QpackDecoderFeedbackState::new();
let mut overflow = QpackBlockedStreamScheduler::new(1);
let err = overflow
.submit_field_section(
&mut context,
&mut overflow_feedback,
H3QpackMode::DynamicTableAllowed,
16,
&overflow_wire,
)
.expect_err("base overflow");
assert_eq!(err, H3NativeError::InvalidFrame("qpack integer overflow"));
assert_eq!(
overflow.record(16).expect("overflow record").status(),
QpackBlockedStreamStatus::Failed
);
let mut evicted_context = QpackContext::new(76);
let old = evicted_context
.insert_dynamic_entry("old".to_string(), "1".to_string())
.expect("insert old");
let evicted = evicted_context
.insert_dynamic_entry("evicted".to_string(), "2".to_string())
.expect("insert evicted");
assert!(evicted_context.dynamic_table_mut().reference_entry(old));
evicted_context
.insert_dynamic_entry("new".to_string(), "3".to_string())
.expect("insert new");
assert_eq!(
qpack_dynamic_entry(evicted_context.dynamic_table(), evicted),
None
);
let mut evicted_wire = Vec::new();
let encoded_ric = qpack_encode_required_insert_count(2, 76).expect("encode RIC");
qpack_encode_prefixed_int(&mut evicted_wire, 0, 8, encoded_ric).expect("RIC prefix");
qpack_encode_prefixed_int(&mut evicted_wire, 0, 7, 0).expect("base prefix");
qpack_encode_prefixed_int(&mut evicted_wire, 0b1000_0000, 6, 0)
.expect("dynamic relative index 0 => evicted absolute 1");
let mut evicted_feedback = QpackDecoderFeedbackState::new();
let mut evicted_scheduler = QpackBlockedStreamScheduler::new(1);
let err = evicted_scheduler
.submit_field_section(
&mut evicted_context,
&mut evicted_feedback,
H3QpackMode::DynamicTableAllowed,
20,
&evicted_wire,
)
.expect_err("evicted reference cannot be protected");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown dynamic qpack reference for stream")
);
}
#[test]
fn qpack_blocked_scheduler_out_of_order_acknowledgement_is_stable_error() {
let mut context = QpackContext::new(128);
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(1);
let err = scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::HeaderAcknowledgement { stream_id: 99 },
)
.expect_err("ack before stream schedule");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown qpack blocked stream acknowledgement")
);
assert_eq!(scheduler.first_failure(), Some(&err));
}
#[test]
fn qpack_blocked_scheduler_e2e_logs_multistream_unblock() {
let mut context = QpackContext::new(128);
let insertion_id = context
.insert_dynamic_entry("x-e2e".to_string(), "e2e".to_string())
.expect("insert dynamic entry");
let wire = qpack_dynamic_wire(&context, insertion_id).expect("encode field section");
let mut feedback = QpackDecoderFeedbackState::new();
let mut scheduler = QpackBlockedStreamScheduler::new(2);
for stream_id in [4, 8] {
scheduler
.submit_field_section(
&mut context,
&mut feedback,
H3QpackMode::DynamicTableAllowed,
stream_id,
&wire,
)
.expect("schedule blocked stream");
}
let before_blocked = scheduler.blocked_stream_count();
let unblocked = scheduler
.apply_decoder_instruction(
&mut feedback,
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackDecoderInstruction::InsertCountIncrement { increment: 1 },
)
.expect("decoder feedback unblocks both streams");
let actual_event = format!("unblocked:{unblocked:?}");
let log = serde_json::json!({
"bead_id": "asupersync-55jlbl",
"scenario_id": "qpack-blocked-multistream-increment-unblock",
"qpack_mode": "DynamicTableAllowed",
"required_insert_count": scheduler.record(4).expect("stream 4").required_insert_count(),
"known_received_count": feedback.known_received_count(),
"blocked_stream_count_before": before_blocked,
"blocked_stream_count_after": scheduler.blocked_stream_count(),
"settings_blocked_streams": scheduler.settings_blocked_streams(),
"expected_event": "unblocked:[4, 8]",
"actual_event": actual_event,
"support_class": "deterministic-unit-e2e",
"verdict": if unblocked == vec![4, 8] { "pass" } else { "fail" },
"first_failure": scheduler.first_failure().map(ToString::to_string),
});
println!("{log}");
assert_eq!(unblocked, vec![4, 8]);
assert_eq!(scheduler.blocked_stream_count(), 0);
assert_eq!(
scheduler.record(4).expect("stream 4").status(),
QpackBlockedStreamStatus::Ready
);
assert_eq!(
scheduler.record(8).expect("stream 8").status(),
QpackBlockedStreamStatus::Ready
);
}
fn qpack_instruction_settings() -> H3Settings {
H3Settings {
qpack_max_table_capacity: Some(128),
qpack_blocked_streams: Some(2),
..H3Settings::default()
}
}
fn qpack_encoder_instruction_bytes(instruction: &QpackEncoderInstruction) -> Vec<u8> {
let mut bytes = Vec::new();
qpack_encode_encoder_instruction(&mut bytes, instruction).expect("encode encoder");
bytes
}
fn qpack_decoder_instruction_bytes(instruction: &QpackDecoderInstruction) -> Vec<u8> {
let mut bytes = Vec::new();
qpack_encode_decoder_instruction(&mut bytes, instruction).expect("encode decoder");
bytes
}
fn qpack_response_status_dynamic_wire() -> Vec<u8> {
let mut wire = Vec::new();
let encoded_ric = qpack_encode_required_insert_count(1, 128).expect("encode RIC");
qpack_encode_prefixed_int(&mut wire, 0, 8, encoded_ric).expect("RIC prefix");
qpack_encode_prefixed_int(&mut wire, 0, 7, 0).expect("base prefix");
qpack_encode_prefixed_int(&mut wire, 0b1000_0000, 6, 0).expect("dynamic relative index 0");
wire
}
fn qpack_instruction_row(
state: &QpackInstructionStreamState,
scenario_id: &str,
required_insert_count: u64,
expected_event: &str,
actual_event: &str,
verdict: &str,
first_failure: Option<String>,
) -> serde_json::Value {
let row = serde_json::json!({
"bead_id": "asupersync-1xxmyo",
"scenario_id": scenario_id,
"qpack_mode": format!("{:?}", state.mode()),
"encoder_stream_id": state.encoder_stream_id(),
"decoder_stream_id": state.decoder_stream_id(),
"required_insert_count": required_insert_count,
"known_received_count": state.known_received_count(),
"blocked_stream_count": state.blocked_stream_count(),
"settings_blocked_streams": state.settings_blocked_streams(),
"expected_event": expected_event,
"actual_event": actual_event,
"support_class": "deterministic-http3-qpack-instruction-proof",
"verdict": verdict,
"first_failure": first_failure,
});
for field in [
"bead_id",
"scenario_id",
"qpack_mode",
"encoder_stream_id",
"decoder_stream_id",
"required_insert_count",
"known_received_count",
"blocked_stream_count",
"settings_blocked_streams",
"expected_event",
"actual_event",
"support_class",
"verdict",
"first_failure",
] {
assert!(row.get(field).is_some(), "missing proof log field {field}");
}
println!("{row}");
row
}
#[test]
fn qpack_instruction_stream_registers_from_h3_state_and_keeps_frame_boundary() {
let mut connection =
H3ConnectionState::with_config(H3ConnectionConfig::default().with_dynamic_qpack());
assert_eq!(
connection
.on_remote_uni_stream_type(3, H3_STREAM_TYPE_QPACK_ENCODER)
.expect("encoder stream type"),
H3UniStreamType::QpackEncoder
);
assert_eq!(
connection
.on_remote_uni_stream_type(7, H3_STREAM_TYPE_QPACK_DECODER)
.expect("decoder stream type"),
H3UniStreamType::QpackDecoder
);
assert_eq!(connection.qpack_encoder_stream_id(), Some(3));
assert_eq!(connection.qpack_decoder_stream_id(), Some(7));
let err = connection
.on_uni_stream_frame(3, &H3Frame::Data(vec![1]))
.expect_err("qpack encoder stream must not parse h3 frames");
assert_eq!(
err,
H3NativeError::StreamProtocol("qpack streams carry instructions, not h3 frames")
);
let err = connection
.on_uni_stream_frame(7, &H3Frame::Headers(vec![0]))
.expect_err("qpack decoder stream must not parse h3 frames");
assert_eq!(
err,
H3NativeError::StreamProtocol("qpack streams carry instructions, not h3 frames")
);
let mut qpack = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("qpack instruction state");
assert_eq!(
connection
.register_qpack_instruction_stream(&mut qpack, 3)
.expect("register encoder"),
H3UniStreamType::QpackEncoder
);
assert_eq!(
connection
.register_qpack_instruction_stream(&mut qpack, 7)
.expect("register decoder"),
H3UniStreamType::QpackDecoder
);
let insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-h3-boundary".to_string(),
value: "value".to_string(),
});
let outcome = connection
.feed_qpack_instruction_stream_bytes(&mut qpack, 3, &insert)
.expect("encoder instruction bytes");
assert_eq!(outcome.instructions_processed(), 1);
assert_eq!(outcome.inserted_entry_ids(), &[0]);
}
#[test]
fn qpack_instruction_stream_registration_and_wrong_stream_errors() {
let mut qpack = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("qpack state");
qpack
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder registration");
qpack
.register_stream(7, H3UniStreamType::QpackDecoder)
.expect("decoder registration");
let err = qpack
.register_stream(11, H3UniStreamType::QpackEncoder)
.expect_err("duplicate encoder stream");
assert_eq!(
err,
H3NativeError::StreamProtocol("duplicate remote qpack encoder stream")
);
let err = qpack
.feed_encoder_stream_bytes(99, &[])
.expect_err("unknown qpack stream id");
assert_eq!(
err,
H3NativeError::StreamProtocol("unknown qpack instruction stream")
);
let decoder =
qpack_decoder_instruction_bytes(&QpackDecoderInstruction::InsertCountIncrement {
increment: 1,
});
let err = qpack
.feed_instruction_stream_bytes(3, H3UniStreamType::QpackDecoder, &decoder)
.expect_err("decoder instruction on encoder stream");
assert_eq!(
err,
H3NativeError::StreamProtocol(
"qpack instruction type does not match registered stream"
)
);
let err = qpack
.register_stream(19, H3UniStreamType::Control)
.expect_err("non-qpack stream rejected");
assert_eq!(
err,
H3NativeError::StreamProtocol("qpack instruction stream requires qpack stream type")
);
}
#[test]
fn qpack_instruction_stream_dynamic_sequence_blocks_unblocks_and_decodes() {
let mut connection =
H3ConnectionState::with_config(H3ConnectionConfig::default().with_dynamic_qpack());
connection
.on_remote_uni_stream_type(3, H3_STREAM_TYPE_QPACK_ENCODER)
.expect("encoder stream");
connection
.on_remote_uni_stream_type(7, H3_STREAM_TYPE_QPACK_DECODER)
.expect("decoder stream");
let mut qpack = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("qpack state");
connection
.register_qpack_instruction_stream(&mut qpack, 3)
.expect("register encoder");
connection
.register_qpack_instruction_stream(&mut qpack, 7)
.expect("register decoder");
let field_section = qpack_response_status_dynamic_wire();
let status = qpack
.submit_received_field_section(4, &field_section)
.expect("field section blocks before insert");
assert_eq!(status, QpackBlockedStreamStatus::Blocked);
assert_eq!(qpack.blocked_stream_count(), 1);
let insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: ":status".to_string(),
value: "200".to_string(),
});
let outcome = connection
.feed_qpack_instruction_stream_bytes(&mut qpack, 3, &insert)
.expect("encoder insert unblocks field section");
assert_eq!(outcome.inserted_entry_ids(), &[0]);
assert_eq!(outcome.unblocked_stream_ids(), &[4]);
assert_eq!(qpack.blocked_stream_count(), 0);
let response = qpack_decode_response_field_section(
&field_section,
H3QpackMode::DynamicTableAllowed,
Some(qpack.context()),
)
.expect("decode unblocked response field section");
assert_eq!(response.status, 200);
let increment =
qpack_decoder_instruction_bytes(&QpackDecoderInstruction::InsertCountIncrement {
increment: 1,
});
let outcome = connection
.feed_qpack_instruction_stream_bytes(&mut qpack, 7, &increment)
.expect("decoder feedback increment");
assert_eq!(outcome.instructions_processed(), 1);
assert_eq!(qpack.known_received_count(), 1);
}
#[test]
fn qpack_instruction_stream_proof_runner_scenarios_log_required_fields() {
let mut rows = Vec::new();
let mut static_only =
QpackInstructionStreamState::new(H3QpackMode::StaticOnly, 0, 0).expect("static qpack");
static_only
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
let insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-static".to_string(),
value: "reject".to_string(),
});
let err = static_only
.feed_encoder_stream_bytes(3, &insert)
.expect_err("static-only rejects encoder stream instructions");
rows.push(qpack_instruction_row(
&static_only,
"static-only-rejection",
0,
"qpack-policy-error",
&err.to_string(),
"pass",
Some(err.to_string()),
));
let mut encoder_rt = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
encoder_rt
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
let insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-roundtrip".to_string(),
value: "ok".to_string(),
});
let outcome = encoder_rt
.feed_encoder_stream_bytes(3, &insert)
.expect("encoder roundtrip");
rows.push(qpack_instruction_row(
&encoder_rt,
"encoder-instruction-roundtrip",
0,
"inserted:[0]",
&format!("inserted:{:?}", outcome.inserted_entry_ids()),
if outcome.inserted_entry_ids() == [0] {
"pass"
} else {
"fail"
},
None,
));
let mut decoder_rt = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
decoder_rt
.register_stream(7, H3UniStreamType::QpackDecoder)
.expect("decoder stream");
let increment =
qpack_decoder_instruction_bytes(&QpackDecoderInstruction::InsertCountIncrement {
increment: 1,
});
decoder_rt
.feed_decoder_stream_bytes(7, &increment)
.expect("decoder feedback");
rows.push(qpack_instruction_row(
&decoder_rt,
"decoder-feedback-roundtrip",
0,
"known-received-count:1",
&format!("known-received-count:{}", decoder_rt.known_received_count()),
if decoder_rt.known_received_count() == 1 {
"pass"
} else {
"fail"
},
None,
));
let mut blocked = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
blocked
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
blocked
.submit_received_field_section(4, &qpack_response_status_dynamic_wire())
.expect("blocked field section");
let insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: ":status".to_string(),
value: "200".to_string(),
});
let outcome = blocked
.feed_encoder_stream_bytes(3, &insert)
.expect("encoder instruction unblocks");
rows.push(qpack_instruction_row(
&blocked,
"blocked-then-unblocked-stream",
1,
"unblocked:[4]",
&format!("unblocked:{:?}", outcome.unblocked_stream_ids()),
if outcome.unblocked_stream_ids() == [4] {
"pass"
} else {
"fail"
},
None,
));
let mut cancelled = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
cancelled
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
cancelled
.register_stream(7, H3UniStreamType::QpackDecoder)
.expect("decoder stream");
let cancel_insert =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-cancel-blocked".to_string(),
value: "value".to_string(),
});
let inserted = cancelled
.feed_encoder_stream_bytes(3, &cancel_insert)
.expect("insert cancellation reference");
let cancel_wire = qpack_dynamic_wire(cancelled.context(), inserted.inserted_entry_ids()[0])
.expect("wire");
cancelled
.submit_field_section(8, &cancel_wire)
.expect("outbound blocked field section");
let cancel =
qpack_decoder_instruction_bytes(&QpackDecoderInstruction::StreamCancellation {
stream_id: 8,
});
cancelled
.feed_decoder_stream_bytes(7, &cancel)
.expect("cancel blocked stream");
let cancel_event = format!(
"cancelled:{:?}",
cancelled
.blocked_scheduler()
.record(8)
.expect("cancelled record")
.status()
);
rows.push(qpack_instruction_row(
&cancelled,
"cancellation-while-blocked",
1,
"cancelled:Cancelled",
&cancel_event,
if cancel_event == "cancelled:Cancelled" {
"pass"
} else {
"fail"
},
None,
));
let mut capacity =
QpackInstructionStreamState::new(H3QpackMode::DynamicTableAllowed, 64, 1)
.expect("capacity-limited qpack");
capacity
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
let set_too_large =
qpack_encoder_instruction_bytes(&QpackEncoderInstruction::SetDynamicTableCapacity {
capacity: 128,
});
let err = capacity
.feed_encoder_stream_bytes(3, &set_too_large)
.expect_err("capacity exceeds setting");
rows.push(qpack_instruction_row(
&capacity,
"capacity-exceeded",
0,
"capacity-policy-error",
&err.to_string(),
"pass",
Some(err.to_string()),
));
let mut malformed = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
malformed
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
let err = malformed
.feed_encoder_stream_bytes(3, &[0b0100_0001])
.expect_err("truncated encoder instruction");
rows.push(qpack_instruction_row(
&malformed,
"malformed-instruction",
0,
"malformed-error",
&err.to_string(),
"pass",
Some(err.to_string()),
));
let mut wrong_stream = QpackInstructionStreamState::from_settings(
H3QpackMode::DynamicTableAllowed,
&qpack_instruction_settings(),
)
.expect("dynamic qpack");
wrong_stream
.register_stream(3, H3UniStreamType::QpackEncoder)
.expect("encoder stream");
let increment =
qpack_decoder_instruction_bytes(&QpackDecoderInstruction::InsertCountIncrement {
increment: 1,
});
let err = wrong_stream
.feed_instruction_stream_bytes(3, H3UniStreamType::QpackDecoder, &increment)
.expect_err("wrong stream type");
rows.push(qpack_instruction_row(
&wrong_stream,
"wrong-stream-instruction",
0,
"wrong-stream-error",
&err.to_string(),
"pass",
Some(err.to_string()),
));
let expected = [
"static-only-rejection",
"encoder-instruction-roundtrip",
"decoder-feedback-roundtrip",
"blocked-then-unblocked-stream",
"cancellation-while-blocked",
"capacity-exceeded",
"malformed-instruction",
"wrong-stream-instruction",
];
assert_eq!(rows.len(), expected.len());
for scenario_id in expected {
let row = rows
.iter()
.find(|row| row["scenario_id"] == scenario_id)
.expect("scenario row");
assert_eq!(row["bead_id"], "asupersync-1xxmyo");
assert_eq!(row["verdict"], "pass");
}
}
#[test]
fn qpack_encoder_state_static_only_rejects_without_mutation() {
let mut context = QpackContext::new(128);
let before = (
context.dynamic_table().len(),
context.dynamic_table().size(),
context.dynamic_table().insertion_counter(),
context.dynamic_table().evicted_count(),
);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::StaticOnly,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-test".to_string(),
value: "value".to_string(),
},
)
.expect_err("static-only mode must reject encoder instructions");
assert_eq!(
err,
H3NativeError::QpackPolicy("encoder instructions require dynamic qpack mode")
);
assert_eq!(
before,
(
context.dynamic_table().len(),
context.dynamic_table().size(),
context.dynamic_table().insertion_counter(),
context.dynamic_table().evicted_count(),
)
);
}
#[test]
fn qpack_encoder_state_set_capacity_grows_shrinks_and_zeroes() {
let mut context = QpackContext::new(128);
context
.insert_dynamic_entry("alpha".to_string(), "one".to_string())
.expect("insert alpha");
context
.insert_dynamic_entry("beta".to_string(), "two".to_string())
.expect("insert beta");
qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 96 },
)
.expect("shrink without eviction");
assert_eq!(context.dynamic_table().capacity(), 96);
assert_eq!(context.dynamic_table().len(), 2);
qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 40 },
)
.expect("shrink with eviction");
assert_eq!(context.dynamic_table().capacity(), 40);
assert_eq!(context.dynamic_table().len(), 1);
assert_eq!(context.dynamic_table().size(), 39);
assert_eq!(context.dynamic_table().evicted_count(), 1);
assert!(qpack_dynamic_entry(context.dynamic_table(), 0).is_none());
assert_eq!(
qpack_dynamic_entry(context.dynamic_table(), 1),
Some(("beta", "two"))
);
qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 0 },
)
.expect("zero capacity evicts unreferenced entries");
assert_eq!(context.dynamic_table().capacity(), 0);
assert_eq!(context.dynamic_table().len(), 0);
assert_eq!(context.dynamic_table().size(), 0);
}
#[test]
fn qpack_encoder_state_set_capacity_rejects_peer_limit_and_referenced_shrink() {
let mut context = QpackContext::new(128);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 129 },
)
.expect_err("capacity exceeds peer maximum");
assert_eq!(
err,
H3NativeError::QpackPolicy("qpack dynamic table capacity exceeds peer limit")
);
assert_eq!(context.dynamic_table().capacity(), 128);
let referenced = context
.insert_dynamic_entry("ref".to_string(), "value".to_string())
.expect("insert referenced");
let _victim = context
.insert_dynamic_entry("victim".to_string(), "value".to_string())
.expect("insert victim");
assert!(context.dynamic_table_mut().reference_entry(referenced));
let before = (
context.dynamic_table().len(),
context.dynamic_table().size(),
context.dynamic_table().capacity(),
context.dynamic_table().evicted_count(),
);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 39 },
)
.expect_err("referenced entry prevents shrink");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"qpack dynamic table capacity shrink blocked by referenced entries"
)
);
assert_eq!(
before,
(
context.dynamic_table().len(),
context.dynamic_table().size(),
context.dynamic_table().capacity(),
context.dynamic_table().evicted_count(),
)
);
}
#[test]
fn qpack_encoder_state_inserts_static_dynamic_and_literal_names() {
let mut context = QpackContext::new(256);
let static_insert = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(0),
value: "www.example.com".to_string(),
},
)
.expect("static-name insert")
.expect("insert id");
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), static_insert),
Some((":authority", "www.example.com"))
);
let literal_insert = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-base".to_string(),
value: String::new(),
},
)
.expect("literal insert")
.expect("insert id");
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), literal_insert),
Some(("x-base", ""))
);
let dynamic_insert = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(0),
value: "next".to_string(),
},
)
.expect("dynamic-name insert")
.expect("insert id");
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), dynamic_insert),
Some(("x-base", "next"))
);
}
#[test]
fn qpack_encoder_state_insert_rejects_unknown_names_and_oversized_entries() {
let mut context = QpackContext::new(33);
let exact = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "x".to_string(),
value: String::new(),
},
)
.expect("exact capacity insert")
.expect("insert id");
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), exact),
Some(("x", ""))
);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "xx".to_string(),
value: String::new(),
},
)
.expect_err("entry exceeds capacity");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack dynamic table entry exceeds capacity")
);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(999),
value: "value".to_string(),
},
)
.expect_err("unknown static name");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown static qpack name index")
);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(9),
value: "value".to_string(),
},
)
.expect_err("unknown dynamic name");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown dynamic qpack name index")
);
}
#[test]
fn qpack_encoder_state_duplicate_handles_pressure_and_evicted_targets() {
let mut context = QpackContext::new(100);
let old = context
.insert_dynamic_entry("old".to_string(), "1".to_string())
.expect("insert old");
let new = context
.insert_dynamic_entry("new".to_string(), "2".to_string())
.expect("insert new");
let duplicate = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::Duplicate { index: 0 },
)
.expect("duplicate newest")
.expect("insert id");
assert!(qpack_entry_by_insertion_id(context.dynamic_table(), old).is_none());
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), new),
Some(("new", "2"))
);
assert_eq!(
qpack_entry_by_insertion_id(context.dynamic_table(), duplicate),
Some(("new", "2"))
);
assert_eq!(context.dynamic_table().evicted_count(), 1);
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::Duplicate { index: 2 },
)
.expect_err("duplicate target was evicted");
assert_eq!(
err,
H3NativeError::InvalidFrame("unknown dynamic qpack duplicate index")
);
}
#[test]
fn qpack_encoder_state_insert_fails_when_all_entries_are_referenced() {
let mut context = QpackContext::new(75);
let first = context
.insert_dynamic_entry("a".to_string(), "1".to_string())
.expect("insert first");
let second = context
.insert_dynamic_entry("b".to_string(), "2".to_string())
.expect("insert second");
assert!(context.dynamic_table_mut().reference_entry(first));
assert!(context.dynamic_table_mut().reference_entry(second));
let err = qpack_apply_encoder_instruction(
&mut context,
H3QpackMode::DynamicTableAllowed,
&QpackEncoderInstruction::InsertWithoutNameReference {
name: "c".to_string(),
value: "3".to_string(),
},
)
.expect_err("referenced entries block eviction");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack dynamic table insert blocked by referenced entries")
);
assert_eq!(context.dynamic_table().insertion_counter(), 2);
assert_eq!(context.dynamic_table().len(), 2);
}
#[test]
fn qpack_encoder_state_instruction_sequences_are_deterministic() {
let instructions = [
QpackEncoderInstruction::SetDynamicTableCapacity { capacity: 96 },
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Static(0),
value: "site".to_string(),
},
QpackEncoderInstruction::InsertWithoutNameReference {
name: "x-test".to_string(),
value: "one".to_string(),
},
QpackEncoderInstruction::InsertWithNameReference {
name: QpackInstructionNameRef::Dynamic(0),
value: "two".to_string(),
},
QpackEncoderInstruction::Duplicate { index: 0 },
];
let mut left = QpackContext::new(128);
let mut right = QpackContext::new(128);
for instruction in &instructions {
qpack_apply_encoder_instruction(
&mut left,
H3QpackMode::DynamicTableAllowed,
instruction,
)
.expect("left sequence step");
qpack_apply_encoder_instruction(
&mut right,
H3QpackMode::DynamicTableAllowed,
instruction,
)
.expect("right sequence step");
}
let left_entries: Vec<_> = left
.dynamic_table()
.entries
.iter()
.map(|entry| {
(
entry.insertion_id(),
entry.name().to_string(),
entry.value().to_string(),
)
})
.collect();
let right_entries: Vec<_> = right
.dynamic_table()
.entries
.iter()
.map(|entry| {
(
entry.insertion_id(),
entry.name().to_string(),
entry.value().to_string(),
)
})
.collect();
assert_eq!(left_entries, right_entries);
assert_eq!(
left.dynamic_table().insertion_counter(),
right.dynamic_table().insertion_counter()
);
assert_eq!(left.dynamic_table().size(), right.dynamic_table().size());
assert_eq!(
left.dynamic_table().evicted_count(),
right.dynamic_table().evicted_count()
);
}
#[test]
fn request_missing_scheme_error() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: None,
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("missing :scheme")
);
}
#[test]
fn request_missing_path_error() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: None,
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("missing :path")
);
}
#[test]
fn request_empty_method_error() {
let pseudo = H3PseudoHeaders {
method: Some(String::new()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("empty :method")
);
}
#[test]
fn request_invalid_method_token_error() {
let pseudo = H3PseudoHeaders {
method: Some("GET POST".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(":method must be a valid HTTP token")
);
}
#[test]
fn request_method_accepts_rfc5234_tchar_vector() {
let pseudo = H3PseudoHeaders {
method: Some("M!#$%&'*+-.^_`|~09".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
validate_request_pseudo_headers(&pseudo).expect("RFC 5234 tchar vector must be valid");
}
#[test]
fn request_empty_scheme_error() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some(String::new()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("empty :scheme")
);
}
#[test]
fn request_empty_path_error() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some(String::new()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("empty :path")
);
}
#[test]
fn connect_empty_authority_error() {
let pseudo = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some(String::new()),
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("CONNECT request missing :authority")
);
}
#[test]
fn connect_invalid_authority_value_error() {
let pseudo = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("example.com\r\nx-bad: 1".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn connect_rejects_authority_whitespace() {
let pseudo = H3PseudoHeaders {
method: Some("CONNECT".to_string()),
authority: Some("example.com :443".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
":authority must be RFC authority-form without whitespace"
)
);
}
#[test]
fn request_authority_rfc4291_ipv6_literal_vector() {
let valid = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("[2001:db8::8:800:200c:417a]:443".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
validate_request_pseudo_headers(&valid).expect("compressed RFC 4291 IPv6 literal valid");
let invalid = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("[2001:db8:::1]:443".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&invalid).expect_err("must reject bad IPv6");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(":authority has invalid IPv6 literal")
);
}
#[test]
fn request_rejects_empty_authority_when_present() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some(String::new()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("empty :authority")
);
}
#[test]
fn request_rejects_non_origin_form_path() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("noslash".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(":path must start with /")
);
}
#[test]
fn request_rejects_asterisk_form_for_non_options() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("*".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader("asterisk-form :path requires OPTIONS")
);
}
#[test]
fn options_allows_asterisk_form_path() {
let pseudo = H3PseudoHeaders {
method: Some("OPTIONS".to_string()),
scheme: Some("https".to_string()),
path: Some("*".to_string()),
status: None,
..H3PseudoHeaders::default()
};
validate_request_pseudo_headers(&pseudo).expect("OPTIONS * is valid");
}
#[test]
fn request_rejects_invalid_scheme_syntax() {
let pseudo = H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("1https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
};
let err = validate_request_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(":scheme must be a valid URI scheme")
);
}
#[test]
fn request_head_constructor_rejects_pseudo_header_in_regular_headers() {
let err = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
},
vec![(":status".to_string(), "200".to_string())],
)
.expect_err("must reject pseudo header contamination");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(
"pseudo headers must not appear in regular header list"
)
);
}
#[test]
fn request_head_constructor_rejects_invalid_regular_header_value() {
let err = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
},
vec![("x-test".to_string(), "bad\r\nvalue".to_string())],
)
.expect_err("must reject invalid regular header value");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn request_head_constructor_rejects_invalid_authority_pseudo_value() {
let err = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com\r\nx-bad: 1".to_string()),
path: Some("/".to_string()),
status: None,
protocol: None,
},
vec![],
)
.expect_err("must reject invalid pseudo header value");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn request_head_constructor_rejects_invalid_path_pseudo_value() {
let err = H3RequestHead::new(
H3PseudoHeaders {
method: Some("GET".to_string()),
scheme: Some("https".to_string()),
authority: Some("example.com".to_string()),
path: Some("/ok\nbad".to_string()),
status: None,
protocol: None,
},
vec![],
)
.expect_err("must reject invalid pseudo header value");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn response_with_method_contaminant_error() {
let pseudo = H3PseudoHeaders {
status: Some(200),
method: Some("GET".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_response_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers"
)
);
}
#[test]
fn response_head_constructor_rejects_request_pseudo_header_in_regular_headers() {
let err = H3ResponseHead::new(200, vec![(":path".to_string(), "/".to_string())])
.expect_err("must reject request pseudo contamination");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers"
)
);
}
#[test]
fn response_head_constructor_rejects_invalid_regular_header_name() {
let err = H3ResponseHead::new(200, vec![("Bad-Header".to_string(), "ok".to_string())])
.expect_err("must reject uppercase regular header");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name must be lowercase in HTTP/3")
);
}
#[test]
fn response_with_scheme_contaminant_error() {
let pseudo = H3PseudoHeaders {
status: Some(200),
scheme: Some("https".to_string()),
..H3PseudoHeaders::default()
};
let err = validate_response_pseudo_headers(&pseudo).expect_err("must fail");
assert_eq!(
err,
H3NativeError::InvalidResponsePseudoHeader(
"response must not include request pseudo headers"
)
);
}
#[test]
fn qpack_static_table_entries_2_through_14_present() {
assert_eq!(qpack_static_entry(2), Some(("age", "0")));
assert_eq!(qpack_static_entry(4), Some(("content-length", "0")));
assert_eq!(qpack_static_entry(5), Some(("cookie", "")));
assert_eq!(qpack_static_entry(6), Some(("date", "")));
assert_eq!(qpack_static_entry(12), Some(("location", "")));
assert_eq!(qpack_static_entry(14), Some(("set-cookie", "")));
}
#[test]
fn qpack_static_table_entries_29_through_62_present() {
assert_eq!(qpack_static_entry(29), Some(("accept", "*/*")));
assert_eq!(
qpack_static_entry(31),
Some(("accept-encoding", "gzip, deflate, br"))
);
assert_eq!(
qpack_static_entry(46),
Some(("content-type", "application/json"))
);
assert_eq!(qpack_static_entry(53), Some(("content-type", "text/plain")));
assert_eq!(qpack_static_entry(59), Some(("vary", "accept-encoding")));
assert_eq!(
qpack_static_entry(62),
Some(("x-xss-protection", "1; mode=block"))
);
}
#[test]
fn qpack_static_table_entries_72_through_98_present() {
assert_eq!(qpack_static_entry(72), Some(("accept-language", "")));
assert_eq!(qpack_static_entry(83), Some(("alt-svc", "clear")));
assert_eq!(qpack_static_entry(90), Some(("origin", "")));
assert_eq!(qpack_static_entry(95), Some(("user-agent", "")));
assert_eq!(
qpack_static_entry(98),
Some(("x-frame-options", "sameorigin"))
);
assert_eq!(qpack_static_entry(99), None);
}
#[test]
fn header_name_rejects_uppercase() {
let err = validate_header_name("Content-Type").expect_err("must reject uppercase");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name must be lowercase in HTTP/3")
);
}
#[test]
fn header_name_rejects_null_byte() {
let err = validate_header_name("x-\0-bad").expect_err("must reject null");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name contains invalid character")
);
}
#[test]
fn header_name_rejects_space() {
let err = validate_header_name("x bad").expect_err("must reject space");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name contains invalid character")
);
}
#[test]
fn header_name_rejects_embedded_colon_in_regular_header() {
let err = validate_header_name("x:bad").expect_err("must reject embedded colon");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name contains invalid character")
);
}
#[test]
fn header_name_accepts_valid_token() {
validate_header_name("content-type").expect("valid");
validate_header_name("x-custom_header.1").expect("valid");
validate_header_name(":method").expect("pseudo header valid");
}
#[test]
fn header_value_rejects_crlf() {
let err = validate_header_value("value\r\ninjected").expect_err("must reject CRLF");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn header_value_rejects_null() {
let err = validate_header_value("value\0null").expect_err("must reject null");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn header_value_accepts_normal_text() {
validate_header_value("application/json").expect("valid");
validate_header_value("").expect("empty is valid");
validate_header_value("value with spaces and tabs\tare ok").expect("valid");
}
#[test]
fn unknown_uni_stream_type_accepted_and_data_ignored() {
let mut c = H3ConnectionState::new();
let kind = c
.on_remote_uni_stream_type(3, 0x42)
.expect("unknown type must be accepted per RFC 9114 §6.2");
assert_eq!(kind, H3UniStreamType::Unknown(0x42));
c.on_uni_stream_frame(3, &H3Frame::Data(vec![1, 2, 3]))
.expect("data on unknown stream must be accepted");
}
#[test]
fn request_decode_rejects_uppercase_header_name() {
let fields = vec![
(":method".to_string(), "GET".to_string()),
(":scheme".to_string(), "https".to_string()),
(":path".to_string(), "/".to_string()),
("Accept".to_string(), "*/*".to_string()),
];
let err = header_fields_to_request_head(&fields).expect_err("must reject uppercase");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name must be lowercase in HTTP/3")
);
}
#[test]
fn request_decode_rejects_embedded_colon_in_regular_header_name() {
let fields = vec![
(":method".to_string(), "GET".to_string()),
(":scheme".to_string(), "https".to_string()),
(":path".to_string(), "/".to_string()),
("x:bad".to_string(), "*/*".to_string()),
];
let err = header_fields_to_request_head(&fields).expect_err("must reject embedded colon");
assert_eq!(
err,
H3NativeError::InvalidFrame("header field name contains invalid character")
);
}
#[test]
fn request_decode_rejects_invalid_method_token() {
let fields = vec![
(":method".to_string(), "GET POST".to_string()),
(":scheme".to_string(), "https".to_string()),
(":path".to_string(), "/".to_string()),
];
let err = header_fields_to_request_head(&fields).expect_err("must reject invalid method");
assert_eq!(
err,
H3NativeError::InvalidRequestPseudoHeader(":method must be a valid HTTP token")
);
}
#[test]
fn response_decode_rejects_crlf_in_header_value() {
let fields = vec![
(":status".to_string(), "200".to_string()),
("x-injected".to_string(), "foo\r\nbar".to_string()),
];
let err = header_fields_to_response_head(&fields).expect_err("must reject CRLF");
assert_eq!(
err,
H3NativeError::InvalidFrame(
"header field value contains forbidden character (NUL, CR, or LF)"
)
);
}
#[test]
fn qpack_decode_string_rejects_prefix_len_8() {
let err = qpack_decode_string(0xFF, 8, &[]).expect_err("must reject prefix_len=8");
assert_eq!(
err,
H3NativeError::InvalidFrame("qpack string prefix length must be less than 8")
);
}
#[test]
fn settings_rejects_h2_reserved_ids() {
for reserved_id in [0x00u64, 0x02, 0x03, 0x04, 0x05] {
let mut payload = Vec::new();
encode_varint(reserved_id, &mut payload).expect("varint");
encode_varint(42, &mut payload).expect("varint");
let err = H3Settings::decode_payload(&payload).expect_err(&format!(
"must reject H2 reserved setting 0x{reserved_id:02x}"
));
assert_eq!(err, H3NativeError::InvalidSettingValue(reserved_id));
}
}
#[test]
fn settings_encode_rejects_h2_reserved_unknown_ids() {
for reserved_id in [0x00u64, 0x02, 0x03, 0x04, 0x05] {
let settings = H3Settings {
unknown: vec![UnknownSetting {
id: reserved_id,
value: 42,
}],
..H3Settings::default()
};
let err = settings
.encode_payload(&mut Vec::new())
.expect_err("must reject reserved HTTP/2 setting IDs on encode");
assert_eq!(err, H3NativeError::InvalidSettingValue(reserved_id));
}
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_roundtrip() {
let frame = H3Frame::Datagram {
quarter_stream_id: 42,
payload: vec![0xCA, 0xFE, 0xBA, 0xBE],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_roundtrip_empty_payload() {
let frame = H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_roundtrip_large_quarter_stream_id() {
let frame = H3Frame::Datagram {
quarter_stream_id: (1u64 << 62) - 1, payload: vec![0x01, 0x02],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_golden_test_simple() {
let frame = H3Frame::Datagram {
quarter_stream_id: 5,
payload: vec![0x01, 0x02, 0x03, 0x04],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let expected = vec![0x30u8, 0x05, 0x05, 0x01, 0x02, 0x03, 0x04];
assert_eq!(buf, expected, "DATAGRAM frame encoding mismatch");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_golden_test_zero_quarter_stream_id() {
let frame = H3Frame::Datagram {
quarter_stream_id: 0,
payload: vec![0xFF],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let expected = vec![0x30u8, 0x02, 0x00, 0xFF];
assert_eq!(buf, expected);
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_large_payload() {
let large_payload = vec![0x42u8; 1024];
let frame = H3Frame::Datagram {
quarter_stream_id: 1000,
payload: large_payload.clone(),
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (decoded, consumed) = H3Frame::decode(&buf, &test_config()).expect("decode");
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
#[test]
fn datagram_frame_forbidden_on_control_stream() {
let frame = H3Frame::Datagram {
quarter_stream_id: 10,
payload: vec![0xAA, 0xBB],
};
let mut state = H3ControlState::new();
state
.on_remote_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
let err = state
.on_remote_control_frame(&frame)
.expect_err("must reject DATAGRAM on control stream");
assert_eq!(
err,
H3NativeError::ControlProtocol("frame type not allowed on control stream")
);
}
#[cfg(feature = "http3")]
#[test]
fn settings_h3_datagram_enabled() {
let settings = H3Settings {
qpack_max_table_capacity: Some(4096),
max_field_section_size: Some(8192),
qpack_blocked_streams: None,
enable_connect_protocol: Some(false),
h3_datagram: Some(true), unknown: vec![],
};
let mut buf = Vec::new();
settings.encode_payload(&mut buf).expect("encode settings");
let decoded = H3Settings::decode_payload(&buf).expect("decode settings");
assert_eq!(decoded.h3_datagram, Some(true));
}
#[cfg(feature = "http3")]
#[test]
fn settings_h3_datagram_disabled() {
let settings = H3Settings {
qpack_max_table_capacity: None,
max_field_section_size: None,
qpack_blocked_streams: None,
enable_connect_protocol: None,
h3_datagram: Some(false), unknown: vec![],
};
let mut buf = Vec::new();
settings.encode_payload(&mut buf).expect("encode settings");
let decoded = H3Settings::decode_payload(&buf).expect("decode settings");
assert_eq!(decoded.h3_datagram, Some(false));
}
#[cfg(feature = "http3")]
#[test]
fn settings_h3_datagram_not_negotiated() {
let settings = H3Settings {
qpack_max_table_capacity: Some(1024),
max_field_section_size: None,
qpack_blocked_streams: None,
enable_connect_protocol: None,
h3_datagram: None, unknown: vec![],
};
let mut buf = Vec::new();
settings.encode_payload(&mut buf).expect("encode settings");
let decoded = H3Settings::decode_payload(&buf).expect("decode settings");
assert_eq!(decoded.h3_datagram, None);
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_context_id_boundary_values() {
let test_cases = vec![
0u64, 1, 63, 64, 16383, 16384, 1073741823, (1u64 << 30), (1u64 << 62) - 1, ];
for quarter_stream_id in test_cases {
let frame = H3Frame::Datagram {
quarter_stream_id,
payload: vec![0x00, 0x01],
};
let mut buf = Vec::new();
frame
.encode(&mut buf)
.unwrap_or_else(|_| panic!("encode quarter_stream_id={quarter_stream_id}"));
let (decoded, consumed) = H3Frame::decode(&buf, &test_config())
.unwrap_or_else(|_| panic!("decode quarter_stream_id={quarter_stream_id}"));
assert_eq!(decoded, frame);
assert_eq!(consumed, buf.len());
}
}
#[test]
fn datagram_frame_decode_truncated_quarter_stream_id() {
let mut buf = Vec::new();
encode_varint(H3_FRAME_DATAGRAM, &mut buf).expect("frame type");
encode_varint(2, &mut buf).expect("frame length");
buf.push(0x80);
let err = H3Frame::decode(&buf, &test_config())
.expect_err("must reject truncated quarter_stream_id");
assert_eq!(err, H3NativeError::InvalidFrame("quarter stream id varint"));
}
#[test]
fn datagram_frame_decode_truncated_payload() {
let mut buf = Vec::new();
encode_varint(H3_FRAME_DATAGRAM, &mut buf).expect("frame type");
encode_varint(10, &mut buf).expect("frame length - claims 10 bytes");
encode_varint(5, &mut buf).expect("quarter_stream_id");
buf.extend_from_slice(&[0x01, 0x02]);
let err = H3Frame::decode(&buf, &test_config()).expect_err("must reject truncated payload");
assert_eq!(
err,
H3NativeError::InvalidFrame("insufficient frame payload")
);
}
#[cfg(feature = "http3")]
#[test]
fn datagram_frame_varint_quarter_stream_id_encoding() {
let test_cases = vec![
(0u64, vec![0x00]), (42, vec![0x2A]), (300, vec![0x41, 0x2C]), (100000, vec![0x80, 0x01, 0x86, 0xA0]), ];
for (quarter_stream_id, expected_varint) in test_cases {
let frame = H3Frame::Datagram {
quarter_stream_id,
payload: vec![0xFF],
};
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode");
let (_, type_len) = decode_varint(&buf).expect("frame type");
let (declared_length, len_len) = decode_varint(&buf[type_len..]).expect("frame length");
let quarter_stream_id_start = type_len + len_len;
let (decoded_id, id_len) =
decode_varint(&buf[quarter_stream_id_start..]).expect("quarter_stream_id");
assert_eq!(decoded_id, quarter_stream_id);
assert_eq!(declared_length as usize, id_len + 1);
assert_eq!(
&buf[quarter_stream_id_start..quarter_stream_id_start + id_len],
&expected_varint
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZeroRttState {
NotAttempted,
Pending,
Accepted,
Rejected,
HandshakeComplete,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ZeroRttConfig {
pub max_early_data: u64,
pub allow_early_requests: bool,
pub allow_early_settings: bool,
pub state: ZeroRttState,
pub early_data_sent: u64,
}
impl Default for ZeroRttConfig {
fn default() -> Self {
Self {
max_early_data: 16384, allow_early_requests: true,
allow_early_settings: false, state: ZeroRttState::NotAttempted,
early_data_sent: 0,
}
}
}
impl ZeroRttConfig {
pub fn is_early_data_allowed(&self) -> bool {
matches!(self.state, ZeroRttState::Pending | ZeroRttState::Accepted)
}
pub fn can_send_early_data(&self, additional_bytes: u64) -> bool {
if !self.is_early_data_allowed() {
return false;
}
match self.early_data_sent.checked_add(additional_bytes) {
Some(total) => total <= self.max_early_data,
None => false,
}
}
pub fn record_early_data_sent(&mut self, bytes: u64) -> Result<(), H3NativeError> {
if !self.is_early_data_allowed() {
return Err(H3NativeError::StreamProtocol(
"0-RTT not allowed in current state",
));
}
if !self.can_send_early_data(bytes) {
return Err(H3NativeError::StreamProtocol("early data limit exceeded"));
}
self.early_data_sent = self.early_data_sent.saturating_add(bytes);
Ok(())
}
pub fn validate_early_frame(&self, frame: &H3Frame) -> Result<(), H3NativeError> {
if !self.is_early_data_allowed() {
return Ok(()); }
match frame {
H3Frame::Data(_) | H3Frame::Headers(_) if self.allow_early_requests => Ok(()),
H3Frame::Settings(_) if self.allow_early_settings => Ok(()),
H3Frame::Settings(_) if !self.allow_early_settings => Err(
H3NativeError::StreamProtocol("SETTINGS frame not allowed in 0-RTT"),
),
H3Frame::Goaway(_) | H3Frame::MaxPushId(_) => Err(H3NativeError::StreamProtocol(
"control frame not allowed in 0-RTT",
)),
H3Frame::PushPromise { .. } => Err(H3NativeError::StreamProtocol(
"PUSH_PROMISE not allowed in 0-RTT",
)),
_ => {
if self.allow_early_requests {
Ok(())
} else {
Err(H3NativeError::StreamProtocol("frame not allowed in 0-RTT"))
}
}
}
}
}
#[test]
fn zero_rtt_state_transitions() {
let mut config = ZeroRttConfig::default();
assert_eq!(config.state, ZeroRttState::NotAttempted);
assert!(!config.is_early_data_allowed());
config.state = ZeroRttState::Pending;
assert!(config.is_early_data_allowed());
assert!(config.can_send_early_data(1000));
config.state = ZeroRttState::Accepted;
assert!(config.is_early_data_allowed());
config.state = ZeroRttState::HandshakeComplete;
assert!(!config.is_early_data_allowed());
config.state = ZeroRttState::Rejected;
assert!(!config.is_early_data_allowed());
}
#[test]
fn zero_rtt_early_data_limits() {
let mut config = ZeroRttConfig {
max_early_data: 1000,
state: ZeroRttState::Pending,
..ZeroRttConfig::default()
};
assert!(config.can_send_early_data(500));
config
.record_early_data_sent(500)
.expect("record early data");
assert_eq!(config.early_data_sent, 500);
assert!(config.can_send_early_data(500));
config
.record_early_data_sent(500)
.expect("record remaining");
assert_eq!(config.early_data_sent, 1000);
assert!(!config.can_send_early_data(1));
let err = config.record_early_data_sent(1).expect_err("should reject");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
}
#[test]
fn zero_rtt_frame_validation_allows_requests() {
let config = ZeroRttConfig {
state: ZeroRttState::Pending,
allow_early_requests: true,
allow_early_settings: false,
..ZeroRttConfig::default()
};
let data_frame = H3Frame::Data(vec![1, 2, 3]);
config
.validate_early_frame(&data_frame)
.expect("DATA allowed");
let headers_frame = H3Frame::Headers(vec![4, 5, 6]);
config
.validate_early_frame(&headers_frame)
.expect("HEADERS allowed");
}
#[test]
fn zero_rtt_frame_validation_rejects_control_frames() {
let config = ZeroRttConfig {
state: ZeroRttState::Pending,
allow_early_requests: true,
allow_early_settings: false,
..ZeroRttConfig::default()
};
let settings_frame = H3Frame::Settings(H3Settings::default());
let err = config
.validate_early_frame(&settings_frame)
.expect_err("SETTINGS rejected");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
let goaway_frame = H3Frame::Goaway(123);
let err = config
.validate_early_frame(&goaway_frame)
.expect_err("GOAWAY rejected");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
let max_push_frame = H3Frame::MaxPushId(456);
let err = config
.validate_early_frame(&max_push_frame)
.expect_err("MAX_PUSH_ID rejected");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
let push_promise_frame = H3Frame::PushPromise {
push_id: 789,
field_block: vec![7, 8, 9],
};
let err = config
.validate_early_frame(&push_promise_frame)
.expect_err("PUSH_PROMISE rejected");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
}
#[test]
fn zero_rtt_settings_policy_enforcement() {
let mut config = ZeroRttConfig {
state: ZeroRttState::Pending,
allow_early_settings: true,
..ZeroRttConfig::default()
};
let settings_frame = H3Frame::Settings(H3Settings::default());
config
.validate_early_frame(&settings_frame)
.expect("SETTINGS allowed with policy");
config.allow_early_settings = false;
let err = config
.validate_early_frame(&settings_frame)
.expect_err("SETTINGS rejected by policy");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
}
#[test]
fn zero_rtt_request_policy_enforcement() {
let config = ZeroRttConfig {
state: ZeroRttState::Pending,
allow_early_requests: false,
..ZeroRttConfig::default()
};
let data_frame = H3Frame::Data(vec![1, 2, 3]);
let err = config
.validate_early_frame(&data_frame)
.expect_err("DATA rejected by policy");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
let headers_frame = H3Frame::Headers(vec![4, 5, 6]);
let err = config
.validate_early_frame(&headers_frame)
.expect_err("HEADERS rejected by policy");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
}
#[test]
fn zero_rtt_no_restrictions_after_handshake() {
let config = ZeroRttConfig {
state: ZeroRttState::HandshakeComplete,
allow_early_requests: false,
allow_early_settings: false,
..ZeroRttConfig::default()
};
let settings_frame = H3Frame::Settings(H3Settings::default());
config
.validate_early_frame(&settings_frame)
.expect("SETTINGS allowed after handshake");
let goaway_frame = H3Frame::Goaway(123);
config
.validate_early_frame(&goaway_frame)
.expect("GOAWAY allowed after handshake");
let data_frame = H3Frame::Data(vec![1, 2, 3]);
config
.validate_early_frame(&data_frame)
.expect("DATA allowed after handshake");
}
#[test]
fn zero_rtt_replay_protection_state_isolation() {
let mut config1 = ZeroRttConfig {
state: ZeroRttState::Accepted,
max_early_data: 1000,
..ZeroRttConfig::default()
};
let mut config2 = ZeroRttConfig {
state: ZeroRttState::Rejected,
..ZeroRttConfig::default()
};
config1
.record_early_data_sent(500)
.expect("config1 early data");
assert_eq!(config1.early_data_sent, 500);
let err = config2
.record_early_data_sent(500)
.expect_err("config2 should reject");
assert!(matches!(err, H3NativeError::StreamProtocol(_)));
assert_eq!(config2.early_data_sent, 0);
}
#[test]
fn zero_rtt_conservative_defaults() {
let config = ZeroRttConfig::default();
assert!(config.allow_early_requests);
assert!(!config.allow_early_settings);
assert_eq!(config.max_early_data, 16384); assert_eq!(config.state, ZeroRttState::NotAttempted);
assert_eq!(config.early_data_sent, 0);
}
#[test]
fn zero_rtt_saturation_arithmetic() {
let mut config = ZeroRttConfig {
state: ZeroRttState::Pending,
max_early_data: u64::MAX,
early_data_sent: u64::MAX - 100,
..ZeroRttConfig::default()
};
assert!(config.can_send_early_data(50));
config.record_early_data_sent(50).expect("within bounds");
assert!(config.can_send_early_data(50));
config.record_early_data_sent(50).expect("exactly at limit");
assert!(!config.can_send_early_data(1));
}
#[test]
fn qpack_conformance_dynamic_table_lru_eviction() {
let mut table = QpackDynamicTable::new(200);
let id1 = table.insert("header-a".into(), "value-a".into()).unwrap();
let id2 = table.insert("header-b".into(), "value-b".into()).unwrap();
let id3 = table.insert("header-c".into(), "value-c".into()).unwrap();
assert_eq!(table.len(), 3);
let id4 = table
.insert(
"large-header".into(),
"very-large-value-that-forces-eviction".into(),
)
.unwrap();
assert!(table.len() < 4);
assert!(!table.reference_entry(id1)); assert!(table.reference_entry(id2)); assert!(table.reference_entry(id3));
assert!(table.reference_entry(id4));
}
#[test]
fn qpack_conformance_dynamic_table_reference_protection() {
let mut table = QpackDynamicTable::new(150);
let id1 = table
.insert("ref-header".into(), "ref-value".into())
.unwrap();
let id2 = table
.insert("temp-header".into(), "temp-value".into())
.unwrap();
assert!(table.reference_entry(id1));
let _id3 = table
.insert("push-header-1".into(), "push-value-1".into())
.unwrap();
let _id4 = table
.insert("push-header-2".into(), "push-value-2".into())
.unwrap();
assert!(table.reference_entry(id1)); assert!(!table.reference_entry(id2)); }
#[test]
fn qpack_conformance_dynamic_table_size_accounting() {
let mut table = QpackDynamicTable::new(1000);
let initial_size = table.size();
let _id1 = table.insert("name".into(), "value".into()).unwrap();
assert_eq!(table.size(), initial_size + 41);
let _id2 = table.insert("content".into(), "response".into()).unwrap();
assert_eq!(table.size(), initial_size + 41 + 47);
assert!(table.size() <= table.capacity());
}
#[test]
fn qpack_conformance_dynamic_table_capacity_enforcement() {
let capacity = 100;
let mut table = QpackDynamicTable::new(capacity);
let _id1 = table.insert("a".into(), "b".into()).unwrap(); let _id2 = table.insert("c".into(), "d".into()).unwrap();
assert_eq!(table.size(), 68);
let _id3 = table.insert("large".into(), "header-value".into()).unwrap();
assert!(table.size() <= capacity);
let result = table.insert(
"oversized-header-name".into(),
"oversized-header-value-that-exceeds-table-capacity".into(),
);
assert!(result.is_err());
}
#[test]
fn qpack_conformance_dynamic_table_insertion_pressure() {
let mut table = QpackDynamicTable::new(200);
let mut insertion_ids = Vec::new();
for i in 0..20 {
let name = format!("header-{}", i);
let value = format!("value-{}", i);
if let Ok(id) = table.insert(name, value) {
insertion_ids.push(id);
}
}
assert!(table.size() <= table.capacity());
assert!(table.evicted_count > 0);
let first_half_present = insertion_ids
.iter()
.take(10)
.filter(|&&id| table.reference_entry(id))
.count();
let second_half_present = insertion_ids
.iter()
.skip(10)
.filter(|&&id| table.reference_entry(id))
.count();
assert!(second_half_present >= first_half_present);
}
#[test]
fn qpack_conformance_dynamic_table_reference_lifecycle() {
let mut table = QpackDynamicTable::new(300);
let id1 = table
.insert("lifecycle".into(), "test-entry".into())
.unwrap();
assert!(table.reference_entry(id1));
assert!(table.reference_entry(id1));
assert!(table.reference_entry(id1));
for i in 0..10 {
let _ = table.insert(format!("filler-{}", i), "filler-value".into());
}
assert!(table.reference_entry(id1));
assert!(table.unreference_entry(id1));
assert!(table.unreference_entry(id1));
assert!(table.unreference_entry(id1));
assert!(table.unreference_entry(id1));
let large_entry_result = table.insert(
"force-eviction".into(),
"large-value-to-trigger-eviction-of-unreferenced-entries".into(),
);
assert!(large_entry_result.is_ok());
assert!(!table.reference_entry(id1));
}
#[test]
fn qpack_conformance_dynamic_table_memory_pressure_simulation() {
let small_capacity = 150;
let mut table = QpackDynamicTable::new(small_capacity);
let mut tiny_ids = Vec::new();
for i in 0..50 {
if let Ok(id) = table.insert(format!("t{}", i), "x".into()) {
tiny_ids.push(id);
}
}
assert!(table.size() <= small_capacity);
let medium_id = table
.insert("medium-header".into(), "medium-value".into())
.unwrap();
assert!(table.reference_entry(medium_id));
let large_result = table.insert(
"emergency-large".into(),
"large-emergency-header-value".into(),
);
assert!(large_result.is_ok());
assert!(table.reference_entry(medium_id));
assert!(table.size() <= small_capacity);
let ids: Vec<_> = table.entries.iter().map(|e| e.insertion_order).collect();
for id in ids {
let _ = table.reference_entry(id);
}
let impossible_result = table.insert(
"impossible".into(),
"this-should-fail-due-to-references".into(),
);
assert!(impossible_result.is_err());
}
#[test]
fn qpack_conformance_dynamic_table_eviction_order_deterministic() {
let mut table1 = QpackDynamicTable::new(120);
let mut table2 = QpackDynamicTable::new(120);
let sequence = vec![
("first", "entry"),
("second", "entry"),
("third", "entry"),
("fourth", "entry"),
];
let mut ids1 = Vec::new();
let mut ids2 = Vec::new();
for (name, value) in &sequence {
ids1.push(table1.insert(name.to_string(), value.to_string()).unwrap());
ids2.push(table2.insert(name.to_string(), value.to_string()).unwrap());
}
let large_name = "eviction-trigger";
let large_value = "large-value-that-forces-eviction";
let _final1 = table1
.insert(large_name.into(), large_value.into())
.unwrap();
let _final2 = table2
.insert(large_name.into(), large_value.into())
.unwrap();
assert_eq!(table1.len(), table2.len());
assert_eq!(table1.size(), table2.size());
assert_eq!(table1.evicted_count, table2.evicted_count);
for (id1, id2) in ids1.iter().zip(ids2.iter()) {
let present1 = table1.reference_entry(*id1);
let present2 = table2.reference_entry(*id2);
assert_eq!(present1, present2, "Eviction determinism violated");
}
}
#[test]
fn frame_payload_size_limit_enforcement() {
let config = H3ConnectionConfig {
max_frame_payload_size: 100, ..Default::default()
};
let large_payload = vec![0x42; 200]; let frame = H3Frame::Data(large_payload);
let mut buf = Vec::new();
frame.encode(&mut buf).expect("encode should succeed");
let err = H3Frame::decode(&buf, &config).expect_err("decode must reject oversized frame");
match err {
H3NativeError::FrameTooLarge {
payload_size,
max_size,
} => {
assert_eq!(payload_size, 200);
assert_eq!(max_size, 100);
}
other => panic!("expected FrameTooLarge error, got: {:?}", other), }
let small_payload = vec![0x42; 50]; let small_frame = H3Frame::Data(small_payload.clone());
let mut small_buf = Vec::new();
small_frame
.encode(&mut small_buf)
.expect("encode small frame");
let (decoded, consumed) = H3Frame::decode(&small_buf, &config).expect("decode small frame");
assert_eq!(decoded, small_frame);
assert_eq!(consumed, small_buf.len());
}
#[test]
fn frame_payload_size_limit_applies_to_all_frame_types() {
let config = H3ConnectionConfig {
max_frame_payload_size: 50,
..Default::default()
};
let large_headers_payload = vec![0x00; 100]; let headers_frame = H3Frame::Headers(large_headers_payload);
let mut buf = Vec::new();
headers_frame
.encode(&mut buf)
.expect("encode headers frame");
let err = H3Frame::decode(&buf, &config).expect_err("headers frame must be rejected");
assert!(matches!(err, H3NativeError::FrameTooLarge { .. }));
let large_field_block = vec![0x00; 100];
let push_promise_frame = H3Frame::PushPromise {
push_id: 42,
field_block: large_field_block,
};
let mut buf = Vec::new();
push_promise_frame
.encode(&mut buf)
.expect("encode push promise frame");
let err = H3Frame::decode(&buf, &config).expect_err("push promise frame must be rejected");
assert!(matches!(err, H3NativeError::FrameTooLarge { .. }));
}
#[test]
fn concurrent_stream_limit_rejects_new_stream_once_full() {
let mut c = H3ConnectionState::with_config(H3ConnectionConfig {
max_concurrent_request_streams: Some(2),
..H3ConnectionConfig::default()
});
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_request_stream_frame(0, &H3Frame::Headers(vec![1]))
.expect("first within limit");
c.on_request_stream_frame(4, &H3Frame::Headers(vec![2]))
.expect("second within limit");
assert_eq!(c.active_request_stream_count(), 2);
let err = c
.on_request_stream_frame(8, &H3Frame::Headers(vec![3]))
.expect_err("third stream must exceed cap");
assert_eq!(
err,
H3NativeError::ConcurrentStreamLimitExceeded {
active: 2,
limit: 2,
}
);
assert_eq!(c.active_request_stream_count(), 2);
}
#[test]
fn concurrent_stream_limit_does_not_block_frames_on_existing_streams() {
let mut c = H3ConnectionState::with_config(H3ConnectionConfig {
max_concurrent_request_streams: Some(1),
..H3ConnectionConfig::default()
});
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_request_stream_frame(0, &H3Frame::Headers(vec![1]))
.expect("create stream 0");
c.on_request_stream_frame(0, &H3Frame::Data(vec![0xAA]))
.expect("additional frame on existing stream");
}
#[test]
fn concurrent_stream_limit_allows_new_stream_after_finish() {
let mut c = H3ConnectionState::with_config(H3ConnectionConfig {
max_concurrent_request_streams: Some(1),
..H3ConnectionConfig::default()
});
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_request_stream_frame(0, &H3Frame::Headers(vec![1]))
.expect("first stream");
c.finish_request_stream(0).expect("finish first stream");
c.on_request_stream_frame(4, &H3Frame::Headers(vec![2]))
.expect("second stream after finish");
assert_eq!(c.active_request_stream_count(), 1);
}
#[test]
fn concurrent_stream_limit_unbounded_by_default() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
for stream_id in (0..20).map(|i| i * 4) {
c.on_request_stream_frame(stream_id, &H3Frame::Headers(vec![1]))
.expect("unbounded by default");
}
assert_eq!(c.active_request_stream_count(), 20);
}
#[test]
fn concurrent_stream_limit_runtime_update_applies_to_future_streams() {
let mut c = H3ConnectionState::new();
c.on_control_frame(&H3Frame::Settings(H3Settings::default()))
.expect("settings");
c.on_request_stream_frame(0, &H3Frame::Headers(vec![1]))
.expect("first stream");
c.on_request_stream_frame(4, &H3Frame::Headers(vec![2]))
.expect("second stream");
c.set_max_concurrent_request_streams(Some(2));
let err = c
.on_request_stream_frame(8, &H3Frame::Headers(vec![3]))
.expect_err("third stream must exceed tightened cap");
assert!(matches!(
err,
H3NativeError::ConcurrentStreamLimitExceeded {
active: 2,
limit: 2
}
));
c.on_request_stream_frame(4, &H3Frame::Data(vec![0xAA]))
.expect("existing stream unaffected");
}
#[test]
fn bidirectional_frame_validation_allows_valid_frames() {
let data_frame = H3Frame::Data(vec![1, 2, 3]);
validate_bidirectional_frame(&data_frame).expect("DATA frame should be allowed");
let headers_frame = H3Frame::Headers(vec![4, 5, 6]);
validate_bidirectional_frame(&headers_frame).expect("HEADERS frame should be allowed");
let push_promise_frame = H3Frame::PushPromise {
push_id: 123,
field_block: vec![7, 8, 9],
};
validate_bidirectional_frame(&push_promise_frame)
.expect("PUSH_PROMISE frame should be allowed");
let datagram_frame = H3Frame::Datagram {
quarter_stream_id: 456,
payload: vec![10, 11, 12],
};
validate_bidirectional_frame(&datagram_frame).expect("DATAGRAM frame should be allowed");
}
#[test]
fn bidirectional_frame_validation_rejects_control_frames() {
let settings_frame = H3Frame::Settings(H3Settings::default());
let err =
validate_bidirectional_frame(&settings_frame).expect_err("SETTINGS should be rejected");
assert_eq!(
err,
H3NativeError::StreamProtocol("SETTINGS frame not allowed on bidirectional stream")
);
let cancel_push_frame = H3Frame::CancelPush(789);
let err = validate_bidirectional_frame(&cancel_push_frame)
.expect_err("CANCEL_PUSH should be rejected");
assert_eq!(
err,
H3NativeError::StreamProtocol("CANCEL_PUSH frame not allowed on bidirectional stream")
);
let goaway_frame = H3Frame::Goaway(101);
let err =
validate_bidirectional_frame(&goaway_frame).expect_err("GOAWAY should be rejected");
assert_eq!(
err,
H3NativeError::StreamProtocol("GOAWAY frame not allowed on bidirectional stream")
);
let max_push_id_frame = H3Frame::MaxPushId(202);
let err = validate_bidirectional_frame(&max_push_id_frame)
.expect_err("MAX_PUSH_ID should be rejected");
assert_eq!(
err,
H3NativeError::StreamProtocol("MAX_PUSH_ID frame not allowed on bidirectional stream")
);
}
#[test]
fn bidirectional_frame_validation_ignores_unknown_frames_per_rfc9114_7_2_8() {
let unknown_frame = H3Frame::Unknown {
frame_type: 0xDEAD_BEEF,
payload: vec![13, 14, 15],
};
validate_bidirectional_frame(&unknown_frame).expect(
"RFC 9114 §7.2.8 violation: unknown frame on bidi stream must be ignored, not errored",
);
let unknown_empty = H3Frame::Unknown {
frame_type: 0xF00D,
payload: Vec::new(),
};
validate_bidirectional_frame(&unknown_empty)
.expect("RFC 9114 §7.2.8 violation: empty-payload unknown frame must be ignored");
for grease_type in [0x21u64, 0x40u64, 0x1f * 12345 + 0x21] {
let grease = H3Frame::Unknown {
frame_type: grease_type,
payload: vec![0xAA; 32],
};
validate_bidirectional_frame(&grease).unwrap_or_else(|e| {
panic!(
"RFC 9114 §7.2.8 violation: GREASE frame type 0x{grease_type:x} \
rejected on bidi stream (got {e:?}); MUST be ignored"
)
});
}
}
#[test]
fn qpack_decode_enforces_max_field_section_size() {
let plan = vec![
QpackFieldPlan::StaticIndex(17), QpackFieldPlan::StaticIndex(23), QpackFieldPlan::StaticIndex(1), QpackFieldPlan::Literal {
name: "x-large-header".to_string(),
value: "a".repeat(1000), },
];
let wire = qpack_encode_field_section(&plan).expect("encode");
let result = qpack_decode_request_field_section(&wire, H3QpackMode::StaticOnly, None);
assert!(result.is_ok(), "decode should succeed without limit");
let result = qpack_decode_request_field_section_with_limit(
&wire,
H3QpackMode::StaticOnly,
None,
Some(1200),
);
assert!(result.is_ok(), "decode should succeed with high limit");
let err = qpack_decode_request_field_section_with_limit(
&wire,
H3QpackMode::StaticOnly,
None,
Some(500),
)
.expect_err("decode should fail with low limit");
assert_eq!(
err,
H3NativeError::QpackPolicy("decoded field section exceeds maximum size limit")
);
let response_plan = vec![
QpackFieldPlan::StaticIndex(25), QpackFieldPlan::Literal {
name: "x-response-header".to_string(),
value: "b".repeat(800), },
];
let response_wire = qpack_encode_field_section(&response_plan).expect("encode response");
let err = qpack_decode_response_field_section_with_limit(
&response_wire,
H3QpackMode::StaticOnly,
None,
Some(400),
)
.expect_err("response decode should fail with low limit");
assert_eq!(
err,
H3NativeError::QpackPolicy("decoded field section exceeds maximum size limit")
);
}
}