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}