use bytes::Bytes;
use crate::qpack::{
encoder::EncoderState,
field::{EncodedFieldSectionPrefix, FieldLine, FieldLineRepresentation},
r#static,
};
pub trait HuffmanStrategize {
fn should_encode_name_with_huffman(&self, name: &Bytes) -> bool;
fn should_encode_value_with_huffman(&self, value: &Bytes) -> bool;
}
pub struct HuffmanAlways;
impl HuffmanStrategize for HuffmanAlways {
fn should_encode_name_with_huffman(&self, _name: &Bytes) -> bool {
true
}
fn should_encode_value_with_huffman(&self, _value: &Bytes) -> bool {
true
}
}
pub struct HuffmanNever;
impl HuffmanStrategize for HuffmanNever {
fn should_encode_name_with_huffman(&self, _name: &Bytes) -> bool {
false
}
fn should_encode_value_with_huffman(&self, _value: &Bytes) -> bool {
false
}
}
pub struct CompressOutput {
pub prefix: EncodedFieldSectionPrefix,
pub representations: Vec<FieldLineRepresentation>,
pub max_referenced_index: Option<u64>,
}
pub trait Algorithm {
fn compress(
&self,
state: &mut EncoderState,
entries: impl IntoIterator<Item = FieldLine> + Send,
may_block: bool,
) -> impl Future<Output = CompressOutput> + Send;
}
pub struct StaticCompressAlgo<HS> {
huffman_strategize: HS,
}
impl<HS> StaticCompressAlgo<HS> {
pub const fn new(huffman_strategize: HS) -> Self {
Self { huffman_strategize }
}
}
impl<HS> Algorithm for StaticCompressAlgo<HS>
where
HS: HuffmanStrategize + Send + Sync,
{
async fn compress(
&self,
_state: &mut EncoderState,
entries: impl IntoIterator<Item = FieldLine> + Send,
_may_block: bool,
) -> CompressOutput {
let prefix = EncodedFieldSectionPrefix {
encoded_insert_count: 0,
sign: false,
delta_base: 0,
};
let mut representations = Vec::new();
for FieldLine { name, value } in entries {
if let (Some(name_index), value_index) = r#static::find(&name, &value) {
if value_index == Some(name_index) {
representations.push(FieldLineRepresentation::IndexedFieldLine {
is_static: true,
index: name_index as u64,
})
} else {
representations.push(
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic: true,
is_static: true,
name_index: name_index as u64,
huffman: self
.huffman_strategize
.should_encode_value_with_huffman(&value),
value: value.clone(),
},
)
}
} else {
representations.push(FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic: true,
name_huffman: self
.huffman_strategize
.should_encode_name_with_huffman(&name),
name: name.clone(),
value_huffman: self
.huffman_strategize
.should_encode_value_with_huffman(&value),
value: value.clone(),
})
}
}
CompressOutput {
prefix,
representations,
max_referenced_index: None,
}
}
}
const SENSITIVE_HEADER_NAMES: &[&[u8]] = &[
b"authorization",
b"proxy-authorization",
b"cookie",
b"set-cookie",
];
fn is_sensitive(name: &[u8]) -> bool {
SENSITIVE_HEADER_NAMES
.iter()
.any(|s| name.eq_ignore_ascii_case(s))
}
pub struct DynamicCompressAlgo<HS> {
huffman_strategize: HS,
}
impl<HS> DynamicCompressAlgo<HS> {
pub const fn new(huffman_strategize: HS) -> Self {
Self { huffman_strategize }
}
}
impl<HS> Algorithm for DynamicCompressAlgo<HS>
where
HS: HuffmanStrategize + Send + Sync,
{
async fn compress(
&self,
state: &mut EncoderState,
entries: impl IntoIterator<Item = FieldLine> + Send,
may_block: bool,
) -> CompressOutput {
let base = state.table_inserted_count();
let max_table_capacity = state.table_capacity();
let known_received_count = state.table_known_received_count();
let mut representations = Vec::new();
let mut max_ref: Option<u64> = None;
for FieldLine { name, value } in entries {
let never_dynamic = is_sensitive(&name);
let (static_name_idx, static_value_idx) = r#static::find(&name, &value);
if let Some(val_idx) = static_value_idx
&& static_name_idx == Some(val_idx)
{
representations.push(FieldLineRepresentation::IndexedFieldLine {
is_static: true,
index: val_idx as u64,
});
continue;
}
if let Some(abs) = find_dynamic_exact(state, &name, &value)
&& can_reference(abs, known_received_count, may_block)
{
track_ref(&mut max_ref, abs);
push_dynamic_index(&mut representations, abs, base);
continue;
}
let dynamic_name_abs = find_dynamic_name(state, &name);
if !never_dynamic && entry_fits_capacity(state, &name, &value) {
let insert_result = if let Some(static_idx) = static_name_idx {
state.insert_with_name_reference(
true,
static_idx as u64,
self.huffman_strategize
.should_encode_value_with_huffman(&value),
value.clone(),
)
} else if let Some(dyn_abs) = dynamic_name_abs {
state.insert_with_name_reference(
false,
dyn_abs,
self.huffman_strategize
.should_encode_value_with_huffman(&value),
value.clone(),
)
} else {
state.insert_with_literal_name(
self.huffman_strategize
.should_encode_name_with_huffman(&name),
name.clone(),
self.huffman_strategize
.should_encode_value_with_huffman(&value),
value.clone(),
)
};
if let Ok(new_abs) = insert_result {
if may_block {
track_ref(&mut max_ref, new_abs);
representations.push(
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex {
index: new_abs - base,
},
);
continue;
}
}
}
if let Some(static_idx) = static_name_idx {
representations.push(FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic,
is_static: true,
name_index: static_idx as u64,
huffman: self
.huffman_strategize
.should_encode_value_with_huffman(&value),
value: value.clone(),
});
} else if let Some(dyn_abs) = dynamic_name_abs {
if can_reference(dyn_abs, known_received_count, may_block) {
track_ref(&mut max_ref, dyn_abs);
push_dynamic_name_ref(
&mut representations,
dyn_abs,
base,
never_dynamic,
self.huffman_strategize
.should_encode_value_with_huffman(&value),
value.clone(),
);
} else {
push_literal_name(
&mut representations,
&self.huffman_strategize,
never_dynamic,
name.clone(),
value.clone(),
);
}
} else {
push_literal_name(
&mut representations,
&self.huffman_strategize,
never_dynamic,
name.clone(),
value.clone(),
);
}
}
let prefix = compute_prefix(max_ref, base, max_table_capacity);
CompressOutput {
prefix,
representations,
max_referenced_index: max_ref,
}
}
}
fn find_dynamic_exact(state: &EncoderState, name: &Bytes, value: &Bytes) -> Option<u64> {
let name_indices = state.find_name(name)?;
let value_indices = state.find_value(value)?;
name_indices.intersection(value_indices).last().copied()
}
fn find_dynamic_name(state: &EncoderState, name: &Bytes) -> Option<u64> {
state.find_name(name)?.iter().next_back().copied()
}
fn entry_fits_capacity(state: &EncoderState, name: &Bytes, value: &Bytes) -> bool {
let entry_size = name.len() as u64 + value.len() as u64 + 32;
entry_size <= state.table_capacity()
}
fn can_reference(abs_index: u64, known_received_count: u64, may_block: bool) -> bool {
abs_index < known_received_count || may_block
}
fn track_ref(max_ref: &mut Option<u64>, index: u64) {
*max_ref = Some(max_ref.map_or(index, |m| m.max(index)));
}
fn push_dynamic_index(representations: &mut Vec<FieldLineRepresentation>, abs: u64, base: u64) {
if abs >= base {
representations
.push(FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index: abs - base });
} else {
representations.push(FieldLineRepresentation::IndexedFieldLine {
is_static: false,
index: base - abs - 1,
});
}
}
fn push_dynamic_name_ref(
representations: &mut Vec<FieldLineRepresentation>,
dyn_abs: u64,
base: u64,
never_dynamic: bool,
huffman: bool,
value: Bytes,
) {
if dyn_abs >= base {
representations.push(
FieldLineRepresentation::LiteralFieldLineWithPostBaseNameReference {
never_dynamic,
name_index: dyn_abs - base,
huffman,
value,
},
);
} else {
representations.push(FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic,
is_static: false,
name_index: base - dyn_abs - 1,
huffman,
value,
});
}
}
fn push_literal_name(
representations: &mut Vec<FieldLineRepresentation>,
hs: &impl HuffmanStrategize,
never_dynamic: bool,
name: Bytes,
value: Bytes,
) {
representations.push(FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic,
name_huffman: hs.should_encode_name_with_huffman(&name),
name,
value_huffman: hs.should_encode_value_with_huffman(&value),
value,
});
}
fn compute_prefix(
max_ref: Option<u64>,
base: u64,
max_table_capacity: u64,
) -> EncodedFieldSectionPrefix {
match max_ref {
Some(max) => {
let required_insert_count = max + 1;
let encoded_insert_count =
EncodedFieldSectionPrefix::encode_ric(required_insert_count, max_table_capacity);
if base >= required_insert_count {
EncodedFieldSectionPrefix {
encoded_insert_count,
sign: false,
delta_base: base - required_insert_count,
}
} else {
EncodedFieldSectionPrefix {
encoded_insert_count,
sign: true,
delta_base: required_insert_count - base - 1,
}
}
}
None => EncodedFieldSectionPrefix {
encoded_insert_count: 0,
sign: false,
delta_base: 0,
},
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use bytes::Bytes;
use crate::{
dhttp::settings::Settings,
qpack::{
algorithm::{
Algorithm, CompressOutput, DynamicCompressAlgo, HuffmanAlways, HuffmanNever,
StaticCompressAlgo,
},
encoder::EncoderState,
field::{EncodedFieldSectionPrefix, FieldLine, FieldLineRepresentation},
},
varint::VarInt,
};
fn state_with_capacity(table_capacity: u32) -> EncoderState {
let mut settings = Settings::default();
settings.set(crate::qpack::settings::QpackMaxTableCapacity::setting(
VarInt::from_u32(table_capacity),
));
settings.set(crate::qpack::settings::QpackBlockedStreams::setting(
VarInt::from_u32(100),
));
let mut state = EncoderState::new(Arc::new(settings));
if table_capacity > 0 {
state
.set_max_table_capacity(table_capacity as u64)
.expect("set capacity failed");
}
state
}
fn field_line(name: &str, value: &str) -> FieldLine {
FieldLine {
name: Bytes::from(name.to_owned()),
value: Bytes::from(value.to_owned()),
}
}
fn algo() -> DynamicCompressAlgo<HuffmanNever> {
DynamicCompressAlgo::new(HuffmanNever)
}
fn huffman_algo() -> DynamicCompressAlgo<HuffmanAlways> {
DynamicCompressAlgo::new(HuffmanAlways)
}
async fn do_compress(
state: &mut EncoderState,
entries: Vec<FieldLine>,
may_block: bool,
) -> CompressOutput {
algo().compress(state, entries, may_block).await
}
#[tokio::test]
async fn static_exact_match() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line(":method", "GET")], true).await;
assert_eq!(output.representations.len(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLine {
is_static: true,
index: 17
}
));
assert!(output.max_referenced_index.is_none());
assert_eq!(output.prefix.encoded_insert_count, 0);
}
#[tokio::test]
async fn insert_and_post_base_reference() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
assert_eq!(output.representations.len(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index: 0 }
));
assert_eq!(output.max_referenced_index, Some(0));
assert_ne!(output.prefix.encoded_insert_count, 0);
assert!(output.prefix.sign);
}
#[tokio::test]
async fn insert_without_blocking_populates_table_but_emits_literal() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], false).await;
assert_eq!(state.table_inserted_count(), 1);
assert!(output.max_referenced_index.is_none());
assert_eq!(output.prefix.encoded_insert_count, 0);
assert!(matches!(
&output.representations[..],
[FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic: false,
name_huffman: false,
name,
value_huffman: false,
value,
}] if name.as_ref() == b"x-custom" && value.as_ref() == b"hello"
));
}
#[tokio::test]
async fn second_request_uses_pre_base_dynamic_ref() {
let mut state = state_with_capacity(256);
let _ = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
assert_eq!(output.representations.len(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLine {
is_static: false,
index: 0
}
));
assert_eq!(output.max_referenced_index, Some(0));
assert!(!output.prefix.sign);
}
#[tokio::test]
async fn sensitive_headers_never_inserted() {
let mut state = state_with_capacity(256);
let output = do_compress(
&mut state,
vec![field_line("authorization", "Bearer secret")],
true,
)
.await;
assert_eq!(output.representations.len(), 1);
match &output.representations[0] {
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic,
is_static,
..
} => {
assert!(
never_dynamic,
"sensitive header should have never_dynamic=true"
);
assert!(is_static, "should use static name reference");
}
FieldLineRepresentation::LiteralFieldLineWithLiteralName { never_dynamic, .. } => {
assert!(
never_dynamic,
"sensitive header should have never_dynamic=true"
);
}
other => panic!("expected literal representation, got {other:?}"),
}
assert!(output.max_referenced_index.is_none());
assert_eq!(state.table_inserted_count(), 0);
}
#[tokio::test]
async fn cookie_header_never_inserted() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line("cookie", "session=abc")], true).await;
assert!(matches!(
output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic: true,
is_static: true,
..
}
));
assert_eq!(state.table_inserted_count(), 0);
}
#[tokio::test]
async fn may_block_false_inserts_speculatively_without_reference() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], false).await;
assert!(output.max_referenced_index.is_none());
assert_eq!(state.table_inserted_count(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithLiteralName { .. }
));
}
#[tokio::test]
async fn may_block_false_can_reference_acknowledged() {
let mut state = state_with_capacity(256);
let _ = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], false).await;
assert_eq!(output.representations.len(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLine {
is_static: false,
..
}
));
assert!(output.max_referenced_index.is_some());
}
#[tokio::test]
async fn static_name_reference_with_literal_value() {
let mut state = state_with_capacity(0);
let output = do_compress(
&mut state,
vec![field_line(":path", "/my/custom/path")],
true,
)
.await;
assert_eq!(output.representations.len(), 1);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithNameReference {
is_static: true,
..
}
));
}
#[tokio::test]
async fn static_algorithm_emits_all_three_representation_forms() {
let mut state = state_with_capacity(256);
let algo = StaticCompressAlgo::new(HuffmanAlways);
let output = algo
.compress(
&mut state,
vec![
field_line(":method", "GET"),
field_line(":path", "/custom"),
field_line("x-literal", "value"),
],
false,
)
.await;
assert_eq!(output.representations.len(), 3);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLine {
is_static: true,
index: 17
}
));
assert!(matches!(
&output.representations[1],
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic: true,
is_static: true,
huffman: true,
value,
..
} if value == b"/custom".as_slice()
));
assert!(matches!(
&output.representations[2],
FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic: true,
name_huffman: true,
name,
value_huffman: true,
value,
} if name == b"x-literal".as_slice() && value == b"value".as_slice()
));
assert_eq!(state.table_inserted_count(), 0);
assert!(output.max_referenced_index.is_none());
assert_eq!(
output.prefix,
EncodedFieldSectionPrefix {
encoded_insert_count: 0,
sign: false,
delta_base: 0,
}
);
}
#[tokio::test]
async fn dynamic_literal_name_uses_huffman_flags_from_strategy() {
let mut state = state_with_capacity(0);
let output = huffman_algo()
.compress(&mut state, vec![field_line("x-huffman", "value")], true)
.await;
assert!(matches!(
&output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic: false,
name_huffman: true,
name,
value_huffman: true,
value,
} if name == b"x-huffman".as_slice() && value == b"value".as_slice()
));
}
#[tokio::test]
async fn all_sensitive_header_names_are_case_insensitive_and_never_inserted() {
let sensitive_names = [
"authorization",
"AUTHORIZATION",
"proxy-authorization",
"Proxy-Authorization",
"cookie",
"COOKIE",
"set-cookie",
"Set-Cookie",
];
for name in sensitive_names {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line(name, "secret")], true).await;
assert_eq!(state.table_inserted_count(), 0, "{name} was inserted");
assert!(
output.max_referenced_index.is_none(),
"{name} referenced dynamic table"
);
match &output.representations[0] {
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic,
..
}
| FieldLineRepresentation::LiteralFieldLineWithPostBaseNameReference {
never_dynamic,
..
}
| FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic, ..
} => assert!(*never_dynamic, "{name} did not set never_dynamic"),
other => panic!("expected literal representation for {name}, got {other:?}"),
}
}
}
#[tokio::test]
async fn dynamic_exact_match_can_use_post_base_reference() {
let mut state = state_with_capacity(256);
let output = do_compress(
&mut state,
vec![
field_line("x-repeat", "same"),
field_line("x-repeat", "same"),
],
true,
)
.await;
assert_eq!(state.table_inserted_count(), 1);
assert_eq!(output.max_referenced_index, Some(0));
assert!(matches!(
output.representations.as_slice(),
[
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index: 0 },
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index: 0 },
]
));
}
#[tokio::test]
async fn dynamic_name_reference_uses_pre_base_when_name_is_acknowledged() {
let mut state = state_with_capacity(48);
let _ = do_compress(&mut state, vec![field_line("x-ref", "a")], true).await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(&mut state, vec![field_line("x-ref", "longer-value")], true).await;
assert_eq!(state.table_inserted_count(), 1);
assert_eq!(output.max_referenced_index, Some(0));
assert!(matches!(
&output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithNameReference {
never_dynamic: false,
is_static: false,
name_index: 0,
huffman: false,
value,
} if value == b"longer-value".as_slice()
));
}
#[tokio::test]
async fn dynamic_name_reference_uses_post_base_when_name_is_new_in_section() {
let mut state = state_with_capacity(48);
let output = do_compress(
&mut state,
vec![
field_line("x-ref", "a"),
field_line("x-ref", "longer-value"),
],
true,
)
.await;
assert_eq!(state.table_inserted_count(), 1);
assert_eq!(output.max_referenced_index, Some(0));
assert!(matches!(
&output.representations[1],
FieldLineRepresentation::LiteralFieldLineWithPostBaseNameReference {
never_dynamic: false,
name_index: 0,
huffman: false,
value,
} if value == b"longer-value".as_slice()
));
}
#[tokio::test]
async fn may_block_false_falls_back_when_dynamic_name_is_unacknowledged() {
let mut state = state_with_capacity(48);
let _ = do_compress(&mut state, vec![field_line("x-ref", "a")], true).await;
assert_eq!(state.table_known_received_count(), 0);
let output =
do_compress(&mut state, vec![field_line("x-ref", "longer-value")], false).await;
assert_eq!(state.table_inserted_count(), 1);
assert!(output.max_referenced_index.is_none());
assert!(matches!(
&output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithLiteralName {
never_dynamic: false,
name_huffman: false,
name,
value_huffman: false,
value,
} if name == b"x-ref".as_slice() && value == b"longer-value".as_slice()
));
}
#[tokio::test]
async fn prefix_required_insert_count_uses_largest_dynamic_reference() {
let mut state = state_with_capacity(256);
let _ = do_compress(
&mut state,
vec![field_line("x-a", "one"), field_line("x-b", "two")],
true,
)
.await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(
&mut state,
vec![field_line("x-a", "one"), field_line("x-b", "two")],
true,
)
.await;
assert_eq!(output.max_referenced_index, Some(1));
assert_eq!(
output.prefix,
EncodedFieldSectionPrefix {
encoded_insert_count: EncodedFieldSectionPrefix::encode_ric(2, 256),
sign: false,
delta_base: 0,
}
);
}
#[tokio::test]
async fn multiple_insertions_correct_post_base_indices() {
let mut state = state_with_capacity(4096);
let output = do_compress(
&mut state,
vec![
field_line("x-header-a", "value-a"),
field_line("x-header-b", "value-b"),
field_line("x-header-c", "value-c"),
],
true,
)
.await;
assert_eq!(output.representations.len(), 3);
for (i, repr) in output.representations.iter().enumerate() {
assert!(
matches!(
repr,
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index }
if *index == i as u64
),
"entry {i}: expected PostBaseIndex({i}), got {repr:?}"
);
}
assert_eq!(state.table_inserted_count(), 3);
assert_eq!(output.max_referenced_index, Some(2));
}
#[tokio::test]
async fn same_name_different_value_inserts_new_entry() {
let mut state = state_with_capacity(4096);
let _ = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(&mut state, vec![field_line("x-custom", "world")], true).await;
assert_eq!(state.table_inserted_count(), 2);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::IndexedFieldLineWithPostBaseIndex { index: 0 }
));
}
#[tokio::test]
async fn prefix_no_dynamic_refs() {
let mut state = state_with_capacity(0);
let output = do_compress(&mut state, vec![field_line(":method", "GET")], true).await;
assert_eq!(
output.prefix,
EncodedFieldSectionPrefix {
encoded_insert_count: 0,
sign: false,
delta_base: 0,
}
);
}
#[tokio::test]
async fn prefix_with_post_base_sign_true() {
let mut state = state_with_capacity(256);
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
assert!(output.prefix.sign);
assert_eq!(output.prefix.delta_base, 0);
}
#[tokio::test]
async fn prefix_with_pre_base_sign_false() {
let mut state = state_with_capacity(256);
let _ = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
state.dynamic_table.known_received_count = state.dynamic_table.inserted_count;
let output = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
assert!(!output.prefix.sign);
assert_eq!(output.prefix.delta_base, 0);
}
#[tokio::test]
async fn insert_emits_encoder_instructions() {
let mut state = state_with_capacity(256);
let _ = do_compress(&mut state, vec![field_line("x-custom", "hello")], true).await;
assert_eq!(state.pending_instructions.len(), 2);
}
#[tokio::test]
async fn insert_with_static_name_emits_name_reference() {
let mut state = state_with_capacity(256);
let _ = do_compress(
&mut state,
vec![field_line(":path", "/my/custom/path")],
true,
)
.await;
assert_eq!(state.table_inserted_count(), 1);
assert!(state.pending_instructions.iter().any(|inst| matches!(
inst,
crate::qpack::encoder::EncoderInstruction::InsertWithNameReference {
is_static: true,
..
}
)));
}
#[tokio::test]
async fn entry_too_large_for_table_falls_to_literal() {
let mut state = state_with_capacity(64);
let output = do_compress(
&mut state,
vec![field_line("x-custom", "a-very-long-value-that-wont-fit-")],
true,
)
.await;
assert_eq!(state.table_inserted_count(), 0);
assert!(matches!(
output.representations[0],
FieldLineRepresentation::LiteralFieldLineWithLiteralName { .. }
));
}
mod proptest_roundtrip {
use std::sync::Arc;
use bytes::Bytes;
use proptest::prelude::*;
use crate::{
dhttp::settings::Settings,
qpack::{
algorithm::{Algorithm, DynamicCompressAlgo, HuffmanNever},
decoder::DecoderState,
encoder::{EncoderInstruction, EncoderState},
field::{EncodedFieldSectionPrefix, FieldLine},
},
varint::VarInt,
};
fn settings_pair(capacity: u32) -> Arc<Settings> {
let mut settings = Settings::default();
settings.set(crate::qpack::settings::QpackMaxTableCapacity::setting(
VarInt::from_u32(capacity),
));
settings.set(crate::qpack::settings::QpackBlockedStreams::setting(
VarInt::from_u32(100),
));
Arc::new(settings)
}
fn apply_instructions(encoder: &mut EncoderState, decoder: &mut DecoderState) {
for instruction in encoder.pending_instructions() {
match instruction {
EncoderInstruction::SetDynamicTableCapacity { capacity } => {
let _ = decoder.set_dynamic_table_capacity(*capacity);
}
EncoderInstruction::InsertWithNameReference {
is_static,
name_index,
value,
..
} => {
let abs_index = if *is_static {
*name_index
} else {
decoder.table_inserted_count().wrapping_sub(name_index + 1)
};
let _ = decoder.insert_with_name_reference(
*is_static,
abs_index,
value.clone(),
);
}
EncoderInstruction::InsertWithLiteralName { name, value, .. } => {
let _ = decoder.insert_with_literal_name(name.clone(), value.clone());
}
EncoderInstruction::Duplicate { index } => {
let abs_index = decoder.table_inserted_count().wrapping_sub(index + 1);
let _ = decoder.duplicate(abs_index);
}
}
}
encoder.pending_instructions.clear();
}
#[test]
fn apply_instructions_replays_all_encoder_instruction_variants() {
let settings = settings_pair(4096);
let mut encoder = EncoderState::new(settings.clone());
encoder.set_max_table_capacity(4096).unwrap();
encoder
.insert_with_literal_name(
false,
Bytes::from_static(b"x-first"),
false,
Bytes::from_static(b"one"),
)
.unwrap();
encoder
.insert_with_name_reference(true, 1, false, Bytes::from_static(b"/custom"))
.unwrap();
encoder
.insert_with_name_reference(false, 0, false, Bytes::from_static(b"two"))
.unwrap();
encoder.duplicate(0).unwrap();
let mut decoder = DecoderState::new(settings);
apply_instructions(&mut encoder, &mut decoder);
assert_eq!(decoder.table_inserted_count(), 4);
assert!(encoder.pending_instructions().is_empty());
}
fn verify_roundtrip(
settings: &Settings,
decoder: &DecoderState,
output: &super::CompressOutput,
original: &[FieldLine],
) {
let max_table_capacity = settings.qpack_max_table_capacity().into_inner();
let total_inserts = decoder.table_inserted_count();
let required_insert_count = EncodedFieldSectionPrefix::decode_ric(
output.prefix.encoded_insert_count,
max_table_capacity,
total_inserts,
)
.expect("decode_ric failed");
let base = EncodedFieldSectionPrefix::resolve_base(
required_insert_count,
output.prefix.sign,
output.prefix.delta_base,
)
.expect("resolve_base failed");
let decoded: Vec<FieldLine> = output
.representations
.iter()
.map(|repr| decoder.decompress(repr, base).expect("decompress failed"))
.collect();
assert_eq!(decoded.len(), original.len());
for (orig, dec) in original.iter().zip(decoded.iter()) {
assert_eq!(orig.name, dec.name);
assert_eq!(orig.value, dec.value);
}
}
fn arb_field_line() -> impl Strategy<Value = FieldLine> {
(
prop::collection::vec(prop::num::u8::ANY, 1..32),
prop::collection::vec(prop::num::u8::ANY, 0..64),
)
.prop_map(|(name, value)| FieldLine {
name: Bytes::from(name),
value: Bytes::from(value),
})
}
fn arb_field_section() -> impl Strategy<Value = Vec<FieldLine>> {
prop::collection::vec(arb_field_line(), 1..8)
}
proptest! {
#[test]
fn single_section_roundtrip(field_lines in arb_field_section()) {
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
rt.block_on(async {
let settings = settings_pair(4096);
let mut encoder = EncoderState::new(settings.clone());
encoder.set_max_table_capacity(4096).unwrap();
let mut decoder = DecoderState::new(settings.clone());
decoder.set_dynamic_table_capacity(4096).unwrap();
let algo = DynamicCompressAlgo::new(HuffmanNever);
let output = algo.compress(&mut encoder, field_lines.clone(), true).await;
apply_instructions(&mut encoder, &mut decoder);
verify_roundtrip(&settings, &decoder, &output, &field_lines);
});
}
#[test]
fn multi_section_roundtrip(
sections in prop::collection::vec(arb_field_section(), 2..5),
) {
let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
rt.block_on(async {
let settings = settings_pair(4096);
let mut encoder = EncoderState::new(settings.clone());
encoder.set_max_table_capacity(4096).unwrap();
let mut decoder = DecoderState::new(settings.clone());
decoder.set_dynamic_table_capacity(4096).unwrap();
let algo = DynamicCompressAlgo::new(HuffmanNever);
for section in §ions {
let output = algo.compress(&mut encoder, section.clone(), true).await;
apply_instructions(&mut encoder, &mut decoder);
verify_roundtrip(&settings, &decoder, &output, section);
}
});
}
}
}
}