time/parsing/combinator/rfc/
rfc2822.rs1use 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#[inline]
16pub(crate) fn fws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
17 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 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#[inline]
52pub(crate) fn cfws(input: &[u8]) -> Option<ParsedItem<'_, ()>> {
53 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 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#[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#[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#[inline]
107fn comment(mut input: &[u8], depth: u8) -> Option<ParsedItem<'_, ()>> {
108 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#[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#[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#[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 Some(ParsedItem(input, ()))
162}
163
164#[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#[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 [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
204pub(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}