use crate::error::HttpError;
const TCHAR: u8 = 1;
const FIELD: u8 = 2;
const TARGET: u8 = 4;
const HEX: u8 = 8;
const QDTEXT: u8 = 16;
const fn build_classes() -> [u8; 256] {
let mut t = [0u8; 256];
let mut b = 0usize;
while b < 256 {
let c = b as u8;
let mut class = 0u8;
let alpha = c.is_ascii_alphabetic();
let digit = c.is_ascii_digit();
if alpha
|| digit
|| matches!(
c,
b'!' | b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
{
class |= TCHAR;
}
if c == b'\t' || (c >= 0x20 && c != 0x7f) {
class |= FIELD;
}
if alpha
|| digit
|| matches!(
c,
b'-' | b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b':'
| b'@'
| b'/'
| b'?'
| b'%'
)
{
class |= TARGET;
}
if c.is_ascii_hexdigit() {
class |= HEX;
}
if c == b'\t'
|| c == b' '
|| c == 0x21
|| (c >= 0x23 && c <= 0x5b)
|| (c >= 0x5d && c <= 0x7e)
|| c >= 0x80
{
class |= QDTEXT;
}
t[b] = class;
b += 1;
}
t
}
static CLASSES: [u8; 256] = build_classes();
#[inline]
fn has(b: u8, class: u8) -> bool {
CLASSES[b as usize] & class != 0
}
#[inline]
pub(crate) fn is_tchar(b: u8) -> bool {
has(b, TCHAR)
}
#[inline]
pub(crate) fn is_token(s: &[u8]) -> bool {
!s.is_empty() && s.iter().all(|&b| is_tchar(b))
}
#[inline]
pub(crate) fn is_ows(b: u8) -> bool {
b == b' ' || b == b'\t'
}
#[inline]
pub(crate) fn trim_ows(mut s: &[u8]) -> &[u8] {
while let [first, rest @ ..] = s {
if is_ows(*first) {
s = rest;
} else {
break;
}
}
while let [rest @ .., last] = s {
if is_ows(*last) {
s = rest;
} else {
break;
}
}
s
}
#[inline]
pub(crate) fn check_field_value(value: &[u8]) -> Result<(), HttpError> {
let mut chunks = value.chunks_exact(8);
for chunk in &mut chunks {
let mut word = [0u8; 8];
word.copy_from_slice(chunk);
let w = u64::from_le_bytes(word);
let low = w.wrapping_sub(0x2020_2020_2020_2020) & !w & 0x8080_8080_8080_8080;
let del = {
let x = w ^ 0x7f7f_7f7f_7f7f_7f7f;
x.wrapping_sub(0x0101_0101_0101_0101) & !x & 0x8080_8080_8080_8080
};
let high = w & 0x8080_8080_8080_8080;
if low | del | high != 0 {
check_field_bytes(chunk)?;
}
}
check_field_bytes(chunks.remainder())
}
#[inline]
fn check_field_bytes(bytes: &[u8]) -> Result<(), HttpError> {
for &b in bytes {
if !has(b, FIELD) {
return Err(if b == b'\r' {
HttpError::BareCr
} else {
HttpError::InvalidHeaderValue
});
}
}
Ok(())
}
#[inline]
pub(crate) fn is_reason(s: &[u8]) -> bool {
s.iter().all(|&b| has(b, FIELD))
}
pub(crate) fn is_target_chars(s: &[u8]) -> bool {
let mut i = 0;
while i < s.len() {
let b = s[i];
if !has(b, TARGET) {
return false;
}
if b == b'%' {
match (s.get(i + 1), s.get(i + 2)) {
(Some(&h1), Some(&h2)) if has(h1, HEX) && has(h2, HEX) => i += 3,
_ => return false,
}
} else {
i += 1;
}
}
true
}
pub(crate) fn is_host_value(s: &[u8]) -> bool {
if s.is_empty() {
return true;
}
let (host, port) = if s[0] == b'[' {
match s.iter().position(|&b| b == b']') {
Some(close) => {
let inner = &s[1..close];
if inner.is_empty()
|| !inner
.iter()
.all(|&b| b.is_ascii_hexdigit() || matches!(b, b':' | b'.' | b'v' | b'V'))
{
return false;
}
let rest = &s[close + 1..];
match rest {
[] => (&s[..0], None),
[b':', port @ ..] => (&s[..0], Some(port)),
_ => return false,
}
}
None => return false,
}
} else {
match s.iter().rposition(|&b| b == b':') {
Some(0) => return false,
Some(colon) => (&s[..colon], Some(&s[colon + 1..])),
None => (s, None),
}
};
if !host_pct_ok(host) {
return false;
}
let host_ok = host.iter().all(|&b| {
b.is_ascii_alphanumeric()
|| matches!(
b,
b'-' | b'.'
| b'_'
| b'~'
| b'!'
| b'$'
| b'&'
| b'\''
| b'('
| b')'
| b'*'
| b'+'
| b','
| b';'
| b'='
| b'%'
)
});
let port_ok = port.is_none_or(|p| p.iter().all(u8::is_ascii_digit));
host_ok && port_ok
}
fn host_pct_ok(host: &[u8]) -> bool {
let mut i = 0;
while let Some(off) = host.get(i..).and_then(|rest| find_byte(b'%', rest)) {
let at = i + off;
match (host.get(at + 1), host.get(at + 2)) {
(Some(&h1), Some(&h2)) if has(h1, HEX) && has(h2, HEX) => i = at + 3,
_ => return false,
}
}
true
}
pub(crate) fn is_protocol(s: &[u8]) -> bool {
match find_byte(b'/', s) {
Some(i) => is_token(&s[..i]) && is_token(&s[i + 1..]),
None => is_token(s),
}
}
pub(crate) fn parse_decimal(s: &[u8]) -> Option<u64> {
if s.is_empty() {
return None;
}
let mut n: u64 = 0;
for &b in s {
if !b.is_ascii_digit() {
return None;
}
n = n.checked_mul(10)?.checked_add(u64::from(b - b'0'))?;
}
Some(n)
}
#[inline]
pub(crate) fn hex_value(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[inline]
pub(crate) fn is_qdtext(b: u8) -> bool {
has(b, QDTEXT)
}
pub(crate) fn list_elements(value: &[u8]) -> impl Iterator<Item = &[u8]> {
value
.split(|&b| b == b',')
.map(trim_ows)
.filter(|e| !e.is_empty())
}
#[inline]
pub(crate) fn find_byte(needle: u8, hay: &[u8]) -> Option<usize> {
let pattern = u64::from_le_bytes([needle; 8]);
let mut chunks = hay.chunks_exact(8);
let mut base = 0usize;
for chunk in &mut chunks {
let mut word = [0u8; 8];
word.copy_from_slice(chunk);
let x = u64::from_le_bytes(word) ^ pattern;
let found = x.wrapping_sub(0x0101_0101_0101_0101) & !x & 0x8080_8080_8080_8080;
if found != 0 {
return Some(base + (found.trailing_zeros() / 8) as usize);
}
base += 8;
}
chunks
.remainder()
.iter()
.position(|&b| b == needle)
.map(|i| base + i)
}
pub(crate) fn push_hex(out: &mut alloc::vec::Vec<u8>, n: u64) {
let mut digits = [0u8; 16];
let mut i = digits.len();
let mut v = n;
loop {
i -= 1;
digits[i] = b"0123456789abcdef"[(v & 0xf) as usize];
v >>= 4;
if v == 0 {
break;
}
}
out.extend_from_slice(&digits[i..]);
}
pub(crate) fn push_decimal(out: &mut alloc::vec::Vec<u8>, n: u64) {
let mut digits = [0u8; 20];
let mut i = digits.len();
let mut v = n;
loop {
i -= 1;
digits[i] = b'0' + (v % 10) as u8;
v /= 10;
if v == 0 {
break;
}
}
out.extend_from_slice(&digits[i..]);
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::*;
#[test]
fn test_token_classes_match_rfc() {
for b in 0u8..=255 {
let expected = b.is_ascii_alphanumeric() || b"!#$%&'*+-.^_`|~".contains(&b);
assert_eq!(is_tchar(b), expected, "byte {b:#x}");
}
assert!(!is_token(b""));
assert!(is_token(b"GET"));
assert!(!is_token(b"GE T"));
}
#[test]
fn test_field_value_rejects_controls_and_reports_cr() {
assert!(check_field_value(b"text/html; q=0.9\tx").is_ok());
assert!(check_field_value(b"caf\xc3\xa9 \x80\xff").is_ok());
assert_eq!(check_field_value(b"a\rb"), Err(HttpError::BareCr));
assert_eq!(
check_field_value(b"abcdefgh\x00ijk"),
Err(HttpError::InvalidHeaderValue)
);
assert_eq!(
check_field_value(b"abcdefghijklmno\x7f"),
Err(HttpError::InvalidHeaderValue)
);
assert_eq!(
check_field_value(b"abcdefg\nhij"),
Err(HttpError::InvalidHeaderValue)
);
}
#[test]
fn test_field_value_fast_path_agrees_with_bytewise() {
for b in 0u8..=255 {
for pos in 0..17 {
let mut v = alloc::vec![b'a'; 17];
v[pos] = b;
let fast = check_field_value(&v).is_ok();
let slow = check_field_bytes(&v).is_ok();
assert_eq!(fast, slow, "byte {b:#x} at {pos}");
}
}
}
#[test]
fn test_trim_ows_only_strips_space_and_tab() {
assert_eq!(trim_ows(b" \t a b \t"), b"a b");
assert_eq!(trim_ows(b"\x0ba\x0c"), b"\x0ba\x0c");
assert_eq!(trim_ows(b" "), b"");
}
#[test]
fn test_target_chars_check_percent_encoding() {
assert!(is_target_chars(b"/a/b?c=d&e=%20"));
assert!(!is_target_chars(b"/a%2"));
assert!(!is_target_chars(b"/a%zz"));
assert!(!is_target_chars(b"/a b"));
assert!(!is_target_chars(b"/a#frag"));
assert!(!is_target_chars(b"/a\x7f"));
}
#[test]
fn test_host_value_forms() {
assert!(is_host_value(b""));
assert!(is_host_value(b"example.com"));
assert!(is_host_value(b"127.0.0.1:8080"));
assert!(is_host_value(b"[::1]:80"));
assert!(is_host_value(b"[::1]"));
assert!(!is_host_value(b"a b"));
assert!(!is_host_value(b"host:8x"));
assert!(!is_host_value(b"[::1"));
assert!(!is_host_value(b"a/b"));
}
#[test]
fn test_host_value_empty_host_with_port_is_refused() {
assert!(!is_host_value(b":80"));
assert!(!is_host_value(b":"));
assert!(is_host_value(b"example.com:"));
assert!(is_host_value(b"example.com:99999"));
assert!(is_host_value(b"[::1]:80"));
assert!(is_host_value(b"[v1.fe80::a]"));
}
#[test]
fn test_host_value_percent_escapes_need_two_hex_digits() {
assert!(is_host_value(b"a%41"));
assert!(is_host_value(b"%41%42:8080"));
assert!(!is_host_value(b"a%zz"));
assert!(!is_host_value(b"a%4"));
assert!(!is_host_value(b"a%"));
assert!(!is_host_value(b"a%4:80"));
}
#[test]
fn test_protocol_names_and_versions() {
assert!(is_protocol(b"websocket"));
assert!(is_protocol(b"foo/2"));
assert!(!is_protocol(b"foo/"));
assert!(!is_protocol(b"/2"));
assert!(!is_protocol(b"a/b/c"));
assert!(!is_protocol(b"a b"));
assert!(!is_protocol(b""));
}
#[test]
fn test_parse_decimal_rejects_signs_and_overflow() {
assert_eq!(parse_decimal(b"0"), Some(0));
assert_eq!(parse_decimal(b"18446744073709551615"), Some(u64::MAX));
assert_eq!(parse_decimal(b"18446744073709551616"), None);
assert_eq!(parse_decimal(b"+1"), None);
assert_eq!(parse_decimal(b""), None);
assert_eq!(parse_decimal(b"1 "), None);
}
#[test]
fn test_find_byte_matches_position() {
let hay: Vec<u8> = (0..64u8).collect();
for needle in 0..70u8 {
assert_eq!(
find_byte(needle, &hay),
hay.iter().position(|&b| b == needle)
);
}
assert_eq!(find_byte(b'\n', b"\x0b\x0a"), Some(1));
assert_eq!(find_byte(1, &[0x81, 0x01]), Some(1));
}
#[test]
fn test_number_formatting() {
let mut out = Vec::new();
push_hex(&mut out, 0);
out.push(b' ');
push_hex(&mut out, 0xdead_beef);
out.push(b' ');
push_decimal(&mut out, 0);
out.push(b' ');
push_decimal(&mut out, u64::MAX);
assert_eq!(out, b"0 deadbeef 0 18446744073709551615");
}
}