Skip to main content

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 num_conv::prelude::*;
6
7use crate::parsing::ParsedItem;
8use crate::parsing::combinator::rfc::rfc2234::wsp;
9use crate::parsing::combinator::{ascii_char, one_or_more, zero_or_more};
10
11const DEPTH_LIMIT: u8 = 32;
12
13/// Consume the `fws` rule.
14// The full rule is equivalent to /\r\n[ \t]+|[ \t]+(?:\r\n[ \t]+)*/
15#[inline]
16pub(crate) fn fws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
17    // Fast path for a single space followed by any ASCII character above space (the highest
18    // codepoint for ASCII whitespace).
19    if input.first() == Some(&b' ') && matches!(input.get(1), Some(0x21..)) {
20        return Some(ParsedItem(&input[1..], ()));
21    }
22
23    crate::hint::cold_path();
24
25    // Secondary fast path for single whitespace character followed by non-whitespace.
26    if !matches!(input.first(), Some(b'\r' | b' ' | b'\t')) {
27        return None;
28    }
29
30    if !matches!(input.get(1), Some(b' ' | b'\r' | b'\n' | b'\t')) {
31        return Some(ParsedItem(&input[1..], ()));
32    }
33
34    #[inline(never)]
35    fn fws_uncommon(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
36        if let [b'\r', b'\n', rest @ ..] = input {
37            one_or_more(wsp)(rest)
38        } else {
39            input = one_or_more(wsp)(input)?.into_inner();
40            while let [b'\r', b'\n', rest @ ..] = input {
41                input = one_or_more(wsp)(rest)?.into_inner();
42            }
43            Some(ParsedItem(input, ()))
44        }
45    }
46    fws_uncommon(input)
47}
48
49/// Consume the `cfws` rule.
50// The full rule is equivalent to any combination of `fws` and `comment` so long as it is not empty.
51#[inline]
52pub(crate) fn cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
53    // Fast path for a single space followed by any ASCII character above the left parenthesis
54    // (which would start a comment).
55    if input.first() == Some(&b' ') && matches!(input.get(1), Some(0x29..)) {
56        return Some(ParsedItem(&input[1..], ()));
57    }
58
59    crate::hint::cold_path();
60
61    // Secondary fast path for whitespace other than a single space.
62    if input.first() != Some(&b'(') {
63        if !matches!(input.first(), Some(b'\r' | b' ' | b'\t')) {
64            return None;
65        }
66
67        if !matches!(input.get(1), Some(b'(' | b' ' | b'\r' | b'\n' | b'\t')) {
68            return Some(ParsedItem(&input[1..], ()));
69        }
70    }
71
72    #[inline(never)]
73    fn cfws_uncommon(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
74        one_or_more(|input| fws(input).or_else(|| comment(input, 1)))(input)
75    }
76    cfws_uncommon(input)
77}
78
79/// Optional `cfws` rule. This is equivalent to `opt(cfws)`, but is better optimized for the common
80/// case where no `cfws` is present.
81#[inline]
82pub(crate) fn opt_cfws(input: &[u8]) -> ParsedItem<'_, ()> {
83    if matches!(input.first(), Some(0x29..)) {
84        ParsedItem(input, ())
85    } else {
86        cfws(input).unwrap_or(ParsedItem(input, ()))
87    }
88}
89
90/// Equivalent to `opt(cfws)`, `ascii_char::<b':'>`, and `opt(cfws)` called in sequence, but is
91/// better optimized for the common case where no `cfws` is present.
92#[inline]
93pub(crate) fn opt_cfws_colon_opt_cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
94    if input.first() == Some(&b':') && matches!(input.get(1), Some(0x29..)) {
95        Some(ParsedItem(&input[1..], ()))
96    } else {
97        crate::hint::cold_path();
98        let input = opt_cfws(input).into_inner();
99        let input = ascii_char::<b':'>(input)?.into_inner();
100        let input = opt_cfws(input).into_inner();
101        Some(ParsedItem(input, ()))
102    }
103}
104
105/// Consume the `comment` rule.
106#[inline]
107fn comment(mut input: &[u8], depth: u8) -> Option<ParsedItem<'_, ()>> {
108    // Avoid stack exhaustion DoS by limiting recursion depth. This will cause highly-nested
109    // comments to fail parsing, but comments *at all* are incredibly rare in practice.
110    //
111    // The error from this will not be descriptive, but the rarity and near-certain maliciousness of
112    // such inputs makes this an acceptable trade-off.
113    if depth == DEPTH_LIMIT {
114        return None;
115    }
116
117    input = ascii_char::<b'('>(input)?.into_inner();
118    input = zero_or_more(fws)(input).into_inner();
119    while let Some(rest) = ccontent(input, depth + 1) {
120        input = rest.into_inner();
121        input = zero_or_more(fws)(input).into_inner();
122    }
123    input = ascii_char::<b')'>(input)?.into_inner();
124
125    Some(ParsedItem(input, ()))
126}
127
128/// Consume the `ccontent` rule.
129#[inline]
130fn ccontent(input: &[u8], depth: u8) -> Option<ParsedItem<'_, ()>> {
131    ctext(input)
132        .or_else(|| quoted_pair(input))
133        .or_else(|| comment(input, depth))
134}
135
136/// Consume the `ctext` rule.
137#[expect(
138    clippy::unnecessary_lazy_evaluations,
139    reason = "rust-lang/rust-clippy#8522"
140)]
141#[inline]
142fn ctext(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
143    no_ws_ctl(input).or_else(|| match input {
144        [33..=39 | 42..=91 | 93..=126, rest @ ..] => Some(ParsedItem(rest, ())),
145        _ => None,
146    })
147}
148
149/// Consume the `quoted_pair` rule.
150#[inline]
151fn quoted_pair(mut input: &[u8]) -> Option<ParsedItem<'_, ()>> {
152    input = ascii_char::<b'\\'>(input)?.into_inner();
153    input = text(input).into_inner();
154
155    // If nothing is parsed by `text`, this means by hit the `obs-text` rule and nothing matched.
156    // This is technically a success, and we used to check the `obs-qp` rule to ensure everything
157    // possible was consumed. After further analysis, it was determined that this check was
158    // unnecessary due to `obs-text` wholly subsuming `obs-qp` in this context. For this reason, if
159    // `text` fails to parse anything, we consider it a success without further consideration.
160
161    Some(ParsedItem(input, ()))
162}
163
164/// Consume the `no_ws_ctl` rule.
165#[inline]
166const fn no_ws_ctl(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
167    match input {
168        [1..=8 | 11..=12 | 14..=31 | 127, rest @ ..] => Some(ParsedItem(rest, ())),
169        _ => None,
170    }
171}
172
173/// Consume the `text` rule.
174#[inline]
175fn text<'a>(input: &'a [u8]) -> ParsedItem<'a, ()> {
176    let new_text = |input: &'a [u8]| match input {
177        [1..=9 | 11..=12 | 14..=127, rest @ ..] => Some(ParsedItem(rest, ())),
178        _ => None,
179    };
180
181    let obs_char = |input: &'a [u8]| match input {
182        // This is technically allowed, but consuming this would mean the rest of the string is
183        // eagerly consumed without consideration for where the comment actually ends.
184        [b')', ..] => None,
185        [0..=9 | 11..=12 | 14..=127, rest @ ..] => Some(rest),
186        _ => None,
187    };
188
189    let obs_text = |mut input| {
190        input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
191        input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
192        while let Some(rest) = obs_char(input) {
193            input = rest;
194            input = zero_or_more(ascii_char::<b'\n'>)(input).into_inner();
195            input = zero_or_more(ascii_char::<b'\r'>)(input).into_inner();
196        }
197
198        ParsedItem(input, ())
199    };
200
201    new_text(input).unwrap_or_else(|| obs_text(input))
202}
203
204/// Consume an old zone literal, returning the offset in hours.
205pub(crate) fn zone_literal(input: &[u8]) -> Option<ParsedItem<'_, i8>> {
206    let [first, second, third, rest @ ..] = input else {
207        const UT_VARIANTS: [u16; 4] = [
208            u16::from_ne_bytes(*b"ut"),
209            u16::from_ne_bytes(*b"uT"),
210            u16::from_ne_bytes(*b"Ut"),
211            u16::from_ne_bytes(*b"UT"),
212        ];
213
214        let [first, rest @ ..] = input else {
215            return None;
216        };
217        if let [second, rest @ ..] = rest
218            && UT_VARIANTS.contains(&u16::from_ne_bytes([*first, *second]))
219        {
220            return Some(ParsedItem(rest, 0));
221        }
222        return (*first != b'j' && *first != b'J' && first.is_ascii_alphabetic())
223            .then_some(ParsedItem(rest, 0));
224    };
225    let byte = u32::from_ne_bytes([
226        0,
227        first.to_ascii_lowercase(),
228        second.to_ascii_lowercase(),
229        third.to_ascii_lowercase(),
230    ]);
231    const ZONES: [u32; 8] = [
232        u32::from_ne_bytes([0, b'e', b's', b't']),
233        u32::from_ne_bytes([0, b'e', b'd', b't']),
234        u32::from_ne_bytes([0, b'c', b's', b't']),
235        u32::from_ne_bytes([0, b'c', b'd', b't']),
236        u32::from_ne_bytes([0, b'm', b's', b't']),
237        u32::from_ne_bytes([0, b'm', b'd', b't']),
238        u32::from_ne_bytes([0, b'p', b's', b't']),
239        u32::from_ne_bytes([0, b'p', b'd', b't']),
240    ];
241
242    let eq = [
243        if ZONES[0] == byte { i32::MAX } else { 0 },
244        if ZONES[1] == byte { i32::MAX } else { 0 },
245        if ZONES[2] == byte { i32::MAX } else { 0 },
246        if ZONES[3] == byte { i32::MAX } else { 0 },
247        if ZONES[4] == byte { i32::MAX } else { 0 },
248        if ZONES[5] == byte { i32::MAX } else { 0 },
249        if ZONES[6] == byte { i32::MAX } else { 0 },
250        if ZONES[7] == byte { i32::MAX } else { 0 },
251    ];
252    if eq == [0; 8] && byte != const { u32::from_ne_bytes([0, b'g', b'm', b't']) } {
253        return None;
254    }
255
256    let nonzero_zones = [
257        eq[0] & -5,
258        eq[1] & -4,
259        eq[2] & -6,
260        eq[3] & -5,
261        eq[4] & -7,
262        eq[5] & -6,
263        eq[6] & -8,
264        eq[7] & -7,
265    ];
266    let zone = nonzero_zones.iter().sum::<i32>().truncate();
267    Some(ParsedItem(rest, zone))
268}