use crate::courierust_error::{Error, Result};
use crate::courierust_hpack::huffman::HuffmanDecoder;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
pub const STATIC_TABLE: [(&str, &str); 99] = [
(":authority", ""),
(":path", "/"),
("age", "0"),
("content-disposition", ""),
("content-length", "0"),
("cookie", ""),
("date", ""),
("etag", ""),
("if-modified-since", ""),
("if-none-match", ""),
("last-modified", ""),
("link", ""),
("location", ""),
("referer", ""),
("set-cookie", ""),
(":method", "CONNECT"),
(":method", "DELETE"),
(":method", "GET"),
(":method", "HEAD"),
(":method", "OPTIONS"),
(":method", "POST"),
(":method", "PUT"),
(":scheme", "http"),
(":scheme", "https"),
(":status", "103"),
(":status", "200"),
(":status", "304"),
(":status", "404"),
(":status", "503"),
("accept", "*/*"),
("accept", "application/dns-message"),
("accept-encoding", "gzip, deflate, br"),
("accept-ranges", "bytes"),
("access-control-allow-headers", "cache-control"),
("access-control-allow-headers", "content-type"),
("access-control-allow-origin", "*"),
("cache-control", "max-age=0"),
("cache-control", "max-age=2592000"),
("cache-control", "max-age=604800"),
("cache-control", "no-cache"),
("cache-control", "no-store"),
("cache-control", "public, max-age=31536000"),
("content-encoding", "br"),
("content-encoding", "gzip"),
("content-type", "application/dns-message"),
("content-type", "application/javascript"),
("content-type", "application/json"),
("content-type", "application/x-www-form-urlencoded"),
("content-type", "image/gif"),
("content-type", "image/jpeg"),
("content-type", "image/png"),
("content-type", "text/css"),
("content-type", "text/html; charset=utf-8"),
("content-type", "text/plain"),
("content-type", "text/plain;charset=utf-8"),
("range", "bytes=0-"),
("strict-transport-security", "max-age=31536000"),
(
"strict-transport-security",
"max-age=31536000; includesubdomains",
),
(
"strict-transport-security",
"max-age=31536000; includesubdomains; preload",
),
("vary", "accept-encoding"),
("vary", "origin"),
("x-content-type-options", "nosniff"),
("x-xss-protection", "1; mode=block"),
(":status", "100"),
(":status", "204"),
(":status", "206"),
(":status", "302"),
(":status", "400"),
(":status", "403"),
(":status", "421"),
(":status", "425"),
(":status", "500"),
("accept-language", ""),
("access-control-allow-credentials", "FALSE"),
("access-control-allow-credentials", "TRUE"),
("access-control-allow-headers", "*"),
("access-control-allow-methods", "get"),
("access-control-allow-methods", "get, post, options"),
("access-control-allow-methods", "options"),
("access-control-expose-headers", "content-length"),
("access-control-request-headers", "content-type"),
("access-control-request-method", "get"),
("access-control-request-method", "post"),
("alt-svc", "clear"),
("authorization", ""),
(
"content-security-policy",
"script-src 'none'; object-src 'none'; base-uri 'none'",
),
("early-data", "1"),
("expect-ct", ""),
("forwarded", ""),
("if-range", ""),
("origin", ""),
("purpose", "prefetch"),
("server", ""),
("timing-allow-origin", "*"),
("upgrade-insecure-requests", "1"),
("user-agent", ""),
("x-forwarded-for", ""),
("x-frame-options", "deny"),
("x-frame-options", "sameorigin"),
];
pub fn static_index(name: &str, value: &str) -> Option<u64> {
STATIC_TABLE
.iter()
.position(|(n, v)| *n == name && !v.is_empty() && *v == value)
.map(|i| i as u64)
}
pub fn static_name_index(name: &str) -> Option<u64> {
STATIC_TABLE
.iter()
.position(|(n, _)| *n == name)
.map(|i| i as u64)
}
pub fn encode_integer(value: u64, prefix_bits: u32, first_byte: u8, out: &mut Vec<u8>) {
let max_prefix = (1u64 << prefix_bits) - 1;
if value < max_prefix {
out.push(first_byte | value as u8);
return;
}
out.push(first_byte | max_prefix as u8);
let mut v = value - max_prefix;
while v >= 128 {
out.push((v as u8 & 0x7f) | 0x80);
v >>= 7;
}
out.push(v as u8);
}
pub fn decode_integer(buf: &[u8], prefix_bits: u32, pos: &mut usize) -> Result<u64> {
if !(1..=8).contains(&prefix_bits) {
return Err(Error::protocol("QPACK prefix width out of range"));
}
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
*pos += 1;
let max_prefix = (1u64 << prefix_bits) - 1;
let mut value = (first & max_prefix as u8) as u64;
if value < max_prefix {
return Ok(value);
}
let mut shift = 0u32;
loop {
let byte = *buf.get(*pos).ok_or_else(Error::eof)?;
*pos += 1;
let part = ((byte & 0x7f) as u64)
.checked_shl(shift)
.ok_or_else(|| Error::overflow("QPACK integer too large"))?;
value = value
.checked_add(part)
.ok_or_else(|| Error::overflow("QPACK integer overflow"))?;
if byte & 0x80 == 0 {
return Ok(value);
}
shift = shift
.checked_add(7)
.ok_or_else(|| Error::overflow("QPACK integer too large"))?;
}
}
pub fn encode_string(value: &[u8], prefix_bits: u8, first_byte: u8, out: &mut Vec<u8>) {
debug_assert!((2..=8).contains(&prefix_bits));
let mut huffman = Vec::new();
crate::courierust_hpack::huffman::encode(value, &mut huffman);
let use_huffman = huffman.len() < value.len();
let (payload, hbit) = if use_huffman {
(&huffman[..], 1u8 << (prefix_bits - 1))
} else {
(value, 0u8)
};
encode_integer(
payload.len() as u64,
(prefix_bits - 1) as u32,
first_byte | hbit,
out,
);
out.extend_from_slice(payload);
}
pub fn decode_string(
buf: &[u8],
prefix_bits: u8,
pos: &mut usize,
huff: &HuffmanDecoder,
) -> Result<Vec<u8>> {
debug_assert!((2..=8).contains(&prefix_bits));
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
let huffman = first & (1u8 << (prefix_bits - 1)) != 0;
let len = usize::try_from(decode_integer(buf, (prefix_bits - 1) as u32, pos)?)
.map_err(|_| Error::overflow("QPACK string length does not fit usize"))?;
let end = (*pos)
.checked_add(len)
.ok_or_else(|| Error::overflow("QPACK string length overflow"))?;
if buf.len() < end {
return Err(Error::eof());
}
let raw = &buf[*pos..end];
*pos = end;
if huffman {
let mut out = Vec::new();
huff.decode(raw, &mut out, raw.len().saturating_mul(2).max(1024))
.map_err(|_| Error::protocol("QPACK Huffman decode failed"))?;
Ok(out)
} else {
Ok(raw.to_vec())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DynEntry {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default)]
pub struct DynamicTable {
entries: alloc::collections::VecDeque<DynEntry>,
size: usize,
capacity: usize,
insert_count: u64,
}
impl DynamicTable {
pub fn new(capacity: usize) -> Self {
Self {
entries: alloc::collections::VecDeque::new(),
size: 0,
capacity,
insert_count: 0,
}
}
fn entry_size(name: &str, value: &str) -> usize {
name.len().saturating_add(value.len()).saturating_add(32)
}
pub fn set_capacity(&mut self, capacity: usize) {
self.capacity = capacity;
while self.size > self.capacity {
self.evict_one();
}
}
pub fn capacity(&self) -> usize {
self.capacity
}
pub fn size(&self) -> usize {
self.size
}
pub fn insert_count(&self) -> u64 {
self.insert_count
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn evict_one(&mut self) {
if let Some(entry) = self.entries.pop_front() {
self.size = self
.size
.saturating_sub(Self::entry_size(&entry.name, &entry.value));
}
}
pub fn insert(&mut self, name: &str, value: &str) -> bool {
let entry_size = Self::entry_size(name, value);
if entry_size > self.capacity {
return false;
}
while self.size.saturating_add(entry_size) > self.capacity {
self.evict_one();
}
self.entries.push_back(DynEntry {
name: name.into(),
value: value.into(),
});
self.size = self.size.saturating_add(entry_size);
self.insert_count = self.insert_count.wrapping_add(1);
true
}
pub fn get(&self, absolute: u64) -> Option<&DynEntry> {
let first = self.insert_count.saturating_sub(self.entries.len() as u64);
let offset = absolute.checked_sub(first)?;
if absolute >= self.insert_count {
return None;
}
usize::try_from(offset)
.ok()
.and_then(|index| self.entries.get(index))
}
pub fn find(&self, name: &str, value: &str) -> Option<u64> {
let first = self.insert_count.saturating_sub(self.entries.len() as u64);
self.entries
.iter()
.rposition(|e| e.name == name && e.value == value)
.map(|i| first + i as u64)
}
pub fn find_name(&self, name: &str) -> Option<u64> {
let first = self.insert_count.saturating_sub(self.entries.len() as u64);
self.entries
.iter()
.rposition(|e| e.name == name)
.map(|i| first + i as u64)
}
}
pub fn encode_field_section_prefix(
required_insert_count: u64,
base: u64,
max_table_capacity: u64,
out: &mut Vec<u8>,
) {
let max_entries = (max_table_capacity / 32).max(1);
let full_range = 2 * max_entries;
let enc = if required_insert_count == 0 {
0
} else {
(required_insert_count % full_range) + 1
};
encode_integer(enc, 8, 0, out);
if base >= required_insert_count {
encode_integer(base - required_insert_count, 7, 0, out);
} else {
encode_integer(required_insert_count - base - 1, 7, 0x80, out);
}
}
pub fn decode_field_section_prefix(
buf: &[u8],
pos: &mut usize,
total_inserts: u64,
max_table_capacity: u64,
) -> Result<(u64, u64)> {
let max_entries = (max_table_capacity / 32).max(1);
let full_range = 2 * max_entries;
let enc = decode_integer(buf, 8, pos)?;
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
let sign = first & 0x80 != 0;
let delta = decode_integer(buf, 7, pos)?;
let required = if enc == 0 {
0
} else {
let max_value = total_inserts
.checked_add(max_entries)
.ok_or_else(|| Error::overflow("QPACK insert count overflow"))?;
let max_wrapped = (max_value / full_range) * full_range;
let mut ric = max_wrapped
.checked_add(enc)
.and_then(|v| v.checked_sub(1))
.ok_or_else(|| Error::overflow("QPACK required insert count overflow"))?;
if ric > max_value {
if ric <= full_range {
return Err(Error::protocol("QPACK Required Insert Count out of range"));
}
ric -= full_range;
}
if ric == 0 {
return Err(Error::protocol(
"QPACK Required Insert Count wrapped to zero",
));
}
ric
};
let base = if sign {
required
.checked_sub(delta + 1)
.ok_or_else(|| Error::protocol("QPACK negative base"))?
} else {
required
.checked_add(delta)
.ok_or_else(|| Error::overflow("QPACK base overflow"))?
};
Ok((required, base))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldLine {
pub name: String,
pub value: Vec<u8>,
pub never_indexed: bool,
}
pub fn encode_field_line(
name: &str,
value: &[u8],
dyn_table: &DynamicTable,
base: u64,
out: &mut Vec<u8>,
) {
let value_str = core::str::from_utf8(value).unwrap_or("");
if let Some(index) = static_index(name, value_str) {
encode_integer(index, 6, 0xc0, out);
return;
}
if let Some(abs) = dyn_table.find(name, value_str) {
if abs < base {
let rel = base - 1 - abs;
encode_integer(rel, 6, 0x80, out);
} else {
let pbi = abs - base;
encode_integer(pbi, 4, 0x10, out);
}
return;
}
if let Some(index) = static_name_index(name) {
encode_integer(index, 4, 0x50, out);
encode_string(value, 8, 0x00, out);
return;
}
if let Some(abs) = dyn_table.find_name(name) {
if abs < base {
let rel = base - 1 - abs;
encode_integer(rel, 4, 0x40, out);
} else {
let pbi = abs - base;
encode_integer(pbi, 3, 0x00, out);
}
encode_string(value, 8, 0x00, out);
return;
}
encode_string(name.as_bytes(), 4, 0x20, out);
encode_string(value, 8, 0x00, out);
}
pub fn decode_field_line(
buf: &[u8],
pos: &mut usize,
dyn_table: &DynamicTable,
base: u64,
huff: &HuffmanDecoder,
) -> Result<FieldLine> {
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
if first & 0x80 != 0 {
let static_ref = first & 0x40 != 0;
let index = decode_integer(buf, 6, pos)?;
if static_ref {
let (name, value) = static_entry(index)?;
Ok(FieldLine {
name,
value: value.as_bytes().to_vec(),
never_indexed: false,
})
} else {
if index >= base {
return Err(Error::protocol("QPACK dynamic relative index out of range"));
}
let abs = base - 1 - index;
let entry = dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK dynamic table entry missing"))?;
Ok(FieldLine {
name: entry.name.clone(),
value: entry.value.as_bytes().to_vec(),
never_indexed: false,
})
}
} else if first & 0xc0 == 0x40 {
let never_indexed = first & 0x20 != 0;
let static_ref = first & 0x10 != 0;
let index = decode_integer(buf, 4, pos)?;
let name = if static_ref {
STATIC_TABLE
.get(
usize::try_from(index)
.map_err(|_| Error::overflow("QPACK static index does not fit usize"))?,
)
.ok_or_else(|| Error::protocol("QPACK static name index out of range"))?
.0
.to_string()
} else {
if index >= base {
return Err(Error::protocol(
"QPACK dynamic name relative index out of range",
));
}
let abs = base - 1 - index;
dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK dynamic table entry missing"))?
.name
.clone()
};
let value = decode_string(buf, 8, pos, huff)?;
Ok(FieldLine {
name,
value,
never_indexed,
})
} else if first & 0xe0 == 0x20 {
let never_indexed = first & 0x10 != 0;
let name_raw = decode_string(buf, 4, pos, huff)?;
let name = String::from_utf8(name_raw)
.map_err(|_| Error::protocol("QPACK field name is not UTF-8"))?;
let value = decode_string(buf, 8, pos, huff)?;
Ok(FieldLine {
name,
value,
never_indexed,
})
} else if first & 0xf0 == 0x10 {
let pbi = decode_integer(buf, 4, pos)?;
let abs = base
.checked_add(pbi)
.ok_or_else(|| Error::overflow("QPACK post-base index overflow"))?;
let entry = dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK post-base index out of range"))?;
Ok(FieldLine {
name: entry.name.clone(),
value: entry.value.as_bytes().to_vec(),
never_indexed: false,
})
} else {
let never_indexed = first & 0x10 != 0;
let pbi = decode_integer(buf, 3, pos)?;
let abs = base
.checked_add(pbi)
.ok_or_else(|| Error::overflow("QPACK post-base index overflow"))?;
let name = dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK post-base name index out of range"))?
.name
.clone();
let value = decode_string(buf, 8, pos, huff)?;
Ok(FieldLine {
name,
value,
never_indexed,
})
}
}
fn static_entry(index: u64) -> Result<(String, String)> {
let (name, value) = STATIC_TABLE
.get(
usize::try_from(index)
.map_err(|_| Error::overflow("QPACK static index does not fit usize"))?,
)
.ok_or_else(|| Error::protocol("QPACK static index out of range"))?;
Ok((name.to_string(), value.to_string()))
}
pub fn decode_encoder_instruction(
buf: &[u8],
pos: &mut usize,
dyn_table: &mut DynamicTable,
insert_count: u64,
advertised_capacity: Option<u64>,
huff: &HuffmanDecoder,
) -> Result<()> {
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
if first & 0x80 != 0 {
let static_ref = first & 0x40 != 0;
let index = decode_integer(buf, 6, pos)?;
let value = decode_string(buf, 8, pos, huff)?;
let value_str =
String::from_utf8(value).map_err(|_| Error::protocol("QPACK value is not UTF-8"))?;
let name = if static_ref {
STATIC_TABLE
.get(
usize::try_from(index)
.map_err(|_| Error::overflow("QPACK static index does not fit usize"))?,
)
.ok_or_else(|| Error::protocol("QPACK static index out of range"))?
.0
.to_string()
} else {
if index >= insert_count {
return Err(Error::protocol("QPACK dynamic reference out of range"));
}
let abs = insert_count - 1 - index;
dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK dynamic table entry missing"))?
.name
.clone()
};
insert_checked(dyn_table, &name, &value_str)?;
return Ok(());
}
if first & 0xc0 == 0x40 {
let name_raw = decode_string(buf, 6, pos, huff)?;
let name = String::from_utf8(name_raw)
.map_err(|_| Error::protocol("QPACK field name is not UTF-8"))?;
let value = decode_string(buf, 8, pos, huff)?;
let value_str =
String::from_utf8(value).map_err(|_| Error::protocol("QPACK value is not UTF-8"))?;
insert_checked(dyn_table, &name, &value_str)?;
return Ok(());
}
if first & 0xe0 == 0x20 {
let capacity = decode_integer(buf, 5, pos)?;
if advertised_capacity.is_some_and(|advertised| capacity > advertised) {
return Err(Error::protocol(
"QPACK Set Capacity exceeds the advertised maximum",
));
}
dyn_table.set_capacity(
usize::try_from(capacity)
.map_err(|_| Error::overflow("QPACK table capacity does not fit usize"))?,
);
return Ok(());
}
let index = decode_integer(buf, 5, pos)?;
if index >= insert_count {
return Err(Error::protocol("QPACK duplicate index out of range"));
}
let abs = insert_count - 1 - index;
let entry = dyn_table
.get(abs)
.ok_or_else(|| Error::protocol("QPACK dynamic table entry missing"))?
.clone();
insert_checked(dyn_table, &entry.name, &entry.value)?;
Ok(())
}
fn insert_checked(table: &mut DynamicTable, name: &str, value: &str) -> Result<()> {
if !table.insert(name, value) {
return Err(Error::protocol(
"QPACK insert exceeds the dynamic table capacity",
));
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderInstruction<'a> {
SetCapacity(u64),
InsertWithNameRef {
static_ref: bool,
index: u64,
value: &'a [u8],
},
InsertWithLiteralName {
name: &'a [u8],
value: &'a [u8],
},
Duplicate(u64),
}
pub fn encode_encoder_instruction(instruction: &EncoderInstruction, out: &mut Vec<u8>) {
match instruction {
EncoderInstruction::SetCapacity(capacity) => {
encode_integer(*capacity, 5, 0x20, out);
}
EncoderInstruction::InsertWithNameRef {
static_ref,
index,
value,
} => {
let first = if *static_ref { 0xc0 } else { 0x80 };
encode_integer(*index, 6, first, out);
encode_string(value, 8, 0x00, out);
}
EncoderInstruction::InsertWithLiteralName { name, value } => {
encode_string(name, 6, 0x40, out);
encode_string(value, 8, 0x00, out);
}
EncoderInstruction::Duplicate(index) => {
encode_integer(*index, 5, 0x00, out);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DecoderInstruction {
SectionAck(u64),
StreamCancellation(u64),
InsertCountIncrement(u64),
}
pub fn encode_decoder_instruction(instruction: &DecoderInstruction, out: &mut Vec<u8>) {
match instruction {
DecoderInstruction::SectionAck(stream_id) => {
encode_integer(*stream_id, 7, 0x80, out);
}
DecoderInstruction::StreamCancellation(stream_id) => {
encode_integer(*stream_id, 6, 0x40, out);
}
DecoderInstruction::InsertCountIncrement(increment) => {
encode_integer(*increment, 6, 0x00, out);
}
}
}
pub fn decode_decoder_instruction(buf: &[u8], pos: &mut usize) -> Result<DecoderInstruction> {
let first = *buf.get(*pos).ok_or_else(Error::eof)?;
if first & 0x80 != 0 {
let stream_id = decode_integer(buf, 7, pos)?;
Ok(DecoderInstruction::SectionAck(stream_id))
} else if first & 0xc0 == 0x40 {
let stream_id = decode_integer(buf, 6, pos)?;
Ok(DecoderInstruction::StreamCancellation(stream_id))
} else {
let increment = decode_integer(buf, 6, pos)?;
Ok(DecoderInstruction::InsertCountIncrement(increment))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_huff() -> HuffmanDecoder {
HuffmanDecoder::new()
}
#[test]
fn static_table_matches_appendix_a() {
assert_eq!(STATIC_TABLE.len(), 99);
assert_eq!(STATIC_TABLE[0], (":authority", ""));
assert_eq!(STATIC_TABLE[1], (":path", "/"));
assert_eq!(STATIC_TABLE[24], (":status", "103"));
assert_eq!(STATIC_TABLE[25], (":status", "200"));
assert_eq!(static_index(":status", "200"), Some(25));
assert_eq!(static_index(":status", "201"), None);
assert_eq!(static_name_index("content-length"), Some(4));
}
#[test]
fn integer_round_trip_prefixes() {
for prefix in [1u32, 3, 4, 5, 6, 7, 8] {
for value in [0u64, 1, 10, 100, 1000, 16384, 1 << 20, 1 << 40] {
let mut out = Vec::new();
encode_integer(value, prefix, 0, &mut out);
let mut pos = 0;
assert_eq!(decode_integer(&out, prefix, &mut pos).unwrap(), value);
assert_eq!(pos, out.len());
}
}
}
#[test]
fn string_round_trip_all_prefixes() {
for prefix in [4u8, 6, 8] {
for value in [b"".as_slice(), b"hello", b"a", &[0u8, 255, 128]] {
let mut out = Vec::new();
encode_string(value, prefix, 0, &mut out);
let mut pos = 0;
assert_eq!(
decode_string(&out, prefix, &mut pos, &test_huff()).unwrap(),
value
);
assert_eq!(pos, out.len());
}
}
}
#[test]
fn rfc_appendix_b1_literal_name_reference() {
let wire: Vec<u8> = vec![
0x00, 0x00, 0x51, 0x0b, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2e, 0x68, 0x74, 0x6d,
0x6c, ];
let mut pos = 0;
let (required, base) = decode_field_section_prefix(&wire, &mut pos, 0, 0).unwrap();
assert_eq!((required, base), (0, 0));
let line =
decode_field_line(&wire, &mut pos, &DynamicTable::new(0), base, &test_huff()).unwrap();
assert_eq!(line.name, ":path");
assert_eq!(line.value, b"/index.html");
assert_eq!(pos, wire.len());
}
#[test]
fn rfc_appendix_b2_dynamic_table() {
let mut table = DynamicTable::new(0);
let mut enc: Vec<u8> = vec![0x3f, 0xbd, 0x01];
enc.extend_from_slice(&[0xc0, 0x0f]);
enc.extend_from_slice(b"www.example.com");
enc.extend_from_slice(&[0xc1, 0x0c]);
enc.extend_from_slice(b"/sample/path");
let mut pos = 0;
while pos < enc.len() {
let insert_count = table.insert_count();
decode_encoder_instruction(
&enc,
&mut pos,
&mut table,
insert_count,
Some(220),
&test_huff(),
)
.unwrap();
}
assert_eq!(table.insert_count(), 2);
assert_eq!(table.get(0).unwrap().name, ":authority");
assert_eq!(table.get(0).unwrap().value, "www.example.com");
assert_eq!(table.get(1).unwrap().name, ":path");
assert_eq!(table.get(1).unwrap().value, "/sample/path");
let section: Vec<u8> = vec![0x03, 0x81, 0x10, 0x11];
let mut pos = 0;
let (required, base) =
decode_field_section_prefix(§ion, &mut pos, table.insert_count(), 220).unwrap();
assert_eq!(required, 2, "RFC 9204 B.2 Required Insert Count");
assert_eq!(base, 0, "RFC 9204 B.2 Base");
let l1 = decode_field_line(§ion, &mut pos, &table, base, &test_huff()).unwrap();
assert_eq!(
(l1.name.as_str(), l1.value.as_slice()),
(":authority", b"www.example.com".as_slice())
);
let l2 = decode_field_line(§ion, &mut pos, &table, base, &test_huff()).unwrap();
assert_eq!(
(l2.name.as_str(), l2.value.as_slice()),
(":path", b"/sample/path".as_slice())
);
assert_eq!(pos, section.len());
}
#[test]
fn rfc_appendix_b3_speculative_insert() {
let mut enc: Vec<u8> = Vec::new();
encode_string(b"custom-key", 6, 0x40, &mut enc);
encode_string(b"custom-value", 8, 0x00, &mut enc);
let mut table = DynamicTable::new(1000);
let mut pos = 0;
decode_encoder_instruction(&enc, &mut pos, &mut table, 0, Some(220), &test_huff()).unwrap();
assert_eq!(pos, enc.len());
assert_eq!(table.insert_count(), 1);
assert_eq!(table.get(0).unwrap().name, "custom-key");
assert_eq!(table.get(0).unwrap().value, "custom-value");
let mut pos = 0;
assert_eq!(
decode_decoder_instruction(&[0x01], &mut pos).unwrap(),
DecoderInstruction::InsertCountIncrement(1)
);
assert_eq!(pos, 1);
}
#[test]
fn rfc_appendix_b4_duplicate_and_cancellation() {
let mut table = DynamicTable::new(1000);
table.insert(":authority", "www.example.com");
table.insert(":path", "/sample/path");
table.insert("custom-key", "custom-value");
assert_eq!(table.insert_count(), 3);
let mut pos = 0;
let insert_count = table.insert_count();
decode_encoder_instruction(
&[0x02],
&mut pos,
&mut table,
insert_count,
Some(220),
&test_huff(),
)
.unwrap();
assert_eq!(table.insert_count(), 4);
assert_eq!(table.get(3).unwrap().name, ":authority");
let section: Vec<u8> = vec![0x05, 0x00, 0x80, 0xc1, 0x81];
let mut pos = 0;
let (required, base) =
decode_field_section_prefix(§ion, &mut pos, table.insert_count(), 220).unwrap();
assert_eq!((required, base), (4, 4));
let l1 = decode_field_line(§ion, &mut pos, &table, base, &test_huff()).unwrap();
assert_eq!(
(l1.name.as_str(), l1.value.as_slice()),
(":authority", b"www.example.com".as_slice())
);
let l2 = decode_field_line(§ion, &mut pos, &table, base, &test_huff()).unwrap();
assert_eq!(
(l2.name.as_str(), l2.value.as_slice()),
(":path", b"/".as_slice())
);
let l3 = decode_field_line(§ion, &mut pos, &table, base, &test_huff()).unwrap();
assert_eq!(
(l3.name.as_str(), l3.value.as_slice()),
("custom-key", b"custom-value".as_slice())
);
let mut pos = 0;
assert_eq!(
decode_decoder_instruction(&[0x48], &mut pos).unwrap(),
DecoderInstruction::StreamCancellation(8)
);
}
#[test]
fn dynamic_table_eviction() {
let mut table = DynamicTable::new(1000);
table.insert("aaaa", "a");
table.insert("bbbb", "b");
assert_eq!(table.len(), 2);
table.set_capacity(4 + 1 + 32);
assert_eq!(table.len(), 1);
assert_eq!(table.get(1).unwrap().name, "bbbb");
}
#[test]
fn prefix_round_trip() {
for (ric, base) in [(0u64, 0u64), (2, 0), (9, 6), (4, 4), (1, 3)] {
let mut out = Vec::new();
encode_field_section_prefix(ric, base, 220, &mut out);
let mut pos = 0;
let (dric, dbase) = decode_field_section_prefix(&out, &mut pos, ric, 220).unwrap();
assert_eq!((dric, dbase), (ric, base), "RIC={ric} Base={base}");
assert_eq!(pos, out.len());
}
}
}