const TCHAR: u32 = 1 << 0;
const VCHAR: u32 = 1 << 1;
const OBS_TEXT: u32 = 1 << 2;
const FIELD_TEXT: u32 = 1 << 3;
const QDTEXT: u32 = 1 << 4;
const CTEXT: u32 = 1 << 5;
const QUOTED_PAIR: u32 = 1 << 6;
const ETAGC: u32 = 1 << 7;
const ATTR_CHAR: u32 = 1 << 8;
const MIME_TOKEN: u32 = 1 << 9;
const UNRESERVED: u32 = 1 << 10;
const SUB_DELIM: u32 = 1 << 11;
const PCHAR_NO_PCT: u32 = 1 << 12;
const URI_TARGET: u32 = 1 << 13;
const HEXDIG: u32 = 1 << 14;
const SF_KEY_START: u32 = 1 << 15;
const SF_KEY: u32 = 1 << 16;
const SF_TOKEN: u32 = 1 << 17;
const SF_STRING: u32 = 1 << 18;
const TOKEN68: u32 = 1 << 19;
const BASE64: u32 = 1 << 20;
const ATEXT: u32 = 1 << 21;
const BCHAR_NOSPACE: u32 = 1 << 22;
const COOKIE_OCTET: u32 = 1 << 23;
const AV_OCTET: u32 = 1 << 24;
const LIST_PLAIN: u32 = 1 << 26;
const fn one_of(c: u8, set: &[u8]) -> bool {
let mut rest = set;
while let [first, tail @ ..] = rest {
if *first == c {
return true;
}
rest = tail;
}
false
}
const fn classify(c: u8) -> u32 {
let alpha = c.is_ascii_alphabetic();
let digit = c.is_ascii_digit();
let alnum = alpha || digit;
let vchar = c >= 0x21 && c <= 0x7e;
let obs = c >= 0x80;
let sp_ht = c == b' ' || c == b'\t';
let mut class = 0u32;
if alnum || one_of(c, b"!#$%&'*+-.^_`|~") {
class |= TCHAR;
}
if vchar {
class |= VCHAR;
}
if obs {
class |= OBS_TEXT;
}
if vchar || obs || sp_ht {
class |= FIELD_TEXT;
}
if sp_ht || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) || obs {
class |= QDTEXT;
}
if sp_ht
|| (c >= 0x21 && c <= 0x27)
|| (c >= 0x2a && c <= 0x5b)
|| (c >= 0x5d && c <= 0x7e)
|| obs
{
class |= CTEXT;
}
if sp_ht || vchar || obs {
class |= QUOTED_PAIR;
}
if c == 0x21 || (c >= 0x23 && c <= 0x7e) || obs {
class |= ETAGC;
}
if alnum || one_of(c, b"!#$&+-.^_`|~") {
class |= ATTR_CHAR;
}
if vchar && !one_of(c, b"()<>@,;:\\\"/[]?=") {
class |= MIME_TOKEN;
}
let unreserved = alnum || one_of(c, b"-._~");
let sub_delim = one_of(c, b"!$&'()*+,;=");
if unreserved {
class |= UNRESERVED;
}
if sub_delim {
class |= SUB_DELIM;
}
let pchar = unreserved || sub_delim || c == b':' || c == b'@';
if pchar {
class |= PCHAR_NO_PCT;
}
if pchar || c == b'/' || c == b'?' || c == b'%' {
class |= URI_TARGET;
}
if c.is_ascii_hexdigit() {
class |= HEXDIG;
}
let lcalpha = c.is_ascii_lowercase();
if lcalpha || c == b'*' {
class |= SF_KEY_START;
}
if lcalpha || digit || one_of(c, b"_-.*") {
class |= SF_KEY;
}
if alnum || one_of(c, b"!#$%&'*+-.^_`|~") || c == b':' || c == b'/' {
class |= SF_TOKEN;
}
if c == 0x20 || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) {
class |= SF_STRING;
}
if alnum || one_of(c, b"-._~+/") {
class |= TOKEN68;
}
if alnum || c == b'+' || c == b'/' {
class |= BASE64;
}
if alnum || one_of(c, b"!#$%&'*+-/=?^_`{|}~") {
class |= ATEXT;
}
if alnum || one_of(c, b"'()+_,-./:=?") {
class |= BCHAR_NOSPACE;
}
if c == 0x21
|| (c >= 0x23 && c <= 0x2b)
|| (c >= 0x2d && c <= 0x3a)
|| (c >= 0x3c && c <= 0x5b)
|| (c >= 0x5d && c <= 0x7e)
{
class |= COOKIE_OCTET;
}
if (c >= 0x20 && c <= 0x3a) || (c >= 0x3c && c <= 0x7e) {
class |= AV_OCTET;
}
if (vchar || obs || sp_ht) && !one_of(c, b",\"()") {
class |= LIST_PLAIN;
}
class
}
const fn build() -> [u32; 256] {
let mut table = [0u32; 256];
let mut b: u8 = 0;
loop {
#[allow(clippy::indexing_slicing)]
{
table[b as usize] = classify(b);
}
if b == u8::MAX {
break;
}
b += 1;
}
table
}
static CLASSES: [u32; 256] = build();
#[inline]
const fn has(b: u8, bit: u32) -> bool {
#[allow(clippy::indexing_slicing)]
let entry = CLASSES[b as usize];
entry & bit != 0
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Class(u32);
impl Class {
pub const TCHAR: Self = Self(TCHAR);
pub const VCHAR: Self = Self(VCHAR);
pub const OBS_TEXT: Self = Self(OBS_TEXT);
pub const FIELD_TEXT: Self = Self(FIELD_TEXT);
pub const QDTEXT: Self = Self(QDTEXT);
pub const CTEXT: Self = Self(CTEXT);
pub const QUOTED_PAIR: Self = Self(QUOTED_PAIR);
pub const ETAGC: Self = Self(ETAGC);
pub const ATTR_CHAR: Self = Self(ATTR_CHAR);
pub const MIME_TOKEN: Self = Self(MIME_TOKEN);
pub const UNRESERVED: Self = Self(UNRESERVED);
pub const SUB_DELIM: Self = Self(SUB_DELIM);
pub const PCHAR_NO_PCT: Self = Self(PCHAR_NO_PCT);
pub const URI_TARGET: Self = Self(URI_TARGET);
pub const HEXDIG: Self = Self(HEXDIG);
pub const SF_KEY_START: Self = Self(SF_KEY_START);
pub const SF_KEY: Self = Self(SF_KEY);
pub const SF_TOKEN: Self = Self(SF_TOKEN);
pub const SF_STRING: Self = Self(SF_STRING);
pub const TOKEN68: Self = Self(TOKEN68);
pub const BASE64: Self = Self(BASE64);
pub const ATEXT: Self = Self(ATEXT);
pub const BCHAR_NOSPACE: Self = Self(BCHAR_NOSPACE);
pub const COOKIE_OCTET: Self = Self(COOKIE_OCTET);
pub const AV_OCTET: Self = Self(AV_OCTET);
#[inline]
#[must_use]
pub const fn contains(self, b: u8) -> bool {
has(b, self.0)
}
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
}
#[inline]
#[must_use]
pub const fn all(s: &[u8], class: Class) -> bool {
let mut i = 0;
while i < s.len() {
#[allow(clippy::indexing_slicing)]
let b = s[i];
if !has(b, class.0) {
return false;
}
i += 1;
}
true
}
macro_rules! predicate {
($(#[$doc:meta])* $name:ident, $bit:ident, $example_yes:literal, $example_no:literal) => {
$(#[$doc])*
#[doc = concat!("assert!(parse_net::class::", stringify!($name), "(", stringify!($example_yes), "));")]
#[doc = concat!("assert!(!parse_net::class::", stringify!($name), "(", stringify!($example_no), "));")]
#[inline]
#[must_use]
pub const fn $name(b: u8) -> bool {
has(b, $bit)
}
};
}
predicate!(
is_tchar, TCHAR, b'~', b'('
);
predicate!(
is_vchar, VCHAR, b'!', b' '
);
predicate!(
is_obs_text, OBS_TEXT, 0x80, 0x7f
);
predicate!(
is_field_text, FIELD_TEXT, b'\t', b'\r'
);
predicate!(
is_qdtext, QDTEXT, b' ', b'"'
);
predicate!(
is_ctext, CTEXT, b'a', b'('
);
predicate!(
is_quoted_pair_char, QUOTED_PAIR, b'"', 0x7f
);
predicate!(
is_etagc, ETAGC, b'!', b'"'
);
predicate!(
is_attr_char, ATTR_CHAR, b'|', b'%'
);
predicate!(
is_mime_token_char, MIME_TOKEN, b'{', b'/'
);
predicate!(
is_unreserved, UNRESERVED, b'~', b'!'
);
predicate!(
is_sub_delim, SUB_DELIM, b'!', b'/'
);
predicate!(
is_pchar_no_pct, PCHAR_NO_PCT, b'@', b'/'
);
predicate!(
is_uri_target, URI_TARGET, b'?', b'#'
);
predicate!(
is_hexdig, HEXDIG, b'F', b'g'
);
predicate!(
is_sf_key_start, SF_KEY_START, b'*', b'A'
);
predicate!(
is_sf_key_char, SF_KEY, b'_', b'A'
);
predicate!(
is_sf_token_char, SF_TOKEN, b':', b'"'
);
predicate!(
is_sf_string_char, SF_STRING, b' ', b'\\'
);
predicate!(
is_token68_char, TOKEN68, b'~', b'='
);
predicate!(
is_base64_char, BASE64, b'+', b'-'
);
predicate!(
is_atext, ATEXT, b'{', b'@'
);
predicate!(
is_bchar_nospace, BCHAR_NOSPACE, b'?', b' '
);
predicate!(
is_cookie_octet, COOKIE_OCTET, b'!', b';'
);
predicate!(
is_av_octet, AV_OCTET, b' ', b';'
);
#[inline]
pub(crate) const fn is_list_plain(b: u8) -> bool {
has(b, LIST_PLAIN)
}
#[inline]
#[must_use]
pub const 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,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn r_alnum(b: u8) -> bool {
b.is_ascii_alphanumeric()
}
fn r_vchar(b: u8) -> bool {
(0x21..=0x7e).contains(&b)
}
fn r_obs(b: u8) -> bool {
b >= 0x80
}
fn r_tchar(b: u8) -> bool {
r_alnum(b) || b"!#$%&'*+-.^_`|~".contains(&b)
}
fn check(name: &str, fast: fn(u8) -> bool, slow: impl Fn(u8) -> bool) {
for b in 0u8..=255 {
assert_eq!(fast(b), slow(b), "{name}: byte {b:#04x}");
}
}
#[test]
fn test_tchar_table_exhaustive() {
check("tchar", is_tchar, r_tchar);
check("vchar", is_vchar, r_vchar);
check("obs-text", is_obs_text, r_obs);
check("field-text", is_field_text, |b| {
r_vchar(b) || r_obs(b) || b == b' ' || b == b'\t'
});
}
#[test]
fn test_quoting_classes_exhaustive() {
check("qdtext", is_qdtext, |b| {
b == b'\t'
|| b == b' '
|| b == 0x21
|| (0x23..=0x5b).contains(&b)
|| (0x5d..=0x7e).contains(&b)
|| r_obs(b)
});
check("ctext", is_ctext, |b| {
b == b'\t'
|| b == b' '
|| (0x21..=0x27).contains(&b)
|| (0x2a..=0x5b).contains(&b)
|| (0x5d..=0x7e).contains(&b)
|| r_obs(b)
});
check("quoted-pair", is_quoted_pair_char, |b| {
b == b'\t' || b == b' ' || r_vchar(b) || r_obs(b)
});
check("etagc", is_etagc, |b| {
b == 0x21 || (0x23..=0x7e).contains(&b) || r_obs(b)
});
}
#[test]
fn test_mime_and_attr_classes_exhaustive() {
check("attr-char", is_attr_char, |b| {
r_alnum(b) || b"!#$&+-.^_`|~".contains(&b)
});
check("mime token", is_mime_token_char, |b| {
(0x21..=0x7e).contains(&b) && !b"()<>@,;:\\\"/[]?=".contains(&b)
});
check("bcharsnospace", is_bchar_nospace, |b| {
r_alnum(b) || b"'()+_,-./:=?".contains(&b)
});
check("atext", is_atext, |b| {
r_alnum(b) || b"!#$%&'*+-/=?^_`{|}~".contains(&b)
});
}
#[test]
fn test_uri_classes_exhaustive() {
let unreserved = |b: u8| r_alnum(b) || b"-._~".contains(&b);
let sub = |b: u8| b"!$&'()*+,;=".contains(&b);
check("unreserved", is_unreserved, unreserved);
check("sub-delims", is_sub_delim, sub);
check("pchar", is_pchar_no_pct, |b| {
unreserved(b) || sub(b) || b == b':' || b == b'@'
});
check("target", is_uri_target, |b| {
unreserved(b) || sub(b) || b":@/?%".contains(&b)
});
check("hexdig", is_hexdig, |b| b.is_ascii_hexdigit());
}
#[test]
fn test_structured_field_classes_exhaustive() {
check("sf key start", is_sf_key_start, |b| {
b.is_ascii_lowercase() || b == b'*'
});
check("sf key", is_sf_key_char, |b| {
b.is_ascii_lowercase() || b.is_ascii_digit() || b"_-.*".contains(&b)
});
check("sf token", is_sf_token_char, |b| {
r_tchar(b) || b == b':' || b == b'/'
});
check("sf string", is_sf_string_char, |b| {
b == 0x20 || b == 0x21 || (0x23..=0x5b).contains(&b) || (0x5d..=0x7e).contains(&b)
});
}
#[test]
fn test_base64_and_cookie_classes_exhaustive() {
check("token68", is_token68_char, |b| {
r_alnum(b) || b"-._~+/".contains(&b)
});
check("base64", is_base64_char, |b| {
r_alnum(b) || b == b'+' || b == b'/'
});
check("cookie-octet", is_cookie_octet, |b| {
b == 0x21
|| (0x23..=0x2b).contains(&b)
|| (0x2d..=0x3a).contains(&b)
|| (0x3c..=0x5b).contains(&b)
|| (0x5d..=0x7e).contains(&b)
});
check("av-octet", is_av_octet, |b| {
(0x20..=0x3a).contains(&b) || (0x3c..=0x7e).contains(&b)
});
}
#[test]
fn test_list_plain_is_field_text_without_delimiters() {
for b in 0u8..=255 {
let expected = is_field_text(b) && !b",\"()".contains(&b);
assert_eq!(is_list_plain(b), expected, "byte {b:#04x}");
}
}
#[test]
fn test_del_is_in_no_class() {
assert_eq!(CLASSES[0x7f], 0);
}
#[test]
fn test_unicode_bytes_never_in_ascii_classes() {
let ascii_only = TCHAR
| VCHAR
| ATTR_CHAR
| MIME_TOKEN
| UNRESERVED
| SUB_DELIM
| PCHAR_NO_PCT
| URI_TARGET
| HEXDIG
| SF_KEY_START
| SF_KEY
| SF_TOKEN
| SF_STRING
| TOKEN68
| BASE64
| ATEXT
| BCHAR_NOSPACE
| COOKIE_OCTET
| AV_OCTET;
for b in 0x80u8..=0xff {
assert_eq!(CLASSES[b as usize] & ascii_only, 0, "byte {b:#04x}");
}
}
#[test]
fn test_hex_value_all_bytes() {
for b in 0u8..=255 {
let expected = (b as char).to_digit(16).map(|d| d as u8);
assert_eq!(hex_value(b), expected, "byte {b:#04x}");
}
}
#[test]
fn test_class_all_and_union() {
assert!(all(b"", Class::TCHAR));
assert!(all(b"abc", Class::TCHAR));
assert!(!all(b"a c", Class::TCHAR));
let c = Class::TCHAR.union(Class::SF_KEY);
assert!(c.contains(b'a') && c.contains(b'*') && !c.contains(b' '));
for b in 0u8..=255 {
assert_eq!(Class::QDTEXT.contains(b), is_qdtext(b));
}
}
}