use crate::ascii::HttpChar;
pub trait HttpValidate {
fn is_valid_token(&self) -> bool;
fn is_valid_header_value(&self) -> bool;
fn is_valid_request_target(&self) -> bool;
fn trim_ows(&self) -> &[u8];
}
macro_rules! impl_bitmask_validate {
() => {
#[inline]
pub(super) fn is_valid_header_value(buf: &[u8]) -> bool {
let len = buf.len();
unsafe {
let nul = simd_splat!(HttpChar::Null.as_u8());
let cr = simd_splat!(HttpChar::CarriageReturn.as_u8());
let lf = simd_splat!(HttpChar::LineFeed.as_u8());
let mut i = 0;
while i + 16 <= len {
let chunk = simd_load!(buf.as_ptr().add(i));
let bad = simd_or!(
simd_cmpeq!(chunk, nul),
simd_or!(simd_cmpeq!(chunk, cr), simd_cmpeq!(chunk, lf))
);
if simd_bitmask!(bad) != 0 {
return false;
}
i += 16;
}
while i < len {
let b = *buf.get_unchecked(i);
if b == HttpChar::Null
|| b == HttpChar::CarriageReturn
|| b == HttpChar::LineFeed
{
return false;
}
i += 1;
}
true
}
}
#[inline]
pub(super) fn is_valid_request_target(buf: &[u8]) -> bool {
let len = buf.len();
unsafe {
let min_valid = simd_splat!(HttpChar::Space.as_u8());
let del = simd_splat!(HttpChar::Delete.as_u8());
let mut i = 0;
while i + 16 <= len {
let chunk = simd_load!(buf.as_ptr().add(i));
let not_ctrl = simd_cmpeq!(simd_max_epu8!(chunk, min_valid), chunk);
let is_del = simd_cmpeq!(chunk, del);
let good = simd_andnot!(is_del, not_ctrl);
if simd_bitmask!(good) != 0xFFFF {
return false;
}
i += 16;
}
while i < len {
let b = *buf.get_unchecked(i);
if b <= 0x1F || b == HttpChar::Delete {
return false;
}
i += 1;
}
true
}
}
};
}
#[cfg(target_arch = "x86_64")]
#[allow(
clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_ptr_alignment
)]
mod sse2 {
crate::simd::define_simd_primitives!();
use crate::ascii::HttpChar;
impl_bitmask_validate!();
}
#[cfg(target_arch = "aarch64")]
mod neon {
use std::arch::aarch64::{
vbicq_u8, vceqq_u8, vcgeq_u8, vdupq_n_u8, vld1q_u8, vmaxvq_u8, vminvq_u8, vorrq_u8,
};
use crate::ascii::HttpChar;
#[inline]
pub(super) fn is_valid_header_value(buf: &[u8]) -> bool {
let len = buf.len();
unsafe {
let nul = vdupq_n_u8(HttpChar::Null.as_u8());
let cr = vdupq_n_u8(HttpChar::CarriageReturn.as_u8());
let lf = vdupq_n_u8(HttpChar::LineFeed.as_u8());
let mut i = 0;
while i + 16 <= len {
let chunk = vld1q_u8(buf.as_ptr().add(i));
let bad = vorrq_u8(
vceqq_u8(chunk, nul),
vorrq_u8(vceqq_u8(chunk, cr), vceqq_u8(chunk, lf)),
);
if vmaxvq_u8(bad) != 0 {
return false;
}
i += 16;
}
while i < len {
let b = *buf.get_unchecked(i);
if b == HttpChar::Null || b == HttpChar::CarriageReturn || b == HttpChar::LineFeed {
return false;
}
i += 1;
}
true
}
}
#[inline]
pub(super) fn is_valid_request_target(buf: &[u8]) -> bool {
let len = buf.len();
unsafe {
let min_valid = vdupq_n_u8(HttpChar::Space.as_u8());
let del = vdupq_n_u8(HttpChar::Delete.as_u8());
let mut i = 0;
while i + 16 <= len {
let chunk = vld1q_u8(buf.as_ptr().add(i));
let not_ctrl = vcgeq_u8(chunk, min_valid);
let is_del = vceqq_u8(chunk, del);
let good = vbicq_u8(not_ctrl, is_del);
if vminvq_u8(good) == 0 {
return false;
}
i += 16;
}
while i < len {
let b = *buf.get_unchecked(i);
if b <= 0x1F || b == HttpChar::Delete {
return false;
}
i += 1;
}
true
}
}
}
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
#[allow(clippy::cast_sign_loss)]
mod wasm_simd {
crate::simd::define_simd_primitives!();
use crate::ascii::HttpChar;
impl_bitmask_validate!();
}
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "aarch64",
all(target_arch = "wasm32", target_feature = "simd128")
)))]
mod scalar {
use crate::ascii::HttpChar;
#[inline]
pub(super) fn is_valid_header_value(buf: &[u8]) -> bool {
buf.iter().all(|&b| {
b != HttpChar::Null && b != HttpChar::CarriageReturn && b != HttpChar::LineFeed
})
}
#[inline]
pub(super) fn is_valid_request_target(buf: &[u8]) -> bool {
buf.iter().all(|&b| b > 0x1F && b != HttpChar::Delete)
}
}
macro_rules! dispatch {
($fn_name:ident $(, $arg:expr)* $(,)?) => {{
#[cfg(target_arch = "x86_64")]
{ sse2::$fn_name($($arg),*) }
#[cfg(target_arch = "aarch64")]
{ neon::$fn_name($($arg),*) }
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
{ wasm_simd::$fn_name($($arg),*) }
#[cfg(not(any(
target_arch = "x86_64",
target_arch = "aarch64",
all(target_arch = "wasm32", target_feature = "simd128")
)))]
{ scalar::$fn_name($($arg),*) }
}};
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "ssse3")]
#[inline]
unsafe fn is_valid_token_ssse3(buf: &[u8]) -> bool {
use crate::tchar::{Ssse3, TcharCheck};
<Ssse3 as TcharCheck>::is_valid_token(buf)
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn tchar_all16_inline(ptr: *const u8) -> bool {
use std::arch::x86_64::{
_mm_and_si128, _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_set1_epi8,
_mm_setzero_si128, _mm_shuffle_epi8, _mm_srli_epi16,
};
unsafe {
let lo_tbl_arr: [u8; 16] = crate::tchar::LO_NIBBLES;
let hi_tbl_arr: [u8; 16] = crate::tchar::HI_NIBBLES;
let chunk = _mm_loadu_si128(ptr.cast());
let lo_tbl = _mm_loadu_si128(lo_tbl_arr.as_ptr().cast());
let hi_tbl = _mm_loadu_si128(hi_tbl_arr.as_ptr().cast());
let nibble_mask = _mm_set1_epi8(0x0F);
let lo_nib = _mm_and_si128(chunk, nibble_mask);
let hi_nib = _mm_and_si128(_mm_srli_epi16(chunk, 4), nibble_mask);
let lo_shuf = _mm_shuffle_epi8(lo_tbl, lo_nib);
let hi_shuf = _mm_shuffle_epi8(hi_tbl, hi_nib);
let valid = _mm_and_si128(lo_shuf, hi_shuf);
let invalid = _mm_cmpeq_epi8(valid, _mm_setzero_si128());
_mm_movemask_epi8(invalid) == 0
}
}
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
#[inline]
unsafe fn is_valid_token_avx2(buf: &[u8]) -> bool {
use crate::tchar::{Avx2, TABLE};
let len = buf.len();
if len == 0 {
return false;
}
let ptr = buf.as_ptr();
let mut i = 0;
while i + 32 <= len {
if unsafe { Avx2::mask32(ptr.add(i)) } != 0xFFFF_FFFF {
return false;
}
i += 32;
}
if i + 16 <= len {
if unsafe { !tchar_all16_inline(ptr.add(i)) } {
return false;
}
i += 16;
}
while i < len {
if !TABLE[buf[i] as usize] {
return false;
}
i += 1;
}
true
}
impl HttpValidate for [u8] {
#[inline]
fn is_valid_token(&self) -> bool {
#[cfg(target_arch = "x86_64")]
{
use crate::tchar::{Sse2Only, TcharCheck, has_avx2, has_ssse3};
if has_avx2() {
unsafe { is_valid_token_avx2(self) }
} else if has_ssse3() {
unsafe { is_valid_token_ssse3(self) }
} else {
<Sse2Only as TcharCheck>::is_valid_token(self)
}
}
#[cfg(not(target_arch = "x86_64"))]
{
use crate::tchar::{Arch, TcharCheck};
Arch::is_valid_token(self)
}
}
#[inline]
fn is_valid_header_value(&self) -> bool {
dispatch!(is_valid_header_value, self)
}
#[inline]
fn is_valid_request_target(&self) -> bool {
dispatch!(is_valid_request_target, self)
}
#[inline]
fn trim_ows(&self) -> &[u8] {
let len = self.len();
if len == 0 {
return self;
}
let first = self[0];
let last = self[len - 1];
let first_ok = first != HttpChar::Space && first != HttpChar::HorizontalTab;
let last_ok = last != HttpChar::Space && last != HttpChar::HorizontalTab;
if first_ok && last_ok {
return self;
}
let mut start = 0;
while start < len {
if self[start] != HttpChar::Space && self[start] != HttpChar::HorizontalTab {
break;
}
start += 1;
}
let mut end = len;
while end > start {
if self[end - 1] != HttpChar::Space && self[end - 1] != HttpChar::HorizontalTab {
break;
}
end -= 1;
}
&self[start..end]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_token_alpha() {
assert!(b"Host".is_valid_token());
assert!(b"Content-Type".is_valid_token());
assert!(b"X-My_Header.v2".is_valid_token());
}
#[test]
fn valid_token_special_tchars() {
assert!(b"!#$%&'*+-.^_`|~".is_valid_token());
}
#[test]
fn invalid_token_empty() {
assert!(!b"".is_valid_token());
}
#[test]
fn invalid_token_space() {
assert!(!b"Bad Name".is_valid_token());
}
#[test]
fn invalid_token_control_char() {
assert!(!b"Bad\x01Name".is_valid_token());
}
#[test]
fn invalid_token_colon() {
assert!(!b"Name:Value".is_valid_token());
}
#[test]
fn invalid_token_crlf() {
assert!(!b"Bad\r\nName".is_valid_token());
}
#[test]
fn valid_value_printable() {
assert!(b"text/html; charset=utf-8".is_valid_header_value());
}
#[test]
fn valid_value_empty() {
assert!(b"".is_valid_header_value());
}
#[test]
fn valid_value_high_bytes() {
assert!([0x80u8, 0xFF, 0xFE].as_slice().is_valid_header_value());
}
#[test]
fn invalid_value_nul() {
assert!(!b"bad\x00value".is_valid_header_value());
}
#[test]
fn invalid_value_cr() {
assert!(!b"bad\rvalue".is_valid_header_value());
}
#[test]
fn invalid_value_lf() {
assert!(!b"bad\nvalue".is_valid_header_value());
}
#[test]
fn invalid_value_crlf_injection() {
assert!(!b"text/html\r\nX-Injected: evil".is_valid_header_value());
}
#[test]
fn value_validation_simd_boundary() {
for pos in 0..16 {
let mut buf = [b'a'; 16];
buf[pos] = 0;
assert!(!buf.as_slice().is_valid_header_value());
}
let mut buf = [b'a'; 17];
buf[16] = b'\r';
assert!(!buf.as_slice().is_valid_header_value());
}
#[test]
fn valid_target_path() {
assert!(b"/index.html".is_valid_request_target());
assert!(b"/search?q=hello&lang=en".is_valid_request_target());
assert!(b"/page#section".is_valid_request_target());
assert!(b"/hello%20world".is_valid_request_target());
}
#[test]
fn valid_target_high_bytes() {
assert!([b'/', 0xFF, 0xFE].as_slice().is_valid_request_target());
}
#[test]
fn invalid_target_nul() {
assert!(!b"/file.txt\x00.jpg".is_valid_request_target());
}
#[test]
fn invalid_target_control_chars() {
for c in 0x00..=0x1F {
let buf = [b'/', c];
assert!(!buf.as_slice().is_valid_request_target(), "byte {c:#04x}");
}
assert!(!b"/path\x7F".is_valid_request_target());
}
#[test]
fn trim_ows_leading() {
assert_eq!(b" value".trim_ows(), b"value");
}
#[test]
fn trim_ows_trailing() {
assert_eq!(b"value ".trim_ows(), b"value");
}
#[test]
fn trim_ows_both() {
assert_eq!(b" \t value \t ".trim_ows(), b"value");
}
#[test]
fn trim_ows_empty() {
assert_eq!(b"".trim_ows(), b"");
}
#[test]
fn trim_ows_only_whitespace() {
assert_eq!(b" ".trim_ows(), b"");
}
#[test]
fn trim_ows_no_whitespace() {
assert_eq!(b"value".trim_ows(), b"value");
}
#[test]
fn trim_ows_tabs() {
assert_eq!(b"\t\tvalue\t".trim_ows(), b"value");
}
}