use crate::hpack::error::HeaderFieldError;
pub(crate) const fn check_field_name_const(name: &[u8]) {
if name.is_empty() {
panic!("HeaderField::from_static: field-name must not be empty (RFC 9110 5.6.2)");
}
let mut i = 0;
while i < name.len() {
let b = name[i];
if b.is_ascii_uppercase() {
panic!("HeaderField::from_static: field-name must be lowercase ASCII (RFC 9113 8.2.1)");
}
if b == b':' {
if i != 0 {
panic!(
"HeaderField::from_static: ':' is only allowed at the start of a pseudo-header name (RFC 9113 8.3)"
);
}
} else if !is_token_char_lower(b) {
panic!("HeaderField::from_static: field-name contains non-token byte (RFC 9110 5.6.2)");
}
i += 1;
}
}
pub(crate) const fn check_field_value_const(value: &[u8]) {
let len = value.len();
if len > 0 {
let first = value[0];
if first == 0x20 || first == 0x09 {
panic!(
"HeaderField::from_static: field-value must not start with SP or HTAB (RFC 9113 8.2.1)"
);
}
let last = value[len - 1];
if last == 0x20 || last == 0x09 {
panic!(
"HeaderField::from_static: field-value must not end with SP or HTAB (RFC 9113 8.2.1)"
);
}
}
let mut i = 0;
while i < len {
let b = value[i];
if b == 0x00 || b == 0x0d || b == 0x0a {
panic!(
"HeaderField::from_static: field-value must not contain NUL, CR, or LF (RFC 9113 8.2.1)"
);
}
i += 1;
}
}
pub(crate) const fn check_pseudo_header_const(name: &[u8], value: &[u8]) {
if name.is_empty() || name[0] != b':' {
return;
}
if bytes_eq(name, b":method") {
check_token_nonempty_const(value);
return;
}
if bytes_eq(name, b":scheme") {
check_scheme_const(value);
return;
}
if bytes_eq(name, b":authority") {
return;
}
if bytes_eq(name, b":path") {
check_path_const(value);
return;
}
if bytes_eq(name, b":protocol") {
check_token_nonempty_const(value);
return;
}
if bytes_eq(name, b":status") {
if value.len() != 3 {
panic!("HeaderField::from_static: :status value must be 3 ASCII digits (RFC 9110 15)");
}
let mut i = 0;
while i < 3 {
let b = value[i];
if !b.is_ascii_digit() {
panic!(
"HeaderField::from_static: :status value must be 3 ASCII digits (RFC 9110 15)"
);
}
i += 1;
}
return;
}
panic!("HeaderField::from_static: unknown pseudo-header name (RFC 9113 8.3, RFC 8441 4)");
}
const fn check_token_nonempty_const(value: &[u8]) {
if value.is_empty() {
panic!("HeaderField::from_static: pseudo-header value must not be empty");
}
let mut i = 0;
while i < value.len() {
let b = value[i];
if !is_token_char_case_insensitive(b) {
panic!("HeaderField::from_static: pseudo-header value contains non-token byte");
}
i += 1;
}
}
const fn check_scheme_const(value: &[u8]) {
if value.is_empty() {
panic!("HeaderField::from_static: :scheme value must not be empty (RFC 3986 3.1)");
}
let first = value[0];
if !first.is_ascii_alphabetic() {
panic!("HeaderField::from_static: :scheme value must start with ALPHA (RFC 3986 3.1)");
}
let mut i = 1;
while i < value.len() {
let b = value[i];
if !(b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.') {
panic!("HeaderField::from_static: :scheme value contains invalid byte (RFC 3986 3.1)");
}
i += 1;
}
}
const fn check_path_const(value: &[u8]) {
if value.is_empty() {
return;
}
if value.len() == 1 && value[0] == b'*' {
return;
}
if value[0] != b'/' {
panic!("HeaderField::from_static: :path must start with '/' or be '*' (RFC 9113 8.3.1)");
}
}
const fn bytes_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
pub(crate) fn validate_field_name(name: &[u8]) -> Result<(), HeaderFieldError> {
if name.is_empty() {
return Err(HeaderFieldError::EmptyFieldName);
}
for (i, &b) in name.iter().enumerate() {
if b.is_ascii_uppercase() {
return Err(HeaderFieldError::UppercaseFieldName {
name: name.to_vec(),
});
}
if b == b':' {
if i != 0 {
return Err(HeaderFieldError::InvalidFieldNameByte {
name: name.to_vec(),
byte: b,
});
}
} else if !is_token_char_lower(b) {
return Err(HeaderFieldError::InvalidFieldNameByte {
name: name.to_vec(),
byte: b,
});
}
}
Ok(())
}
pub(crate) fn validate_field_value(name: &[u8], value: &[u8]) -> Result<(), HeaderFieldError> {
if let Some(&first) = value.first()
&& (first == 0x20 || first == 0x09)
{
return Err(HeaderFieldError::FieldValueLeadingOrTrailingWhitespace {
name: name.to_vec(),
});
}
if let Some(&last) = value.last()
&& (last == 0x20 || last == 0x09)
{
return Err(HeaderFieldError::FieldValueLeadingOrTrailingWhitespace {
name: name.to_vec(),
});
}
for &b in value {
if b == 0x00 || b == 0x0d || b == 0x0a {
return Err(HeaderFieldError::InvalidFieldValueByte {
name: name.to_vec(),
byte: b,
});
}
}
Ok(())
}
pub(crate) fn validate_pseudo_header(name: &[u8], value: &[u8]) -> Result<(), HeaderFieldError> {
if name.is_empty() || name[0] != b':' {
return Ok(());
}
match name {
b":method" => {
if !is_valid_token_case_insensitive(value) {
return Err(HeaderFieldError::InvalidPseudoHeaderValue {
name: name.to_vec(),
value: value.to_vec(),
});
}
}
b":scheme" => {
if !is_valid_scheme(value) {
return Err(HeaderFieldError::InvalidPseudoHeaderValue {
name: name.to_vec(),
value: value.to_vec(),
});
}
}
b":path" => {
if !value.is_empty() && value != b"*" && !value.starts_with(b"/") {
return Err(HeaderFieldError::InvalidPseudoHeaderValue {
name: name.to_vec(),
value: value.to_vec(),
});
}
}
b":status" => {
if value.len() != 3 || !value.iter().all(u8::is_ascii_digit) {
return Err(HeaderFieldError::InvalidPseudoHeaderValue {
name: name.to_vec(),
value: value.to_vec(),
});
}
}
b":protocol" => {
if !is_valid_token_case_insensitive(value) {
return Err(HeaderFieldError::InvalidPseudoHeaderValue {
name: name.to_vec(),
value: value.to_vec(),
});
}
}
b":authority" => {
}
_ => {
return Err(HeaderFieldError::UnknownPseudoHeader {
name: name.to_vec(),
});
}
}
Ok(())
}
const fn is_token_char_lower(b: u8) -> bool {
matches!(b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'^' | b'_' | b'`' | b'|' | b'~' |
b'0'..=b'9' |
b'a'..=b'z'
)
}
const fn is_token_char_case_insensitive(b: u8) -> bool {
matches!(b,
b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' |
b'^' | b'_' | b'`' | b'|' | b'~' |
b'0'..=b'9' |
b'a'..=b'z' |
b'A'..=b'Z'
)
}
fn is_valid_token_case_insensitive(value: &[u8]) -> bool {
!value.is_empty() && value.iter().all(|&b| is_token_char_case_insensitive(b))
}
fn is_valid_scheme(value: &[u8]) -> bool {
if value.is_empty() {
return false;
}
if !value[0].is_ascii_alphabetic() {
return false;
}
value[1..]
.iter()
.all(|&b| b.is_ascii_alphanumeric() || b == b'+' || b == b'-' || b == b'.')
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn const_check_accepts_valid_pseudo() {
const _: () = check_field_name_const(b":method");
const _: () = check_pseudo_header_const(b":method", b"GET");
const _: () = check_pseudo_header_const(b":status", b"200");
const _: () = check_pseudo_header_const(b":scheme", b"https");
const _: () = check_pseudo_header_const(b":path", b"/");
const _: () = check_pseudo_header_const(b":path", b"*");
const _: () = check_pseudo_header_const(b":protocol", b"webtransport");
const _: () = check_field_value_const(b"GET");
}
#[test]
fn const_check_accepts_valid_regular() {
const _: () = check_field_name_const(b"content-type");
const _: () = check_pseudo_header_const(b"content-type", b"text/html");
const _: () = check_field_value_const(b"text/html");
}
mod syntax_equivalence {
use super::*;
use proptest::prelude::*;
use std::panic;
fn panic_payload_message(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"<non-string panic payload>".to_string()
}
}
fn catch_silent<F: FnOnce() + panic::UnwindSafe>(f: F) -> Result<(), String> {
let prev = panic::take_hook();
panic::set_hook(Box::new(|_| {}));
let result = panic::catch_unwind(f).map_err(panic_payload_message);
panic::set_hook(prev);
result
}
fn check_field_name_const_result(name: &[u8]) -> Result<(), String> {
catch_silent(panic::AssertUnwindSafe(|| {
check_field_name_const(name);
}))
}
fn validate_field_name_result(name: &[u8]) -> Result<(), String> {
validate_field_name(name).map_err(|e| format!("{e}"))
}
fn check_field_value_const_result(value: &[u8]) -> Result<(), String> {
catch_silent(panic::AssertUnwindSafe(|| {
check_field_value_const(value);
}))
}
fn validate_field_value_result(name: &[u8], value: &[u8]) -> Result<(), String> {
validate_field_value(name, value).map_err(|e| format!("{e}"))
}
fn check_pseudo_header_const_result(name: &[u8], value: &[u8]) -> Result<(), String> {
catch_silent(panic::AssertUnwindSafe(|| {
check_pseudo_header_const(name, value);
}))
}
fn validate_pseudo_header_result(name: &[u8], value: &[u8]) -> Result<(), String> {
validate_pseudo_header(name, value).map_err(|e| format!("{e}"))
}
fn name_strategy() -> impl Strategy<Value = Vec<u8>> {
prop_oneof![
Just(Vec::<u8>::new()),
Just(b":".to_vec()),
Just(b":method".to_vec()),
Just(b":scheme".to_vec()),
Just(b":authority".to_vec()),
Just(b":path".to_vec()),
Just(b":status".to_vec()),
Just(b":protocol".to_vec()),
Just(b":unknown".to_vec()),
Just(b"Host".to_vec()),
Just(b"content-type".to_vec()),
Just(b"X-Inject\r\nname".to_vec()),
prop::collection::vec(any::<u8>(), 0..=32),
]
}
fn value_strategy() -> impl Strategy<Value = Vec<u8>> {
prop_oneof![
Just(Vec::<u8>::new()),
Just(b" ".to_vec()),
Just(b"\t".to_vec()),
Just(b" GET".to_vec()),
Just(b"GET ".to_vec()),
Just(b"GET\r\n".to_vec()),
Just(b"\0".to_vec()),
Just(b"200".to_vec()),
Just(b"199".to_vec()),
Just(b"99".to_vec()),
Just(b"https".to_vec()),
Just(b"/foo".to_vec()),
Just(b"*".to_vec()),
Just(b"webtransport".to_vec()),
prop::collection::vec(any::<u8>(), 0..=48),
]
}
proptest! {
#![proptest_config(ProptestConfig { cases: 2048, ..ProptestConfig::default() })]
#[test]
fn prop_check_field_name_equivalence(name in name_strategy()) {
let const_r = check_field_name_const_result(&name);
let runtime_r = validate_field_name_result(&name);
prop_assert_eq!(
const_r.is_err(),
runtime_r.is_err(),
"field-name check mismatch for {:?}: const={:?} runtime={:?}",
name,
const_r,
runtime_r
);
}
#[test]
fn prop_check_field_value_equivalence(value in value_strategy()) {
let const_r = check_field_value_const_result(&value);
let runtime_r = validate_field_value_result(b"x-test", &value);
prop_assert_eq!(
const_r.is_err(),
runtime_r.is_err(),
"field-value check mismatch for {:?}: const={:?} runtime={:?}",
value,
const_r,
runtime_r
);
}
#[test]
fn prop_check_pseudo_header_equivalence(
name in name_strategy(),
value in value_strategy(),
) {
let const_r = check_pseudo_header_const_result(&name, &value);
let runtime_r = validate_pseudo_header_result(&name, &value);
prop_assert_eq!(
const_r.is_err(),
runtime_r.is_err(),
"pseudo-header check mismatch for name={:?} value={:?}: const={:?} runtime={:?}",
name,
value,
const_r,
runtime_r
);
}
}
}
}