time/parsing/combinator/rfc/
rfc2822.rs

1//! Rules defined in [RFC 2822].
2//!
3//! [RFC 2822]: https://datatracker.ietf.org/doc/html/rfc2822
4
5use crate::parsing::combinator::rfc::rfc2234::wsp;
6use crate::parsing::combinator::{ascii_char, one_or_more, zero_or_more};
7use crate::parsing::ParsedItem;
8
9/// Consume the `fws` rule.
10// The full rule is equivalent to /\r\n[ \t]+|[ \t]+(?:\r\n[ \t]+)*/
11#[inline]
12pub(crate) fn fws(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
13    if let [b'\r', b'\n', rest @ ..] = input {
14        one_or_more(wsp)(rest)
15    } else {
16        input = one_or_more(wsp)(input)?.into_inner();
17        while let [b'\r', b'\n', rest @ ..] = input {
18            input = one_or_more(wsp)(rest)?.into_inner();
19        }
20        Some(ParsedItem(input, ()))
21    }
22}
23
24/// Consume the `cfws` rule.
25// The full rule is equivalent to any combination of `fws` and `comment` so long as it is not empty.
26#[inline]
27pub(crate) fn cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
28    one_or_more(|input| fws(input).or_else(|| comment(input)))(input)
29}
30
31/// Consume the `comment` rule.
32#[inline]
33fn comment(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
34    input = ascii_char::<b'('>(input)?.into_inner();
35    input = zero_or_more(fws)(input).into_inner();
36    while let Some(rest) = ccontent(input) {
37        input = rest.into_inner();
38        input = zero_or_more(fws)(input).into_inner();
39    }
40    input = ascii_char::<b')'>(input)?.into_inner();
41
42    Some(ParsedItem(input, ()))
43}
44
45/// Consume the `ccontent` rule.
46#[inline]
47fn ccontent(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
48    ctext(input)
49        .or_else(|| quoted_pair(input))
50        .or_else(|| comment(input))
51}
52
53/// Consume the `ctext` rule.
54#[expect(
55    clippy::unnecessary_lazy_evaluations,
56    reason = "rust-lang/rust-clippy#8522"
57)]
58#[inline]
59fn ctext(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
60    no_ws_ctl(input).or_else(|| match input {
61        [33..=39 | 42..=91 | 93..=126, rest @ ..] => Some(ParsedItem(rest, ())),
62        _ => None,
63    })
64}
65
66/// Consume the `quoted_pair` rule.
67#[inline]
68fn quoted_pair(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
69    input = ascii_char::<b'\\'>(input)?.into_inner();
70    input = text(input).into_inner();
71
72    // If nothing is parsed by `text`, this means by hit the `obs-text` rule and nothing matched.
73    // This is technically a success, and we used to check the `obs-qp` rule to ensure everything
74    // possible was consumed. After further analysis, it was determined that this check was
75    // unnecessary due to `obs-text` wholly subsuming `obs-qp` in this context. For this reason, if
76    // `text` fails to parse anything, we consider it a success without further consideration.
77
78    Some(ParsedItem(input, ()))
79}
80
81/// Consume the `no_ws_ctl` rule.
82#[inline]
83const fn no_ws_ctl(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
84    match input {
85        [1..=8 | 11..=12 | 14..=31 | 127, rest @ ..] => Some(ParsedItem(rest, ())),
86        _ => None,
87    }
88}
89
90/// Consume the `text` rule.
91#[inline]
92fn text<'a>(input: &'a [u8]) -> ParsedItem<'a, ()> {
93    let new_text = |input: &'a [u8]| match input {
94        [1..=9 | 11..=12 | 14..=127, rest @ ..] => Some(ParsedItem(rest, ())),
95        _ => None,
96    };
97
98    let obs_char = |input: &'a [u8]| match input {
99        // This is technically allowed, but consuming this would mean the rest of the string is
100        // eagerly consumed without consideration for where the comment actually ends.
101        [b')', ..] => None,
102        [0..=9 | 11..=12 | 14..=127, rest @ ..] => Some(rest),
103        _ => None,
104    };
105
106    let obs_text = |mut input| {
107        input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
108        input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
109        while let Some(rest) = obs_char(input) {
110            input = rest;
111            input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
112            input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
113        }
114
115        ParsedItem(input, ())
116    };
117
118    new_text(input).unwrap_or_else(|| obs_text(input))
119}