use super::decode;
use crate::{
KnownHeaderName,
headers::{
entry_name::EntryName, field_section::FieldLineValue, hpack::dynamic_table::DynamicTable,
},
};
const DEFAULT_MAX: usize = 4096;
fn assert_table(table: &DynamicTable, expected: &[(usize, &str, &[u8])]) {
assert_eq!(
table.len(),
expected.len(),
"dynamic table length mismatch — got {} entries, expected {}",
table.len(),
expected.len()
);
for (dyn_index, expected_name, expected_value) in expected {
let entry = table
.get(*dyn_index)
.unwrap_or_else(|| panic!("no entry at dynamic index {dyn_index}"));
assert_eq!(
entry.name.as_str(),
*expected_name,
"entry {dyn_index} name mismatch"
);
assert_eq!(
entry.value.as_bytes(),
*expected_value,
"entry {dyn_index} value mismatch"
);
}
}
fn assert_table_size(table: &DynamicTable, expected: usize) {
assert_eq!(
table.size(),
expected,
"dynamic table size accounting mismatch"
);
}
#[test]
fn c_2_1_literal_header_with_indexing() {
const INPUT: &[u8] = &[
0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79, 0x0d, 0x63, 0x75,
0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72,
];
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(INPUT, &mut table, DEFAULT_MAX).unwrap();
let (_, headers) = fs.into_parts();
assert_eq!(
headers.get_str("custom-key").map(ToOwned::to_owned),
Some("custom-header".to_owned())
);
assert_table(&table, &[(1, "custom-key", b"custom-header")]);
assert_table_size(&table, 55);
}
#[test]
fn c_2_2_literal_header_without_indexing() {
const INPUT: &[u8] = &[
0x04, 0x0c, 0x2f, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x70, 0x61, 0x74, 0x68,
];
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(INPUT, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().path(), Some("/sample/path"));
assert_eq!(table.len(), 0, "without-indexing MUST NOT insert");
}
#[test]
fn c_2_3_literal_header_never_indexed() {
const INPUT: &[u8] = &[
0x10, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x06, 0x73, 0x65, 0x63, 0x72,
0x65, 0x74,
];
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(INPUT, &mut table, DEFAULT_MAX).unwrap();
let (_, headers) = fs.into_parts();
assert_eq!(
headers.get_str("password").map(ToOwned::to_owned),
Some("secret".to_owned())
);
assert_eq!(table.len(), 0, "never-indexed MUST NOT insert");
}
#[test]
fn c_2_4_indexed_header() {
const INPUT: &[u8] = &[0x82];
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(INPUT, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(
fs.pseudo_headers().method(),
Some(crate::Method::Get),
"static index 2 = :method: GET"
);
assert_eq!(table.len(), 0);
}
const C3_REQUEST_1: &[u8] = &[
0x82, 0x86, 0x84, 0x41, 0x0f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
0x2e, 0x63, 0x6f, 0x6d,
];
const C3_REQUEST_2: &[u8] = &[
0x82, 0x86, 0x84, 0xbe, 0x58, 0x08, 0x6e, 0x6f, 0x2d, 0x63, 0x61, 0x63, 0x68, 0x65,
];
const C3_REQUEST_3: &[u8] = &[
0x82, 0x87, 0x85, 0xbf, 0x40, 0x0a, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x6b, 0x65, 0x79,
0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x2d, 0x76, 0x61, 0x6c, 0x75, 0x65,
];
#[test]
fn c_3_request_sequence_without_huffman() {
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(C3_REQUEST_1, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().method(), Some(crate::Method::Get));
assert_eq!(fs.pseudo_headers().scheme(), Some("http"));
assert_eq!(fs.pseudo_headers().path(), Some("/"));
assert_eq!(fs.pseudo_headers().authority(), Some("www.example.com"));
assert_table(&table, &[(1, ":authority", b"www.example.com")]);
assert_table_size(&table, 57);
let fs = decode(C3_REQUEST_2, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().method(), Some(crate::Method::Get));
assert_eq!(fs.pseudo_headers().scheme(), Some("http"));
assert_eq!(fs.pseudo_headers().path(), Some("/"));
assert_eq!(fs.pseudo_headers().authority(), Some("www.example.com"));
let (_, headers) = fs.into_parts();
assert_eq!(
headers
.get_str(KnownHeaderName::CacheControl)
.map(ToOwned::to_owned),
Some("no-cache".to_owned())
);
assert_table(
&table,
&[
(1, "cache-control", b"no-cache"),
(2, ":authority", b"www.example.com"),
],
);
assert_table_size(&table, 110);
let fs = decode(C3_REQUEST_3, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().method(), Some(crate::Method::Get));
assert_eq!(fs.pseudo_headers().scheme(), Some("https"));
assert_eq!(fs.pseudo_headers().path(), Some("/index.html"));
assert_eq!(fs.pseudo_headers().authority(), Some("www.example.com"));
let (_, headers) = fs.into_parts();
assert_eq!(
headers.get_str("custom-key").map(ToOwned::to_owned),
Some("custom-value".to_owned())
);
assert_table(
&table,
&[
(1, "custom-key", b"custom-value"),
(2, "cache-control", b"no-cache"),
(3, ":authority", b"www.example.com"),
],
);
assert_table_size(&table, 164);
}
const C4_REQUEST_1: &[u8] = &[
0x82, 0x86, 0x84, 0x41, 0x8c, 0xf1, 0xe3, 0xc2, 0xe5, 0xf2, 0x3a, 0x6b, 0xa0, 0xab, 0x90, 0xf4,
0xff,
];
const C4_REQUEST_2: &[u8] = &[
0x82, 0x86, 0x84, 0xbe, 0x58, 0x86, 0xa8, 0xeb, 0x10, 0x64, 0x9c, 0xbf,
];
const C4_REQUEST_3: &[u8] = &[
0x82, 0x87, 0x85, 0xbf, 0x40, 0x88, 0x25, 0xa8, 0x49, 0xe9, 0x5b, 0xa9, 0x7d, 0x7f, 0x89, 0x25,
0xa8, 0x49, 0xe9, 0x5b, 0xb8, 0xe8, 0xb4, 0xbf,
];
#[test]
fn c_4_request_sequence_with_huffman() {
let mut table = DynamicTable::new(DEFAULT_MAX);
let fs = decode(C4_REQUEST_1, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().authority(), Some("www.example.com"));
assert_table_size(&table, 57);
let fs = decode(C4_REQUEST_2, &mut table, DEFAULT_MAX).unwrap();
let (_, headers) = fs.into_parts();
assert_eq!(
headers
.get_str(KnownHeaderName::CacheControl)
.map(ToOwned::to_owned),
Some("no-cache".to_owned())
);
assert_table_size(&table, 110);
let fs = decode(C4_REQUEST_3, &mut table, DEFAULT_MAX).unwrap();
assert_eq!(fs.pseudo_headers().path(), Some("/index.html"));
let (_, headers) = fs.into_parts();
assert_eq!(
headers.get_str("custom-key").map(ToOwned::to_owned),
Some("custom-value".to_owned())
);
assert_table_size(&table, 164);
}
const C5_MAX: usize = 256;
const C5_RESPONSE_1: &[u8] = &[
0x48, 0x03, 0x33, 0x30, 0x32, 0x58, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x61, 0x1d,
0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20, 0x32, 0x30, 0x31, 0x33,
0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x31, 0x20, 0x47, 0x4d, 0x54, 0x6e, 0x17, 0x68,
0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x77, 0x77, 0x77, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70,
0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d,
];
const C5_RESPONSE_2: &[u8] = &[0x48, 0x03, 0x33, 0x30, 0x37, 0xc1, 0xc0, 0xbf];
const C5_RESPONSE_3: &[u8] = &[
0x88, 0xc1, 0x61, 0x1d, 0x4d, 0x6f, 0x6e, 0x2c, 0x20, 0x32, 0x31, 0x20, 0x4f, 0x63, 0x74, 0x20,
0x32, 0x30, 0x31, 0x33, 0x20, 0x32, 0x30, 0x3a, 0x31, 0x33, 0x3a, 0x32, 0x32, 0x20, 0x47, 0x4d,
0x54, 0xc0, 0x5a, 0x04, 0x67, 0x7a, 0x69, 0x70, 0x77, 0x38, 0x66, 0x6f, 0x6f, 0x3d, 0x41, 0x53,
0x44, 0x4a, 0x4b, 0x48, 0x51, 0x4b, 0x42, 0x5a, 0x58, 0x4f, 0x51, 0x57, 0x45, 0x4f, 0x50, 0x49,
0x55, 0x41, 0x58, 0x51, 0x57, 0x45, 0x4f, 0x49, 0x55, 0x3b, 0x20, 0x6d, 0x61, 0x78, 0x2d, 0x61,
0x67, 0x65, 0x3d, 0x33, 0x36, 0x30, 0x30, 0x3b, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
0x3d, 0x31,
];
#[test]
fn c_5_response_sequence_with_eviction() {
let mut table = DynamicTable::new(C5_MAX);
let fs = decode(C5_RESPONSE_1, &mut table, C5_MAX).unwrap();
assert_eq!(fs.pseudo_headers().status(), Some(crate::Status::Found)); assert_table(
&table,
&[
(1, "location", b"https://www.example.com"),
(2, "date", b"Mon, 21 Oct 2013 20:13:21 GMT"),
(3, "cache-control", b"private"),
(4, ":status", b"302"),
],
);
assert_table_size(&table, 222);
let fs = decode(C5_RESPONSE_2, &mut table, C5_MAX).unwrap();
assert_eq!(
fs.pseudo_headers().status(),
Some(crate::Status::TemporaryRedirect)
);
let (_, headers) = fs.into_parts();
assert_eq!(
headers
.get_str(KnownHeaderName::CacheControl)
.map(ToOwned::to_owned),
Some("private".to_owned())
);
assert_table(
&table,
&[
(1, ":status", b"307"),
(2, "location", b"https://www.example.com"),
(3, "date", b"Mon, 21 Oct 2013 20:13:21 GMT"),
(4, "cache-control", b"private"),
],
);
assert_table_size(&table, 222);
let fs = decode(C5_RESPONSE_3, &mut table, C5_MAX).unwrap();
assert_eq!(fs.pseudo_headers().status(), Some(crate::Status::Ok));
let (_, headers) = fs.into_parts();
assert_eq!(
headers
.get_str(KnownHeaderName::ContentEncoding)
.map(ToOwned::to_owned),
Some("gzip".to_owned())
);
assert_eq!(
headers
.get_str(KnownHeaderName::SetCookie)
.map(ToOwned::to_owned),
Some("foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1".to_owned())
);
assert_table(
&table,
&[
(
1,
"set-cookie",
b"foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1",
),
(2, "content-encoding", b"gzip"),
(3, "date", b"Mon, 21 Oct 2013 20:13:22 GMT"),
],
);
assert_table_size(&table, 215);
}
#[test]
fn inserted_values_are_owned() {
let mut table = DynamicTable::new(DEFAULT_MAX);
decode(C3_REQUEST_1, &mut table, DEFAULT_MAX).unwrap();
let entry = table.get(1).unwrap();
assert!(matches!(entry.value, FieldLineValue::Owned(_)));
assert!(matches!(entry.name, EntryName::Pseudo(_)));
}