1const 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;
50const LIST_PLAIN: u32 = 1 << 26;
53
54const 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 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 if vchar || obs || sp_ht {
87 class |= FIELD_TEXT;
88 }
89 if sp_ht || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) || obs {
91 class |= QDTEXT;
92 }
93 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 if sp_ht || vchar || obs {
104 class |= QUOTED_PAIR;
105 }
106 if c == 0x21 || (c >= 0x23 && c <= 0x7e) || obs {
108 class |= ETAGC;
109 }
110 if alnum || one_of(c, b"!#$&+-.^_`|~") {
112 class |= ATTR_CHAR;
113 }
114 if vchar && !one_of(c, b"()<>@,;:\\\"/[]?=") {
116 class |= MIME_TOKEN;
117 }
118 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 if pchar || c == b'/' || c == b'?' || c == b'%' {
133 class |= URI_TARGET;
134 }
135 if c.is_ascii_hexdigit() {
136 class |= HEXDIG;
137 }
138 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 if alnum || one_of(c, b"!#$%&'*+-.^_`|~") || c == b':' || c == b'/' {
148 class |= SF_TOKEN;
149 }
150 if c == 0x20 || c == 0x21 || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x7e) {
152 class |= SF_STRING;
153 }
154 if alnum || one_of(c, b"-._~+/") {
156 class |= TOKEN68;
157 }
158 if alnum || c == b'+' || c == b'/' {
160 class |= BASE64;
161 }
162 if alnum || one_of(c, b"!#$%&'*+-/=?^_`{|}~") {
164 class |= ATEXT;
165 }
166 if alnum || one_of(c, b"'()+_,-./:=?") {
168 class |= BCHAR_NOSPACE;
169 }
170 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 #[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 #[allow(clippy::indexing_slicing)]
211 let entry = CLASSES[b as usize];
212 entry & bit != 0
213}
214
215#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
228pub struct Class(u32);
229
230impl Class {
231 pub const TCHAR: Self = Self(TCHAR);
233 pub const VCHAR: Self = Self(VCHAR);
235 pub const OBS_TEXT: Self = Self(OBS_TEXT);
237 pub const FIELD_TEXT: Self = Self(FIELD_TEXT);
239 pub const QDTEXT: Self = Self(QDTEXT);
241 pub const CTEXT: Self = Self(CTEXT);
243 pub const QUOTED_PAIR: Self = Self(QUOTED_PAIR);
245 pub const ETAGC: Self = Self(ETAGC);
247 pub const ATTR_CHAR: Self = Self(ATTR_CHAR);
249 pub const MIME_TOKEN: Self = Self(MIME_TOKEN);
252 pub const UNRESERVED: Self = Self(UNRESERVED);
254 pub const SUB_DELIM: Self = Self(SUB_DELIM);
256 pub const PCHAR_NO_PCT: Self = Self(PCHAR_NO_PCT);
258 pub const URI_TARGET: Self = Self(URI_TARGET);
260 pub const HEXDIG: Self = Self(HEXDIG);
262 pub const SF_KEY_START: Self = Self(SF_KEY_START);
264 pub const SF_KEY: Self = Self(SF_KEY);
266 pub const SF_TOKEN: Self = Self(SF_TOKEN);
268 pub const SF_STRING: Self = Self(SF_STRING);
270 pub const TOKEN68: Self = Self(TOKEN68);
272 pub const BASE64: Self = Self(BASE64);
274 pub const ATEXT: Self = Self(ATEXT);
276 pub const BCHAR_NOSPACE: Self = Self(BCHAR_NOSPACE);
278 pub const COOKIE_OCTET: Self = Self(COOKIE_OCTET);
280 pub const AV_OCTET: Self = Self(AV_OCTET);
282
283 #[inline]
285 #[must_use]
286 pub const fn contains(self, b: u8) -> bool {
287 has(b, self.0)
288 }
289
290 #[must_use]
292 pub const fn union(self, other: Self) -> Self {
293 Self(self.0 | other.0)
294 }
295}
296
297#[inline]
310#[must_use]
311pub const fn all(s: &[u8], class: Class) -> bool {
312 let mut i = 0;
315 while i < s.len() {
316 #[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 #[doc = concat!("assert!(parse_net::class::", stringify!($name), "(", stringify!($example_yes), "));")]
335 #[doc = concat!("assert!(!parse_net::class::", stringify!($name), "(", stringify!($example_no), "));")]
336 #[inline]
338 #[must_use]
339 pub const fn $name(b: u8) -> bool {
340 has(b, $bit)
341 }
342 };
343}
344
345predicate!(
346 is_tchar, TCHAR, b'~', b'('
348);
349predicate!(
350 is_vchar, VCHAR, b'!', b' '
352);
353predicate!(
354 is_obs_text, OBS_TEXT, 0x80, 0x7f
356);
357predicate!(
358 is_field_text, FIELD_TEXT, b'\t', b'\r'
361);
362predicate!(
363 is_qdtext, QDTEXT, b' ', b'"'
365);
366predicate!(
367 is_ctext, CTEXT, b'a', b'('
369);
370predicate!(
371 is_quoted_pair_char, QUOTED_PAIR, b'"', 0x7f
373);
374predicate!(
375 is_etagc, ETAGC, b'!', b'"'
377);
378predicate!(
379 is_attr_char, ATTR_CHAR, b'|', b'%'
381);
382predicate!(
383 is_mime_token_char, MIME_TOKEN, b'{', b'/'
385);
386predicate!(
387 is_unreserved, UNRESERVED, b'~', b'!'
389);
390predicate!(
391 is_sub_delim, SUB_DELIM, b'!', b'/'
393);
394predicate!(
395 is_pchar_no_pct, PCHAR_NO_PCT, b'@', b'/'
398);
399predicate!(
400 is_uri_target, URI_TARGET, b'?', b'#'
403);
404predicate!(
405 is_hexdig, HEXDIG, b'F', b'g'
407);
408predicate!(
409 is_sf_key_start, SF_KEY_START, b'*', b'A'
411);
412predicate!(
413 is_sf_key_char, SF_KEY, b'_', b'A'
415);
416predicate!(
417 is_sf_token_char, SF_TOKEN, b':', b'"'
419);
420predicate!(
421 is_sf_string_char, SF_STRING, b' ', b'\\'
423);
424predicate!(
425 is_token68_char, TOKEN68, b'~', b'='
427);
428predicate!(
429 is_base64_char, BASE64, b'+', b'-'
431);
432predicate!(
433 is_atext, ATEXT, b'{', b'@'
435);
436predicate!(
437 is_bchar_nospace, BCHAR_NOSPACE, b'?', b' '
439);
440predicate!(
441 is_cookie_octet, COOKIE_OCTET, b'!', b';'
443);
444predicate!(
445 is_av_octet, AV_OCTET, b' ', b';'
447);
448
449#[inline]
451pub(crate) const fn is_list_plain(b: u8) -> bool {
452 has(b, LIST_PLAIN)
453}
454
455#[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 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}