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    ///
233    /// # Examples
234    ///
235    /// ```
236    /// use parse_net::class::{all, Class};
237    ///
238    /// assert!(all(b"no-cache", Class::TCHAR) && !Class::TCHAR.contains(b'('));
239    /// ```
240    pub const TCHAR: Self = Self(TCHAR);
241    /// `VCHAR`: 0x21 to 0x7E (RFC 5234).
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use parse_net::class::{all, Class};
247    ///
248    /// assert!(all(b"a=b;c", Class::VCHAR) && !Class::VCHAR.contains(b' '));
249    /// ```
250    pub const VCHAR: Self = Self(VCHAR);
251    /// `obs-text`: 0x80 to 0xFF (RFC 9110 section 5.5).
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use parse_net::class::{all, Class};
257    ///
258    /// assert!(all(b"\xc3\xa9", Class::OBS_TEXT) && !Class::OBS_TEXT.contains(b'a'));
259    /// ```
260    pub const OBS_TEXT: Self = Self(OBS_TEXT);
261    /// `field-vchar / SP / HTAB` (RFC 9110 section 5.5).
262    ///
263    /// # Examples
264    ///
265    /// ```
266    /// use parse_net::class::{all, Class};
267    ///
268    /// assert!(all(b"text/html; q=0.9\t", Class::FIELD_TEXT));
269    /// assert!(!Class::FIELD_TEXT.contains(b'\r'));
270    /// ```
271    pub const FIELD_TEXT: Self = Self(FIELD_TEXT);
272    /// `qdtext` (RFC 9110 section 5.6.4).
273    ///
274    /// # Examples
275    ///
276    /// ```
277    /// use parse_net::class::{all, Class};
278    ///
279    /// assert!(all(b"a b,c", Class::QDTEXT));
280    /// assert!(!Class::QDTEXT.contains(b'"') && !Class::QDTEXT.contains(b'\\'));
281    /// ```
282    pub const QDTEXT: Self = Self(QDTEXT);
283    /// `ctext` (RFC 9110 section 5.6.5).
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use parse_net::class::{all, Class};
289    ///
290    /// assert!(all(b"compatible; MSIE", Class::CTEXT) && !Class::CTEXT.contains(b'('));
291    /// ```
292    pub const CTEXT: Self = Self(CTEXT);
293    /// The byte after `\` in a `quoted-pair`: `HTAB / SP / VCHAR / obs-text`.
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use parse_net::class::Class;
299    ///
300    /// assert!(Class::QUOTED_PAIR.contains(b'"') && Class::QUOTED_PAIR.contains(b'\t'));
301    /// assert!(!Class::QUOTED_PAIR.contains(0x7f));
302    /// ```
303    pub const QUOTED_PAIR: Self = Self(QUOTED_PAIR);
304    /// `etagc` (RFC 9110 section 8.8.3).
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// use parse_net::class::{all, Class};
310    ///
311    /// assert!(all(b"W/xyz", Class::ETAGC) && !Class::ETAGC.contains(b'"'));
312    /// ```
313    pub const ETAGC: Self = Self(ETAGC);
314    /// `attr-char` (RFC 8187 section 3.2.1).
315    ///
316    /// # Examples
317    ///
318    /// ```
319    /// use parse_net::class::{all, Class};
320    ///
321    /// assert!(all(b"utf-8", Class::ATTR_CHAR));
322    /// assert!(!Class::ATTR_CHAR.contains(b'%') && !Class::ATTR_CHAR.contains(b'\''));
323    /// ```
324    pub const ATTR_CHAR: Self = Self(ATTR_CHAR);
325    /// The MIME `token` characters (RFC 2045 section 5.1); unlike `tchar`,
326    /// they include `{` and `}`.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use parse_net::class::{all, Class};
332    ///
333    /// assert!(all(b"{x}", Class::MIME_TOKEN) && !all(b"{x}", Class::TCHAR));
334    /// assert!(!Class::MIME_TOKEN.contains(b'/'));
335    /// ```
336    pub const MIME_TOKEN: Self = Self(MIME_TOKEN);
337    /// `unreserved` (RFC 3986 section 2.3).
338    ///
339    /// # Examples
340    ///
341    /// ```
342    /// use parse_net::class::{all, Class};
343    ///
344    /// assert!(all(b"a-b_c.d~", Class::UNRESERVED) && !Class::UNRESERVED.contains(b'!'));
345    /// ```
346    pub const UNRESERVED: Self = Self(UNRESERVED);
347    /// `sub-delims` (RFC 3986 section 2.2).
348    ///
349    /// # Examples
350    ///
351    /// ```
352    /// use parse_net::class::{all, Class};
353    ///
354    /// assert!(all(b"!$&'()*+,;=", Class::SUB_DELIM) && !Class::SUB_DELIM.contains(b'/'));
355    /// ```
356    pub const SUB_DELIM: Self = Self(SUB_DELIM);
357    /// `unreserved / sub-delims / ":" / "@"`: `pchar` without `pct-encoded`.
358    ///
359    /// # Examples
360    ///
361    /// ```
362    /// use parse_net::class::{all, Class};
363    ///
364    /// assert!(all(b"user:pass@host", Class::PCHAR_NO_PCT));
365    /// assert!(!Class::PCHAR_NO_PCT.contains(b'/') && !Class::PCHAR_NO_PCT.contains(b'%'));
366    /// ```
367    pub const PCHAR_NO_PCT: Self = Self(PCHAR_NO_PCT);
368    /// `pchar / "/" / "?"` plus `%`: the characters of a request target.
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use parse_net::class::{all, Class};
374    ///
375    /// assert!(all(b"/a%20b?q=1", Class::URI_TARGET) && !Class::URI_TARGET.contains(b'#'));
376    /// ```
377    pub const URI_TARGET: Self = Self(URI_TARGET);
378    /// `HEXDIG`, either case.
379    ///
380    /// # Examples
381    ///
382    /// ```
383    /// use parse_net::class::{all, Class};
384    ///
385    /// assert!(all(b"0aF9", Class::HEXDIG) && !Class::HEXDIG.contains(b'g'));
386    /// ```
387    pub const HEXDIG: Self = Self(HEXDIG);
388    /// The first byte of a Structured Field key: `lcalpha / "*"`.
389    ///
390    /// # Examples
391    ///
392    /// ```
393    /// use parse_net::class::Class;
394    ///
395    /// assert!(Class::SF_KEY_START.contains(b'*') && Class::SF_KEY_START.contains(b'a'));
396    /// assert!(!Class::SF_KEY_START.contains(b'_') && !Class::SF_KEY_START.contains(b'0'));
397    /// ```
398    pub const SF_KEY_START: Self = Self(SF_KEY_START);
399    /// A Structured Field key byte: `lcalpha / DIGIT / "_" / "-" / "." / "*"`.
400    ///
401    /// # Examples
402    ///
403    /// ```
404    /// use parse_net::class::{all, Class};
405    ///
406    /// assert!(all(b"max-age_2.*", Class::SF_KEY) && !Class::SF_KEY.contains(b'A'));
407    /// ```
408    pub const SF_KEY: Self = Self(SF_KEY);
409    /// A Structured Field token byte after the first: `tchar / ":" / "/"`.
410    ///
411    /// # Examples
412    ///
413    /// ```
414    /// use parse_net::class::{all, Class};
415    ///
416    /// assert!(all(b"text/html:x", Class::SF_TOKEN) && !Class::SF_TOKEN.contains(b'"'));
417    /// ```
418    pub const SF_TOKEN: Self = Self(SF_TOKEN);
419    /// An unescaped Structured Field string byte: `%x20-21 / %x23-5B / %x5D-7E`.
420    ///
421    /// # Examples
422    ///
423    /// ```
424    /// use parse_net::class::{all, Class};
425    ///
426    /// assert!(all(b"hello world!", Class::SF_STRING));
427    /// assert!(!Class::SF_STRING.contains(b'\\') && !Class::SF_STRING.contains(b'"'));
428    /// ```
429    pub const SF_STRING: Self = Self(SF_STRING);
430    /// A `token68` byte (RFC 9110 section 11.2), without the `=` padding.
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use parse_net::class::{all, Class};
436    ///
437    /// assert!(all(b"abc-._~+/", Class::TOKEN68) && !Class::TOKEN68.contains(b'='));
438    /// ```
439    pub const TOKEN68: Self = Self(TOKEN68);
440    /// The base64 alphabet (RFC 4648 section 4), without the `=` padding.
441    ///
442    /// # Examples
443    ///
444    /// ```
445    /// use parse_net::class::{all, Class};
446    ///
447    /// assert!(all(b"aGk+/w", Class::BASE64));
448    /// assert!(!Class::BASE64.contains(b'=') && !Class::BASE64.contains(b'-'));
449    /// ```
450    pub const BASE64: Self = Self(BASE64);
451    /// `atext` (RFC 5322 section 3.2.3).
452    ///
453    /// # Examples
454    ///
455    /// ```
456    /// use parse_net::class::{all, Class};
457    ///
458    /// assert!(all(b"user+tag", Class::ATEXT) && !Class::ATEXT.contains(b'@'));
459    /// ```
460    pub const ATEXT: Self = Self(ATEXT);
461    /// `bcharsnospace` (RFC 2046 section 5.1.1).
462    ///
463    /// # Examples
464    ///
465    /// ```
466    /// use parse_net::class::{all, Class};
467    ///
468    /// assert!(all(b"----=_Part_0", Class::BCHAR_NOSPACE) && !Class::BCHAR_NOSPACE.contains(b' '));
469    /// ```
470    pub const BCHAR_NOSPACE: Self = Self(BCHAR_NOSPACE);
471    /// `cookie-octet` (rfc6265bis section 4.1.1), for cookie-net.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use parse_net::class::{all, Class};
477    ///
478    /// assert!(all(b"abc123!", Class::COOKIE_OCTET));
479    /// assert!(!Class::COOKIE_OCTET.contains(b';') && !Class::COOKIE_OCTET.contains(b','));
480    /// ```
481    pub const COOKIE_OCTET: Self = Self(COOKIE_OCTET);
482    /// Cookie attribute value bytes (rfc6265bis section 4.1.1), for cookie-net.
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// use parse_net::class::{all, Class};
488    ///
489    /// assert!(all(b"Mon, 01 Jan", Class::AV_OCTET) && !Class::AV_OCTET.contains(b';'));
490    /// ```
491    pub const AV_OCTET: Self = Self(AV_OCTET);
492
493    /// Whether `b` is in this class.
494    ///
495    /// # Examples
496    ///
497    /// ```
498    /// use parse_net::class::Class;
499    ///
500    /// assert!(Class::HEXDIG.contains(b'a') && !Class::HEXDIG.contains(b'g'));
501    /// ```
502    #[inline]
503    #[must_use]
504    pub const fn contains(self, b: u8) -> bool {
505        has(b, self.0)
506    }
507
508    /// A class holding the bytes of both.
509    ///
510    /// # Examples
511    ///
512    /// ```
513    /// use parse_net::class::Class;
514    ///
515    /// let c = Class::HEXDIG.union(Class::SUB_DELIM);
516    /// assert!(c.contains(b'f') && c.contains(b'+') && !c.contains(b'g'));
517    /// ```
518    #[must_use]
519    pub const fn union(self, other: Self) -> Self {
520        Self(self.0 | other.0)
521    }
522}
523
524/// Whether every byte of `s` is in `class` (true for an empty `s`). Stops at
525/// the first byte outside.
526///
527/// # Examples
528///
529/// ```
530/// use parse_net::class::{all, Class};
531///
532/// assert!(all(b"gzip", Class::TCHAR));
533/// assert!(!all(b"gzip, br", Class::TCHAR));
534/// assert!(all(b"", Class::TCHAR));
535/// ```
536#[inline]
537#[must_use]
538pub const fn all(s: &[u8], class: Class) -> bool {
539    // An indexed loop: the compiler drops the bounds check for `i < len`,
540    // which the slice-pattern form does not get as reliably.
541    let mut i = 0;
542    while i < s.len() {
543        // `i < s.len()` was just checked.
544        #[allow(clippy::indexing_slicing)]
545        let b = s[i];
546        if !has(b, class.0) {
547            return false;
548        }
549        i += 1;
550    }
551    true
552}
553
554macro_rules! predicate {
555    ($(#[$doc:meta])* $name:ident, $bit:ident, $example_yes:literal, $example_no:literal) => {
556        $(#[$doc])*
557        ///
558        /// # Examples
559        ///
560        /// ```
561        #[doc = concat!("assert!(parse_net::class::", stringify!($name), "(", stringify!($example_yes), "));")]
562        #[doc = concat!("assert!(!parse_net::class::", stringify!($name), "(", stringify!($example_no), "));")]
563        /// ```
564        #[inline]
565        #[must_use]
566        pub const fn $name(b: u8) -> bool {
567            has(b, $bit)
568        }
569    };
570}
571
572predicate!(
573    /// Whether `b` is a `tchar`, a byte allowed in a token (RFC 9110 section 5.6.2).
574    is_tchar, TCHAR, b'~', b'('
575);
576predicate!(
577    /// Whether `b` is a visible ASCII character, 0x21 to 0x7E.
578    is_vchar, VCHAR, b'!', b' '
579);
580predicate!(
581    /// Whether `b` is `obs-text`, 0x80 to 0xFF.
582    is_obs_text, OBS_TEXT, 0x80, 0x7f
583);
584predicate!(
585    /// Whether `b` may appear in a field value: `field-vchar / SP / HTAB`
586    /// (RFC 9110 section 5.5).
587    is_field_text, FIELD_TEXT, b'\t', b'\r'
588);
589predicate!(
590    /// Whether `b` may appear unescaped in a quoted string (RFC 9110 section 5.6.4).
591    is_qdtext, QDTEXT, b' ', b'"'
592);
593predicate!(
594    /// Whether `b` may appear unescaped in a comment (RFC 9110 section 5.6.5).
595    is_ctext, CTEXT, b'a', b'('
596);
597predicate!(
598    /// Whether `b` may follow `\` in a `quoted-pair`: `HTAB / SP / VCHAR / obs-text`.
599    is_quoted_pair_char, QUOTED_PAIR, b'"', 0x7f
600);
601predicate!(
602    /// Whether `b` is an `etagc` (RFC 9110 section 8.8.3).
603    is_etagc, ETAGC, b'!', b'"'
604);
605predicate!(
606    /// Whether `b` is an `attr-char` (RFC 8187 section 3.2.1).
607    is_attr_char, ATTR_CHAR, b'|', b'%'
608);
609predicate!(
610    /// Whether `b` may appear in a MIME `token` (RFC 2045 section 5.1).
611    is_mime_token_char, MIME_TOKEN, b'{', b'/'
612);
613predicate!(
614    /// Whether `b` is `unreserved` (RFC 3986 section 2.3).
615    is_unreserved, UNRESERVED, b'~', b'!'
616);
617predicate!(
618    /// Whether `b` is a `sub-delims` byte (RFC 3986 section 2.2).
619    is_sub_delim, SUB_DELIM, b'!', b'/'
620);
621predicate!(
622    /// Whether `b` is a `pchar` other than the `%` of `pct-encoded`
623    /// (RFC 3986 section 3.3).
624    is_pchar_no_pct, PCHAR_NO_PCT, b'@', b'/'
625);
626predicate!(
627    /// Whether `b` may appear in a request target: `pchar / "/" / "?"` or the
628    /// `%` of a `pct-encoded` triple (whose digits are checked apart).
629    is_uri_target, URI_TARGET, b'?', b'#'
630);
631predicate!(
632    /// Whether `b` is a hex digit, either case.
633    is_hexdig, HEXDIG, b'F', b'g'
634);
635predicate!(
636    /// Whether `b` may start a Structured Field key: `lcalpha / "*"`.
637    is_sf_key_start, SF_KEY_START, b'*', b'A'
638);
639predicate!(
640    /// Whether `b` may appear in a Structured Field key.
641    is_sf_key_char, SF_KEY, b'_', b'A'
642);
643predicate!(
644    /// Whether `b` may appear in a Structured Field token after its first byte.
645    is_sf_token_char, SF_TOKEN, b':', b'"'
646);
647predicate!(
648    /// Whether `b` may appear unescaped in a Structured Field string.
649    is_sf_string_char, SF_STRING, b' ', b'\\'
650);
651predicate!(
652    /// Whether `b` is a `token68` byte other than the `=` padding.
653    is_token68_char, TOKEN68, b'~', b'='
654);
655predicate!(
656    /// Whether `b` is in the base64 alphabet (RFC 4648 section 4), padding aside.
657    is_base64_char, BASE64, b'+', b'-'
658);
659predicate!(
660    /// Whether `b` is an `atext` byte (RFC 5322 section 3.2.3).
661    is_atext, ATEXT, b'{', b'@'
662);
663predicate!(
664    /// Whether `b` is a `bcharsnospace` byte (RFC 2046 section 5.1.1).
665    is_bchar_nospace, BCHAR_NOSPACE, b'?', b' '
666);
667predicate!(
668    /// Whether `b` is a `cookie-octet` (rfc6265bis section 4.1.1).
669    is_cookie_octet, COOKIE_OCTET, b'!', b';'
670);
671predicate!(
672    /// Whether `b` may appear in a cookie attribute value (rfc6265bis section 4.1.1).
673    is_av_octet, AV_OCTET, b' ', b';'
674);
675
676/// Whether `b` is field text other than a list delimiter (`,` `"` `(` `)`).
677#[inline]
678pub(crate) const fn is_list_plain(b: u8) -> bool {
679    has(b, LIST_PLAIN)
680}
681
682/// The value of one hex digit (either case), or `None`.
683///
684/// # Examples
685///
686/// ```
687/// use parse_net::class::hex_value;
688///
689/// assert_eq!(hex_value(b'A'), Some(10));
690/// assert_eq!(hex_value(b'g'), None);
691/// ```
692#[inline]
693#[must_use]
694pub const fn hex_value(b: u8) -> Option<u8> {
695    match b {
696        b'0'..=b'9' => Some(b - b'0'),
697        b'a'..=b'f' => Some(b - b'a' + 10),
698        b'A'..=b'F' => Some(b - b'A' + 10),
699        _ => None,
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    // Slow references written straight from the ABNF, one per class.
708    fn r_alnum(b: u8) -> bool {
709        b.is_ascii_alphanumeric()
710    }
711    fn r_vchar(b: u8) -> bool {
712        (0x21..=0x7e).contains(&b)
713    }
714    fn r_obs(b: u8) -> bool {
715        b >= 0x80
716    }
717    fn r_tchar(b: u8) -> bool {
718        r_alnum(b) || b"!#$%&'*+-.^_`|~".contains(&b)
719    }
720
721    fn check(name: &str, fast: fn(u8) -> bool, slow: impl Fn(u8) -> bool) {
722        for b in 0u8..=255 {
723            assert_eq!(fast(b), slow(b), "{name}: byte {b:#04x}");
724        }
725    }
726
727    #[test]
728    fn test_tchar_table_exhaustive() {
729        check("tchar", is_tchar, r_tchar);
730        check("vchar", is_vchar, r_vchar);
731        check("obs-text", is_obs_text, r_obs);
732        check("field-text", is_field_text, |b| {
733            r_vchar(b) || r_obs(b) || b == b' ' || b == b'\t'
734        });
735    }
736
737    #[test]
738    fn test_quoting_classes_exhaustive() {
739        check("qdtext", is_qdtext, |b| {
740            b == b'\t'
741                || b == b' '
742                || b == 0x21
743                || (0x23..=0x5b).contains(&b)
744                || (0x5d..=0x7e).contains(&b)
745                || r_obs(b)
746        });
747        check("ctext", is_ctext, |b| {
748            b == b'\t'
749                || b == b' '
750                || (0x21..=0x27).contains(&b)
751                || (0x2a..=0x5b).contains(&b)
752                || (0x5d..=0x7e).contains(&b)
753                || r_obs(b)
754        });
755        check("quoted-pair", is_quoted_pair_char, |b| {
756            b == b'\t' || b == b' ' || r_vchar(b) || r_obs(b)
757        });
758        check("etagc", is_etagc, |b| {
759            b == 0x21 || (0x23..=0x7e).contains(&b) || r_obs(b)
760        });
761    }
762
763    #[test]
764    fn test_mime_and_attr_classes_exhaustive() {
765        check("attr-char", is_attr_char, |b| {
766            r_alnum(b) || b"!#$&+-.^_`|~".contains(&b)
767        });
768        check("mime token", is_mime_token_char, |b| {
769            (0x21..=0x7e).contains(&b) && !b"()<>@,;:\\\"/[]?=".contains(&b)
770        });
771        check("bcharsnospace", is_bchar_nospace, |b| {
772            r_alnum(b) || b"'()+_,-./:=?".contains(&b)
773        });
774        check("atext", is_atext, |b| {
775            r_alnum(b) || b"!#$%&'*+-/=?^_`{|}~".contains(&b)
776        });
777    }
778
779    #[test]
780    fn test_uri_classes_exhaustive() {
781        let unreserved = |b: u8| r_alnum(b) || b"-._~".contains(&b);
782        let sub = |b: u8| b"!$&'()*+,;=".contains(&b);
783        check("unreserved", is_unreserved, unreserved);
784        check("sub-delims", is_sub_delim, sub);
785        check("pchar", is_pchar_no_pct, |b| {
786            unreserved(b) || sub(b) || b == b':' || b == b'@'
787        });
788        check("target", is_uri_target, |b| {
789            unreserved(b) || sub(b) || b":@/?%".contains(&b)
790        });
791        check("hexdig", is_hexdig, |b| b.is_ascii_hexdigit());
792    }
793
794    #[test]
795    fn test_structured_field_classes_exhaustive() {
796        check("sf key start", is_sf_key_start, |b| {
797            b.is_ascii_lowercase() || b == b'*'
798        });
799        check("sf key", is_sf_key_char, |b| {
800            b.is_ascii_lowercase() || b.is_ascii_digit() || b"_-.*".contains(&b)
801        });
802        check("sf token", is_sf_token_char, |b| {
803            r_tchar(b) || b == b':' || b == b'/'
804        });
805        check("sf string", is_sf_string_char, |b| {
806            b == 0x20 || b == 0x21 || (0x23..=0x5b).contains(&b) || (0x5d..=0x7e).contains(&b)
807        });
808    }
809
810    #[test]
811    fn test_base64_and_cookie_classes_exhaustive() {
812        check("token68", is_token68_char, |b| {
813            r_alnum(b) || b"-._~+/".contains(&b)
814        });
815        check("base64", is_base64_char, |b| {
816            r_alnum(b) || b == b'+' || b == b'/'
817        });
818        check("cookie-octet", is_cookie_octet, |b| {
819            b == 0x21
820                || (0x23..=0x2b).contains(&b)
821                || (0x2d..=0x3a).contains(&b)
822                || (0x3c..=0x5b).contains(&b)
823                || (0x5d..=0x7e).contains(&b)
824        });
825        check("av-octet", is_av_octet, |b| {
826            (0x20..=0x3a).contains(&b) || (0x3c..=0x7e).contains(&b)
827        });
828    }
829
830    #[test]
831    fn test_list_plain_is_field_text_without_delimiters() {
832        for b in 0u8..=255 {
833            let expected = is_field_text(b) && !b",\"()".contains(&b);
834            assert_eq!(is_list_plain(b), expected, "byte {b:#04x}");
835        }
836    }
837
838    #[test]
839    fn test_del_is_in_no_class() {
840        assert_eq!(CLASSES[0x7f], 0);
841    }
842
843    #[test]
844    fn test_unicode_bytes_never_in_ascii_classes() {
845        let ascii_only = TCHAR
846            | VCHAR
847            | ATTR_CHAR
848            | MIME_TOKEN
849            | UNRESERVED
850            | SUB_DELIM
851            | PCHAR_NO_PCT
852            | URI_TARGET
853            | HEXDIG
854            | SF_KEY_START
855            | SF_KEY
856            | SF_TOKEN
857            | SF_STRING
858            | TOKEN68
859            | BASE64
860            | ATEXT
861            | BCHAR_NOSPACE
862            | COOKIE_OCTET
863            | AV_OCTET;
864        for b in 0x80u8..=0xff {
865            assert_eq!(CLASSES[b as usize] & ascii_only, 0, "byte {b:#04x}");
866        }
867    }
868
869    #[test]
870    fn test_hex_value_all_bytes() {
871        for b in 0u8..=255 {
872            let expected = (b as char).to_digit(16).map(|d| d as u8);
873            assert_eq!(hex_value(b), expected, "byte {b:#04x}");
874        }
875    }
876
877    #[test]
878    fn test_class_all_and_union() {
879        assert!(all(b"", Class::TCHAR));
880        assert!(all(b"abc", Class::TCHAR));
881        assert!(!all(b"a c", Class::TCHAR));
882        let c = Class::TCHAR.union(Class::SF_KEY);
883        assert!(c.contains(b'a') && c.contains(b'*') && !c.contains(b' '));
884        for b in 0u8..=255 {
885            assert_eq!(Class::QDTEXT.contains(b), is_qdtext(b));
886        }
887    }
888}