Skip to main content

parse_net/
class.rs

1//! The byte classes of the ABNF, in one table.
2//!
3//! One `static` table of 256 `u32` entries (1 KiB), built at compile time from
4//! the ABNF, holds one bit per class, so classifying a byte is one load and
5//! one mask. The table is the single reviewable source of every class; the
6//! tests compare it, for all 256 bytes, with slow functions written straight
7//! from the RFC text.
8//!
9//! Every class is ASCII-only except `OBS_TEXT`, `FIELD_TEXT`, `QDTEXT`,
10//! `CTEXT`, `QUOTED_PAIR`, and `ETAGC`, which include 0x80 to 0xFF as
11//! `obs-text`. DEL (0x7F) is in no class.
12//!
13//! # Examples
14//!
15//! ```
16//! use parse_net::class::{self, Class};
17//!
18//! assert!(class::is_tchar(b'!') && !class::is_tchar(b'('));
19//! assert!(class::is_mime_token_char(b'{') && !class::is_tchar(b'{'));
20//! assert_eq!(class::hex_value(b'f'), Some(15));
21//! assert!(class::all(b"no-cache", Class::TCHAR));
22//! assert!(Class::UNRESERVED.union(Class::SUB_DELIM).contains(b'~'));
23//! ```
24
25const TCHAR: u32 = 1 << 0;
26const VCHAR: u32 = 1 << 1;
27const OBS_TEXT: u32 = 1 << 2;
28const FIELD_TEXT: u32 = 1 << 3;
29const QDTEXT: u32 = 1 << 4;
30const CTEXT: u32 = 1 << 5;
31const QUOTED_PAIR: u32 = 1 << 6;
32const ETAGC: u32 = 1 << 7;
33const ATTR_CHAR: u32 = 1 << 8;
34const MIME_TOKEN: u32 = 1 << 9;
35const UNRESERVED: u32 = 1 << 10;
36const SUB_DELIM: u32 = 1 << 11;
37const PCHAR_NO_PCT: u32 = 1 << 12;
38const URI_TARGET: u32 = 1 << 13;
39const HEXDIG: u32 = 1 << 14;
40const SF_KEY_START: u32 = 1 << 15;
41const SF_KEY: u32 = 1 << 16;
42const SF_TOKEN: u32 = 1 << 17;
43const SF_STRING: u32 = 1 << 18;
44const TOKEN68: u32 = 1 << 19;
45const BASE64: u32 = 1 << 20;
46const ATEXT: u32 = 1 << 21;
47const BCHAR_NOSPACE: u32 = 1 << 22;
48const COOKIE_OCTET: u32 = 1 << 23;
49const AV_OCTET: u32 = 1 << 24;
50/// Internal: field text that is not a list delimiter (`,` `"` `(` `)`), so
51/// the list walker can skip it with one lookup per byte.
52const LIST_PLAIN: u32 = 1 << 26;
53
54/// Whether `c` is one of the bytes in `set`.
55const fn one_of(c: u8, set: &[u8]) -> bool {
56    let mut rest = set;
57    while let [first, tail @ ..] = rest {
58        if *first == c {
59            return true;
60        }
61        rest = tail;
62    }
63    false
64}
65
66const fn classify(c: u8) -> u32 {
67    let alpha = c.is_ascii_alphabetic();
68    let digit = c.is_ascii_digit();
69    let alnum = alpha || digit;
70    let vchar = c >= 0x21 && c <= 0x7e;
71    let obs = c >= 0x80;
72    let sp_ht = c == b' ' || c == b'\t';
73    let mut class = 0u32;
74
75    // RFC 9110 section 5.6.2.
76    if alnum || one_of(c, b"!#$%&'*+-.^_`|~") {
77        class |= TCHAR;
78    }
79    if vchar {
80        class |= VCHAR;
81    }
82    if obs {
83        class |= OBS_TEXT;
84    }
85    // RFC 9110 section 5.5: field-vchar / SP / HTAB.
86    if vchar || obs || sp_ht {
87        class |= FIELD_TEXT;
88    }
89    // RFC 9110 section 5.6.4: HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text.
90    if sp_ht || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) || obs {
91        class |= QDTEXT;
92    }
93    // RFC 9110 section 5.6.5: HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text.
94    if sp_ht
95        || (c >= 0x21 && c <= 0x27)
96        || (c >= 0x2a && c <= 0x5b)
97        || (c >= 0x5d && c <= 0x7e)
98        || obs
99    {
100        class |= CTEXT;
101    }
102    // The byte after "\": HTAB / SP / VCHAR / obs-text.
103    if sp_ht || vchar || obs {
104        class |= QUOTED_PAIR;
105    }
106    // RFC 9110 section 8.8.3: "!" / %x23-7E / obs-text.
107    if c == 0x21 || (c >= 0x23 && c <= 0x7e) || obs {
108        class |= ETAGC;
109    }
110    // RFC 8187 section 3.2.1.
111    if alnum || one_of(c, b"!#$&+-.^_`|~") {
112        class |= ATTR_CHAR;
113    }
114    // RFC 2045 section 5.1: any CHAR except SP, CTLs, and tspecials.
115    if vchar && !one_of(c, b"()<>@,;:\\\"/[]?=") {
116        class |= MIME_TOKEN;
117    }
118    // RFC 3986 sections 2.2 and 2.3.
119    let unreserved = alnum || one_of(c, b"-._~");
120    let sub_delim = one_of(c, b"!$&'()*+,;=");
121    if unreserved {
122        class |= UNRESERVED;
123    }
124    if sub_delim {
125        class |= SUB_DELIM;
126    }
127    let pchar = unreserved || sub_delim || c == b':' || c == b'@';
128    if pchar {
129        class |= PCHAR_NO_PCT;
130    }
131    // pchar / "/" / "?", plus the "%" that introduces a pct-encoded triple.
132    if pchar || c == b'/' || c == b'?' || c == b'%' {
133        class |= URI_TARGET;
134    }
135    if c.is_ascii_hexdigit() {
136        class |= HEXDIG;
137    }
138    // RFC 9651 section 3.1.2: key = ( lcalpha / "*" ) *( lcalpha / DIGIT / "_" / "-" / "." / "*" ).
139    let lcalpha = c.is_ascii_lowercase();
140    if lcalpha || c == b'*' {
141        class |= SF_KEY_START;
142    }
143    if lcalpha || digit || one_of(c, b"_-.*") {
144        class |= SF_KEY;
145    }
146    // RFC 9651 section 3.3.4: the characters after the first in a token.
147    if alnum || one_of(c, b"!#$%&'*+-.^_`|~") || c == b':' || c == b'/' {
148        class |= SF_TOKEN;
149    }
150    // RFC 9651 section 3.3.3: unescaped = %x20-21 / %x23-5B / %x5D-7E.
151    if c == 0x20 || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) {
152        class |= SF_STRING;
153    }
154    // RFC 9110 section 11.2 (the trailing "=" is handled apart).
155    if alnum || one_of(c, b"-._~+/") {
156        class |= TOKEN68;
157    }
158    // RFC 4648 section 4 (the "=" padding is handled apart).
159    if alnum || c == b'+' || c == b'/' {
160        class |= BASE64;
161    }
162    // RFC 5322 section 3.2.3.
163    if alnum || one_of(c, b"!#$%&'*+-/=?^_`{|}~") {
164        class |= ATEXT;
165    }
166    // RFC 2046 section 5.1.1: bcharsnospace.
167    if alnum || one_of(c, b"'()+_,-./:=?") {
168        class |= BCHAR_NOSPACE;
169    }
170    // draft-ietf-httpbis-rfc6265bis section 4.1.1.
171    if c == 0x21
172        || (c >= 0x23 && c <= 0x2b)
173        || (c >= 0x2d && c <= 0x3a)
174        || (c >= 0x3c && c <= 0x5b)
175        || (c >= 0x5d && c <= 0x7e)
176    {
177        class |= COOKIE_OCTET;
178    }
179    if (c >= 0x20 && c <= 0x3a) || (c >= 0x3c && c <= 0x7e) {
180        class |= AV_OCTET;
181    }
182    if (vchar || obs || sp_ht) && !one_of(c, b",\"()") {
183        class |= LIST_PLAIN;
184    }
185    class
186}
187
188const fn build() -> [u32; 256] {
189    let mut table = [0u32; 256];
190    let mut b: u8 = 0;
191    loop {
192        // `b` is a u8, so the index is always below 256.
193        #[allow(clippy::indexing_slicing)]
194        {
195            table[b as usize] = classify(b);
196        }
197        if b == u8::MAX {
198            break;
199        }
200        b += 1;
201    }
202    table
203}
204
205static CLASSES: [u32; 256] = build();
206
207#[inline]
208const fn has(b: u8, bit: u32) -> bool {
209    // A u8 index into a 256-entry table cannot be out of bounds.
210    #[allow(clippy::indexing_slicing)]
211    let entry = CLASSES[b as usize];
212    entry & bit != 0
213}
214
215/// A set of byte classes, for [`all`] and the scanning functions.
216///
217/// The named constants are the classes of [`dev/PLAN.md` section 7.1](https://github.com/jamesgober/parse-net/blob/main/dev/PLAN.md#71-the-byte-class-table).
218///
219/// # Examples
220///
221/// ```
222/// use parse_net::class::Class;
223///
224/// let host_chars = Class::UNRESERVED.union(Class::SUB_DELIM);
225/// assert!(host_chars.contains(b'!') && !host_chars.contains(b'/'));
226/// ```
227#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
228pub struct Class(u32);
229
230impl Class {
231    /// `tchar` (RFC 9110 section 5.6.2).
232    pub const TCHAR: Self = Self(TCHAR);
233    /// `VCHAR`: 0x21 to 0x7E (RFC 5234).
234    pub const VCHAR: Self = Self(VCHAR);
235    /// `obs-text`: 0x80 to 0xFF (RFC 9110 section 5.5).
236    pub const OBS_TEXT: Self = Self(OBS_TEXT);
237    /// `field-vchar / SP / HTAB` (RFC 9110 section 5.5).
238    pub const FIELD_TEXT: Self = Self(FIELD_TEXT);
239    /// `qdtext` (RFC 9110 section 5.6.4).
240    pub const QDTEXT: Self = Self(QDTEXT);
241    /// `ctext` (RFC 9110 section 5.6.5).
242    pub const CTEXT: Self = Self(CTEXT);
243    /// The byte after `\` in a `quoted-pair`: `HTAB / SP / VCHAR / obs-text`.
244    pub const QUOTED_PAIR: Self = Self(QUOTED_PAIR);
245    /// `etagc` (RFC 9110 section 8.8.3).
246    pub const ETAGC: Self = Self(ETAGC);
247    /// `attr-char` (RFC 8187 section 3.2.1).
248    pub const ATTR_CHAR: Self = Self(ATTR_CHAR);
249    /// The MIME `token` characters (RFC 2045 section 5.1); unlike `tchar`,
250    /// they include `{` and `}`.
251    pub const MIME_TOKEN: Self = Self(MIME_TOKEN);
252    /// `unreserved` (RFC 3986 section 2.3).
253    pub const UNRESERVED: Self = Self(UNRESERVED);
254    /// `sub-delims` (RFC 3986 section 2.2).
255    pub const SUB_DELIM: Self = Self(SUB_DELIM);
256    /// `unreserved / sub-delims / ":" / "@"`: `pchar` without `pct-encoded`.
257    pub const PCHAR_NO_PCT: Self = Self(PCHAR_NO_PCT);
258    /// `pchar / "/" / "?"` plus `%`: the characters of a request target.
259    pub const URI_TARGET: Self = Self(URI_TARGET);
260    /// `HEXDIG`, either case.
261    pub const HEXDIG: Self = Self(HEXDIG);
262    /// The first byte of a Structured Field key: `lcalpha / "*"`.
263    pub const SF_KEY_START: Self = Self(SF_KEY_START);
264    /// A Structured Field key byte: `lcalpha / DIGIT / "_" / "-" / "." / "*"`.
265    pub const SF_KEY: Self = Self(SF_KEY);
266    /// A Structured Field token byte after the first: `tchar / ":" / "/"`.
267    pub const SF_TOKEN: Self = Self(SF_TOKEN);
268    /// An unescaped Structured Field string byte: `%x20-21 / %x23-5B / %x5D-7E`.
269    pub const SF_STRING: Self = Self(SF_STRING);
270    /// A `token68` byte (RFC 9110 section 11.2), without the `=` padding.
271    pub const TOKEN68: Self = Self(TOKEN68);
272    /// The base64 alphabet (RFC 4648 section 4), without the `=` padding.
273    pub const BASE64: Self = Self(BASE64);
274    /// `atext` (RFC 5322 section 3.2.3).
275    pub const ATEXT: Self = Self(ATEXT);
276    /// `bcharsnospace` (RFC 2046 section 5.1.1).
277    pub const BCHAR_NOSPACE: Self = Self(BCHAR_NOSPACE);
278    /// `cookie-octet` (rfc6265bis section 4.1.1), for cookie-net.
279    pub const COOKIE_OCTET: Self = Self(COOKIE_OCTET);
280    /// Cookie attribute value bytes (rfc6265bis section 4.1.1), for cookie-net.
281    pub const AV_OCTET: Self = Self(AV_OCTET);
282
283    /// Whether `b` is in this class.
284    #[inline]
285    #[must_use]
286    pub const fn contains(self, b: u8) -> bool {
287        has(b, self.0)
288    }
289
290    /// A class holding the bytes of both.
291    #[must_use]
292    pub const fn union(self, other: Self) -> Self {
293        Self(self.0 | other.0)
294    }
295}
296
297/// Whether every byte of `s` is in `class` (true for an empty `s`). Stops at
298/// the first byte outside.
299///
300/// # Examples
301///
302/// ```
303/// use parse_net::class::{all, Class};
304///
305/// assert!(all(b"gzip", Class::TCHAR));
306/// assert!(!all(b"gzip, br", Class::TCHAR));
307/// assert!(all(b"", Class::TCHAR));
308/// ```
309#[inline]
310#[must_use]
311pub const fn all(s: &[u8], class: Class) -> bool {
312    // An indexed loop: the compiler drops the bounds check for `i < len`,
313    // which the slice-pattern form does not get as reliably.
314    let mut i = 0;
315    while i < s.len() {
316        // `i < s.len()` was just checked.
317        #[allow(clippy::indexing_slicing)]
318        let b = s[i];
319        if !has(b, class.0) {
320            return false;
321        }
322        i += 1;
323    }
324    true
325}
326
327macro_rules! predicate {
328    ($(#[$doc:meta])* $name:ident, $bit:ident, $example_yes:literal, $example_no:literal) => {
329        $(#[$doc])*
330        ///
331        /// # Examples
332        ///
333        /// ```
334        #[doc = concat!("assert!(parse_net::class::", stringify!($name), "(", stringify!($example_yes), "));")]
335        #[doc = concat!("assert!(!parse_net::class::", stringify!($name), "(", stringify!($example_no), "));")]
336        /// ```
337        #[inline]
338        #[must_use]
339        pub const fn $name(b: u8) -> bool {
340            has(b, $bit)
341        }
342    };
343}
344
345predicate!(
346    /// Whether `b` is a `tchar`, a byte allowed in a token (RFC 9110 section 5.6.2).
347    is_tchar, TCHAR, b'~', b'('
348);
349predicate!(
350    /// Whether `b` is a visible ASCII character, 0x21 to 0x7E.
351    is_vchar, VCHAR, b'!', b' '
352);
353predicate!(
354    /// Whether `b` is `obs-text`, 0x80 to 0xFF.
355    is_obs_text, OBS_TEXT, 0x80, 0x7f
356);
357predicate!(
358    /// Whether `b` may appear in a field value: `field-vchar / SP / HTAB`
359    /// (RFC 9110 section 5.5).
360    is_field_text, FIELD_TEXT, b'\t', b'\r'
361);
362predicate!(
363    /// Whether `b` may appear unescaped in a quoted string (RFC 9110 section 5.6.4).
364    is_qdtext, QDTEXT, b' ', b'"'
365);
366predicate!(
367    /// Whether `b` may appear unescaped in a comment (RFC 9110 section 5.6.5).
368    is_ctext, CTEXT, b'a', b'('
369);
370predicate!(
371    /// Whether `b` may follow `\` in a `quoted-pair`: `HTAB / SP / VCHAR / obs-text`.
372    is_quoted_pair_char, QUOTED_PAIR, b'"', 0x7f
373);
374predicate!(
375    /// Whether `b` is an `etagc` (RFC 9110 section 8.8.3).
376    is_etagc, ETAGC, b'!', b'"'
377);
378predicate!(
379    /// Whether `b` is an `attr-char` (RFC 8187 section 3.2.1).
380    is_attr_char, ATTR_CHAR, b'|', b'%'
381);
382predicate!(
383    /// Whether `b` may appear in a MIME `token` (RFC 2045 section 5.1).
384    is_mime_token_char, MIME_TOKEN, b'{', b'/'
385);
386predicate!(
387    /// Whether `b` is `unreserved` (RFC 3986 section 2.3).
388    is_unreserved, UNRESERVED, b'~', b'!'
389);
390predicate!(
391    /// Whether `b` is a `sub-delims` byte (RFC 3986 section 2.2).
392    is_sub_delim, SUB_DELIM, b'!', b'/'
393);
394predicate!(
395    /// Whether `b` is a `pchar` other than the `%` of `pct-encoded`
396    /// (RFC 3986 section 3.3).
397    is_pchar_no_pct, PCHAR_NO_PCT, b'@', b'/'
398);
399predicate!(
400    /// Whether `b` may appear in a request target: `pchar / "/" / "?"` or the
401    /// `%` of a `pct-encoded` triple (whose digits are checked apart).
402    is_uri_target, URI_TARGET, b'?', b'#'
403);
404predicate!(
405    /// Whether `b` is a hex digit, either case.
406    is_hexdig, HEXDIG, b'F', b'g'
407);
408predicate!(
409    /// Whether `b` may start a Structured Field key: `lcalpha / "*"`.
410    is_sf_key_start, SF_KEY_START, b'*', b'A'
411);
412predicate!(
413    /// Whether `b` may appear in a Structured Field key.
414    is_sf_key_char, SF_KEY, b'_', b'A'
415);
416predicate!(
417    /// Whether `b` may appear in a Structured Field token after its first byte.
418    is_sf_token_char, SF_TOKEN, b':', b'"'
419);
420predicate!(
421    /// Whether `b` may appear unescaped in a Structured Field string.
422    is_sf_string_char, SF_STRING, b' ', b'\\'
423);
424predicate!(
425    /// Whether `b` is a `token68` byte other than the `=` padding.
426    is_token68_char, TOKEN68, b'~', b'='
427);
428predicate!(
429    /// Whether `b` is in the base64 alphabet (RFC 4648 section 4), padding aside.
430    is_base64_char, BASE64, b'+', b'-'
431);
432predicate!(
433    /// Whether `b` is an `atext` byte (RFC 5322 section 3.2.3).
434    is_atext, ATEXT, b'{', b'@'
435);
436predicate!(
437    /// Whether `b` is a `bcharsnospace` byte (RFC 2046 section 5.1.1).
438    is_bchar_nospace, BCHAR_NOSPACE, b'?', b' '
439);
440predicate!(
441    /// Whether `b` is a `cookie-octet` (rfc6265bis section 4.1.1).
442    is_cookie_octet, COOKIE_OCTET, b'!', b';'
443);
444predicate!(
445    /// Whether `b` may appear in a cookie attribute value (rfc6265bis section 4.1.1).
446    is_av_octet, AV_OCTET, b' ', b';'
447);
448
449/// Whether `b` is field text other than a list delimiter (`,` `"` `(` `)`).
450#[inline]
451pub(crate) const fn is_list_plain(b: u8) -> bool {
452    has(b, LIST_PLAIN)
453}
454
455/// The value of one hex digit (either case), or `None`.
456///
457/// # Examples
458///
459/// ```
460/// use parse_net::class::hex_value;
461///
462/// assert_eq!(hex_value(b'A'), Some(10));
463/// assert_eq!(hex_value(b'g'), None);
464/// ```
465#[inline]
466#[must_use]
467pub const fn hex_value(b: u8) -> Option<u8> {
468    match b {
469        b'0'..=b'9' => Some(b - b'0'),
470        b'a'..=b'f' => Some(b - b'a' + 10),
471        b'A'..=b'F' => Some(b - b'A' + 10),
472        _ => None,
473    }
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    // Slow references written straight from the ABNF, one per class.
481    fn r_alnum(b: u8) -> bool {
482        b.is_ascii_alphanumeric()
483    }
484    fn r_vchar(b: u8) -> bool {
485        (0x21..=0x7e).contains(&b)
486    }
487    fn r_obs(b: u8) -> bool {
488        b >= 0x80
489    }
490    fn r_tchar(b: u8) -> bool {
491        r_alnum(b) || b"!#$%&'*+-.^_`|~".contains(&b)
492    }
493
494    fn check(name: &str, fast: fn(u8) -> bool, slow: impl Fn(u8) -> bool) {
495        for b in 0u8..=255 {
496            assert_eq!(fast(b), slow(b), "{name}: byte {b:#04x}");
497        }
498    }
499
500    #[test]
501    fn test_tchar_table_exhaustive() {
502        check("tchar", is_tchar, r_tchar);
503        check("vchar", is_vchar, r_vchar);
504        check("obs-text", is_obs_text, r_obs);
505        check("field-text", is_field_text, |b| {
506            r_vchar(b) || r_obs(b) || b == b' ' || b == b'\t'
507        });
508    }
509
510    #[test]
511    fn test_quoting_classes_exhaustive() {
512        check("qdtext", is_qdtext, |b| {
513            b == b'\t'
514                || b == b' '
515                || b == 0x21
516                || (0x23..=0x5b).contains(&b)
517                || (0x5d..=0x7e).contains(&b)
518                || r_obs(b)
519        });
520        check("ctext", is_ctext, |b| {
521            b == b'\t'
522                || b == b' '
523                || (0x21..=0x27).contains(&b)
524                || (0x2a..=0x5b).contains(&b)
525                || (0x5d..=0x7e).contains(&b)
526                || r_obs(b)
527        });
528        check("quoted-pair", is_quoted_pair_char, |b| {
529            b == b'\t' || b == b' ' || r_vchar(b) || r_obs(b)
530        });
531        check("etagc", is_etagc, |b| {
532            b == 0x21 || (0x23..=0x7e).contains(&b) || r_obs(b)
533        });
534    }
535
536    #[test]
537    fn test_mime_and_attr_classes_exhaustive() {
538        check("attr-char", is_attr_char, |b| {
539            r_alnum(b) || b"!#$&+-.^_`|~".contains(&b)
540        });
541        check("mime token", is_mime_token_char, |b| {
542            (0x21..=0x7e).contains(&b) && !b"()<>@,;:\\\"/[]?=".contains(&b)
543        });
544        check("bcharsnospace", is_bchar_nospace, |b| {
545            r_alnum(b) || b"'()+_,-./:=?".contains(&b)
546        });
547        check("atext", is_atext, |b| {
548            r_alnum(b) || b"!#$%&'*+-/=?^_`{|}~".contains(&b)
549        });
550    }
551
552    #[test]
553    fn test_uri_classes_exhaustive() {
554        let unreserved = |b: u8| r_alnum(b) || b"-._~".contains(&b);
555        let sub = |b: u8| b"!$&'()*+,;=".contains(&b);
556        check("unreserved", is_unreserved, unreserved);
557        check("sub-delims", is_sub_delim, sub);
558        check("pchar", is_pchar_no_pct, |b| {
559            unreserved(b) || sub(b) || b == b':' || b == b'@'
560        });
561        check("target", is_uri_target, |b| {
562            unreserved(b) || sub(b) || b":@/?%".contains(&b)
563        });
564        check("hexdig", is_hexdig, |b| b.is_ascii_hexdigit());
565    }
566
567    #[test]
568    fn test_structured_field_classes_exhaustive() {
569        check("sf key start", is_sf_key_start, |b| {
570            b.is_ascii_lowercase() || b == b'*'
571        });
572        check("sf key", is_sf_key_char, |b| {
573            b.is_ascii_lowercase() || b.is_ascii_digit() || b"_-.*".contains(&b)
574        });
575        check("sf token", is_sf_token_char, |b| {
576            r_tchar(b) || b == b':' || b == b'/'
577        });
578        check("sf string", is_sf_string_char, |b| {
579            b == 0x20 || b == 0x21 || (0x23..=0x5b).contains(&b) || (0x5d..=0x7e).contains(&b)
580        });
581    }
582
583    #[test]
584    fn test_base64_and_cookie_classes_exhaustive() {
585        check("token68", is_token68_char, |b| {
586            r_alnum(b) || b"-._~+/".contains(&b)
587        });
588        check("base64", is_base64_char, |b| {
589            r_alnum(b) || b == b'+' || b == b'/'
590        });
591        check("cookie-octet", is_cookie_octet, |b| {
592            b == 0x21
593                || (0x23..=0x2b).contains(&b)
594                || (0x2d..=0x3a).contains(&b)
595                || (0x3c..=0x5b).contains(&b)
596                || (0x5d..=0x7e).contains(&b)
597        });
598        check("av-octet", is_av_octet, |b| {
599            (0x20..=0x3a).contains(&b) || (0x3c..=0x7e).contains(&b)
600        });
601    }
602
603    #[test]
604    fn test_list_plain_is_field_text_without_delimiters() {
605        for b in 0u8..=255 {
606            let expected = is_field_text(b) && !b",\"()".contains(&b);
607            assert_eq!(is_list_plain(b), expected, "byte {b:#04x}");
608        }
609    }
610
611    #[test]
612    fn test_del_is_in_no_class() {
613        assert_eq!(CLASSES[0x7f], 0);
614    }
615
616    #[test]
617    fn test_unicode_bytes_never_in_ascii_classes() {
618        let ascii_only = TCHAR
619            | VCHAR
620            | ATTR_CHAR
621            | MIME_TOKEN
622            | UNRESERVED
623            | SUB_DELIM
624            | PCHAR_NO_PCT
625            | URI_TARGET
626            | HEXDIG
627            | SF_KEY_START
628            | SF_KEY
629            | SF_TOKEN
630            | SF_STRING
631            | TOKEN68
632            | BASE64
633            | ATEXT
634            | BCHAR_NOSPACE
635            | COOKIE_OCTET
636            | AV_OCTET;
637        for b in 0x80u8..=0xff {
638            assert_eq!(CLASSES[b as usize] & ascii_only, 0, "byte {b:#04x}");
639        }
640    }
641
642    #[test]
643    fn test_hex_value_all_bytes() {
644        for b in 0u8..=255 {
645            let expected = (b as char).to_digit(16).map(|d| d as u8);
646            assert_eq!(hex_value(b), expected, "byte {b:#04x}");
647        }
648    }
649
650    #[test]
651    fn test_class_all_and_union() {
652        assert!(all(b"", Class::TCHAR));
653        assert!(all(b"abc", Class::TCHAR));
654        assert!(!all(b"a c", Class::TCHAR));
655        let c = Class::TCHAR.union(Class::SF_KEY);
656        assert!(c.contains(b'a') && c.contains(b'*') && !c.contains(b' '));
657        for b in 0u8..=255 {
658            assert_eq!(Class::QDTEXT.contains(b), is_qdtext(b));
659        }
660    }
661}