Skip to main content

mail_auth/common/
parse.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use mail_parser::decoders::{
8    base64::base64_decode_slice, quoted_printable::quoted_printable_decode_char,
9};
10use memchr::{memchr, memchr_iter};
11use std::{borrow::Cow, slice::Iter};
12
13const MAX_ITEMS: usize = 32;
14
15pub(crate) const V: u64 = b'v' as u64;
16pub(crate) const A: u64 = b'a' as u64;
17pub(crate) const B: u64 = b'b' as u64;
18pub(crate) const BH: u64 = (b'b' as u64) | ((b'h' as u64) << 8);
19pub(crate) const C: u64 = b'c' as u64;
20pub(crate) const D: u64 = b'd' as u64;
21pub(crate) const H: u64 = b'h' as u64;
22pub(crate) const I: u64 = b'i' as u64;
23pub(crate) const K: u64 = b'k' as u64;
24pub(crate) const L: u64 = b'l' as u64;
25pub(crate) const N: u64 = b'n' as u64;
26pub(crate) const O: u64 = b'o' as u64;
27pub(crate) const P: u64 = b'p' as u64;
28pub(crate) const R: u64 = b'r' as u64;
29pub(crate) const S: u64 = b's' as u64;
30pub(crate) const T: u64 = b't' as u64;
31pub(crate) const U: u64 = b'u' as u64;
32pub(crate) const X: u64 = b'x' as u64;
33pub(crate) const Y: u64 = b'y' as u64;
34pub(crate) const Z: u64 = b'z' as u64;
35
36pub trait TxtRecordParser: Sized {
37    fn parse(record: &[u8]) -> crate::Result<Self>;
38}
39
40pub(crate) trait TagParser: Sized {
41    fn match_bytes(&mut self, bytes: &[u8]) -> bool;
42    fn key(&mut self) -> Option<u64>;
43    fn value(&mut self) -> u64;
44    fn text(&mut self, to_lower: bool) -> String;
45    fn text_qp(&mut self, base: Vec<u8>, to_lower: bool, stop_comma: bool) -> String;
46    fn headers_qp<T: ItemParser>(&mut self) -> Vec<T>;
47    fn number(&mut self) -> Option<u64>;
48    fn items<T: ItemParser>(&mut self) -> Vec<T>;
49    fn flag_value(&mut self) -> (u64, u8);
50    fn flags<T: ItemParser + Into<u64>>(&mut self) -> u64;
51    fn ignore(&mut self);
52    fn base64(&mut self) -> Option<Vec<u8>>;
53    fn seek_tag_end(&mut self) -> bool;
54    fn next_skip_whitespaces(&mut self) -> Option<u8>;
55}
56
57pub(crate) trait ItemParser: Sized {
58    fn parse(bytes: &[u8]) -> Option<Self>;
59}
60
61#[inline(always)]
62fn split_tag_value(slice: &[u8]) -> (&[u8], &[u8]) {
63    match memchr(b';', slice) {
64        Some(pos) => (
65            slice.get(..pos).unwrap_or(slice),
66            slice.get(pos + 1..).unwrap_or_default(),
67        ),
68        None => (slice, &[]),
69    }
70}
71
72#[inline(always)]
73fn is_text_stop(ch: u8, to_lower: bool) -> bool {
74    ch.is_ascii_whitespace() || (to_lower && (ch.is_ascii_uppercase() || ch >= 0x7f))
75}
76
77#[inline(always)]
78fn is_qp_stop(ch: u8, stop_comma: bool) -> bool {
79    ch == b'=' || ch == b';' || ch.is_ascii_whitespace() || (stop_comma && ch == b',')
80}
81
82#[inline(always)]
83fn is_header_stop(ch: u8) -> bool {
84    ch == b'=' || ch == b'|' || ch == b';' || ch.is_ascii_whitespace()
85}
86
87#[inline(always)]
88fn slice_to_string(value: &[u8]) -> String {
89    match std::str::from_utf8(value) {
90        Ok(value) => value.to_string(),
91        Err(_) => String::from_utf8_lossy(value).into_owned(),
92    }
93}
94
95#[inline(always)]
96fn vec_to_string(tag: Vec<u8>) -> String {
97    String::from_utf8(tag)
98        .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned())
99}
100
101#[inline(always)]
102fn parse_item<T: ItemParser>(item: &[u8], scratch: &mut Vec<u8>) -> Option<T> {
103    match item.iter().position(|&ch| ch.is_ascii_whitespace()) {
104        None => {
105            if !item.is_empty() {
106                T::parse(item)
107            } else {
108                None
109            }
110        }
111        Some(pos) => {
112            let (head, tail) = item.split_at_checked(pos).unwrap_or((item, &[]));
113            scratch.clear();
114            scratch.reserve(item.len());
115            scratch.extend_from_slice(head);
116            for &ch in tail {
117                if !ch.is_ascii_whitespace() {
118                    scratch.push(ch);
119                }
120            }
121            if !scratch.is_empty() {
122                T::parse(scratch)
123            } else {
124                None
125            }
126        }
127    }
128}
129
130#[inline(never)]
131fn text_value(value: &[u8], pos: usize, to_lower: bool) -> String {
132    let Some((head, mut rest)) = value.split_at_checked(pos) else {
133        return slice_to_string(value);
134    };
135    let mut tag = Vec::with_capacity(value.len());
136    let mut has_high = false;
137    tag.extend_from_slice(head);
138
139    while let Some((&ch, next)) = rest.split_first() {
140        if !is_text_stop(ch, to_lower) {
141            let end = rest
142                .iter()
143                .position(|&ch| is_text_stop(ch, to_lower))
144                .unwrap_or(rest.len());
145            let Some((run, tail)) = rest.split_at_checked(end) else {
146                break;
147            };
148            tag.extend_from_slice(run);
149            rest = tail;
150            continue;
151        }
152
153        rest = next;
154        if ch.is_ascii_whitespace() {
155        } else if ch.is_ascii_uppercase() {
156            tag.push(ch + 32);
157        } else {
158            has_high = true;
159            tag.push(ch);
160        }
161    }
162
163    if to_lower && has_high {
164        String::from_utf8_lossy(&tag).to_lowercase()
165    } else {
166        vec_to_string(tag)
167    }
168}
169
170#[inline(always)]
171fn push_item<T: ItemParser>(tag: &mut Vec<u8>, tags: &mut Vec<T>) {
172    if !tag.is_empty() {
173        if let Some(parsed) = T::parse(tag) {
174            tags.push(parsed);
175        }
176        tag.clear();
177    }
178}
179
180impl TagParser for Iter<'_, u8> {
181    #[allow(clippy::while_let_on_iterator)]
182    fn key(&mut self) -> Option<u64> {
183        let mut key: u64 = 0;
184        let mut shift = 0;
185
186        while let Some(&ch) = self.next() {
187            match ch {
188                b'a'..=b'z' if shift < 64 => {
189                    key |= (ch as u64) << shift;
190                    shift += 8;
191                }
192                b' ' | b'\t' | b'\r' | b'\n' => (),
193                b'=' => {
194                    return key.into();
195                }
196                b'A'..=b'Z' if shift < 64 => {
197                    key |= ((ch - b'A' + b'a') as u64) << shift;
198                    shift += 8;
199                }
200                b';' => {
201                    key = 0;
202                }
203                _ => {
204                    key = u64::MAX;
205                    shift = 64;
206                }
207            }
208        }
209
210        None
211    }
212
213    #[allow(clippy::while_let_on_iterator)]
214    fn value(&mut self) -> u64 {
215        let mut value: u64 = 0;
216        let mut shift = 0;
217
218        while let Some(&ch) = self.next() {
219            match ch {
220                b'a'..=b'z' | b'0'..=b'9' if shift < 64 => {
221                    value |= (ch as u64) << shift;
222                    shift += 8;
223                }
224                b' ' | b'\t' | b'\r' | b'\n' => (),
225                b'A'..=b'Z' if shift < 64 => {
226                    value |= ((ch - b'A' + b'a') as u64) << shift;
227                    shift += 8;
228                }
229                b';' => {
230                    break;
231                }
232                _ => {
233                    value = u64::MAX;
234                    shift = 64;
235                }
236            }
237        }
238
239        value
240    }
241
242    #[allow(clippy::while_let_on_iterator)]
243    fn flag_value(&mut self) -> (u64, u8) {
244        let mut value: u64 = 0;
245        let mut shift = 0;
246
247        while let Some(&ch) = self.next() {
248            match ch {
249                b'a'..=b'z' | b'0'..=b'9' if shift < 64 => {
250                    value |= (ch as u64) << shift;
251                    shift += 8;
252                }
253                b' ' | b'\t' | b'\r' | b'\n' => (),
254                b'A'..=b'Z' if shift < 64 => {
255                    value |= ((ch - b'A' + b'a') as u64) << shift;
256                    shift += 8;
257                }
258                b';' | b':' => {
259                    return (value, ch);
260                }
261                _ => {
262                    value = u64::MAX;
263                    shift = 64;
264                }
265            }
266        }
267
268        (value, 0)
269    }
270
271    #[inline(always)]
272    #[allow(clippy::while_let_on_iterator)]
273    fn match_bytes(&mut self, bytes: &[u8]) -> bool {
274        let slice = self.as_slice();
275
276        if let Some(head) = slice.get(..bytes.len())
277            && head
278                .iter()
279                .zip(bytes)
280                .all(|(ch, byte)| ch.eq_ignore_ascii_case(byte) && !ch.is_ascii_whitespace())
281        {
282            *self = slice.get(bytes.len()..).unwrap_or_default().iter();
283            return true;
284        }
285
286        'outer: for byte in bytes {
287            while let Some(&ch) = self.next() {
288                if !ch.is_ascii_whitespace() {
289                    if ch.eq_ignore_ascii_case(byte) {
290                        continue 'outer;
291                    } else {
292                        return false;
293                    }
294                }
295            }
296            return false;
297        }
298
299        true
300    }
301
302    #[inline(always)]
303    fn text(&mut self, to_lower: bool) -> String {
304        let slice = self.as_slice();
305        let (value, tail) = split_tag_value(slice);
306        *self = tail.iter();
307
308        match value.iter().position(|&ch| is_text_stop(ch, to_lower)) {
309            Some(pos) => text_value(value, pos, to_lower),
310            None => slice_to_string(value),
311        }
312    }
313
314    #[inline(always)]
315    fn text_qp(&mut self, mut tag: Vec<u8>, to_lower: bool, stop_comma: bool) -> String {
316        let mut rest = self.as_slice();
317
318        'outer: loop {
319            let Some(pos) = rest.iter().position(|&ch| is_qp_stop(ch, stop_comma)) else {
320                tag.extend_from_slice(rest);
321                rest = &[];
322                break;
323            };
324            let Some((head, next)) = rest.split_at_checked(pos) else {
325                break;
326            };
327            tag.extend_from_slice(head);
328            let Some((&ch, mut next)) = next.split_first() else {
329                break;
330            };
331
332            if ch == b';' || ch == b',' {
333                rest = next;
334                break;
335            } else if ch == b'=' {
336                let mut hex1 = 0;
337
338                while let Some((&ch, tail)) = next.split_first() {
339                    next = tail;
340                    if ch.is_ascii_hexdigit() {
341                        if hex1 != 0 {
342                            if let Some(ch) = quoted_printable_decode_char(hex1, ch) {
343                                tag.push(ch);
344                            }
345                            break;
346                        } else {
347                            hex1 = ch;
348                        }
349                    } else if ch == b';' {
350                        rest = next;
351                        break 'outer;
352                    } else if !ch.is_ascii_whitespace() {
353                        break;
354                    }
355                }
356            }
357
358            rest = next;
359        }
360
361        *self = rest.iter();
362
363        if !to_lower {
364            vec_to_string(tag)
365        } else if tag.is_ascii() {
366            tag.make_ascii_lowercase();
367            vec_to_string(tag)
368        } else {
369            String::from_utf8_lossy(&tag).to_lowercase()
370        }
371    }
372
373    #[inline(always)]
374    fn headers_qp<T: ItemParser>(&mut self) -> Vec<T> {
375        let mut tags = Vec::new();
376        let mut tag = Vec::with_capacity(20);
377        let mut rest = self.as_slice();
378
379        'outer: loop {
380            let Some(pos) = rest.iter().position(|&ch| is_header_stop(ch)) else {
381                tag.extend_from_slice(rest);
382                rest = &[];
383                break;
384            };
385            let Some((head, next)) = rest.split_at_checked(pos) else {
386                break;
387            };
388            tag.extend_from_slice(head);
389            let Some((&ch, mut next)) = next.split_first() else {
390                break;
391            };
392
393            if ch == b';' {
394                rest = next;
395                break;
396            } else if ch == b'|' {
397                push_item(&mut tag, &mut tags);
398            } else if ch == b'=' {
399                let mut hex1 = 0;
400
401                while let Some((&ch, tail)) = next.split_first() {
402                    next = tail;
403                    if ch.is_ascii_hexdigit() {
404                        if hex1 != 0 {
405                            if let Some(ch) = quoted_printable_decode_char(hex1, ch) {
406                                tag.push(ch);
407                            }
408                            break;
409                        } else {
410                            hex1 = ch;
411                        }
412                    } else if ch == b'|' {
413                        push_item(&mut tag, &mut tags);
414                        break;
415                    } else if ch == b';' {
416                        rest = next;
417                        break 'outer;
418                    } else if !ch.is_ascii_whitespace() {
419                        break;
420                    }
421                }
422            }
423
424            rest = next;
425        }
426
427        *self = rest.iter();
428
429        if !tag.is_empty()
430            && let Some(tag) = T::parse(&tag)
431        {
432            tags.push(tag);
433        }
434
435        tags
436    }
437
438    #[inline(always)]
439    fn number(&mut self) -> Option<u64> {
440        let mut num: u64 = 0;
441        let mut has_digits = false;
442
443        for &ch in &mut *self {
444            if ch == b';' {
445                break;
446            } else if ch.is_ascii_digit() {
447                num = (num.saturating_mul(10)).saturating_add((ch - b'0') as u64);
448                has_digits = true;
449            } else if !ch.is_ascii_whitespace() {
450                return None;
451            }
452        }
453
454        if has_digits { num.into() } else { None }
455    }
456
457    #[inline(always)]
458    fn ignore(&mut self) {
459        let (_, tail) = split_tag_value(self.as_slice());
460        *self = tail.iter();
461    }
462
463    #[inline(always)]
464    fn base64(&mut self) -> Option<Vec<u8>> {
465        let slice = self.as_slice();
466        match base64_decode_slice(slice, b';') {
467            Some((decoded, consumed)) => {
468                *self = slice.get(consumed..).unwrap_or_default().iter();
469                Some(decoded)
470            }
471            None => {
472                self.ignore();
473                None
474            }
475        }
476    }
477
478    #[inline(always)]
479    fn seek_tag_end(&mut self) -> bool {
480        for &ch in &mut *self {
481            if ch == b';' {
482                return true;
483            } else if !ch.is_ascii_whitespace() {
484                return false;
485            }
486        }
487
488        true
489    }
490
491    #[inline(always)]
492    fn next_skip_whitespaces(&mut self) -> Option<u8> {
493        for &ch in &mut *self {
494            if !ch.is_ascii_whitespace() {
495                return ch.into();
496            }
497        }
498
499        None
500    }
501
502    fn items<T: ItemParser>(&mut self) -> Vec<T> {
503        let (value, tail) = split_tag_value(self.as_slice());
504        *self = tail.iter();
505
506        if value.is_empty() {
507            return Vec::new();
508        }
509
510        let mut items = Vec::with_capacity(memchr_iter(b':', value).count().min(MAX_ITEMS) + 1);
511        let mut scratch = Vec::new();
512
513        for item in value.split(|&ch| ch == b':') {
514            if let Some(item) = parse_item(item, &mut scratch) {
515                items.push(item);
516            }
517        }
518
519        items
520    }
521
522    fn flags<T: ItemParser + Into<u64>>(&mut self) -> u64 {
523        let (value, tail) = split_tag_value(self.as_slice());
524        *self = tail.iter();
525
526        let mut flags = 0;
527        let mut scratch = Vec::new();
528
529        for item in value.split(|&ch| ch == b':') {
530            if let Some(item) = parse_item::<T>(item, &mut scratch) {
531                flags |= item.into();
532            }
533        }
534
535        flags
536    }
537}
538
539impl ItemParser for Vec<u8> {
540    fn parse(bytes: &[u8]) -> Option<Self> {
541        Some(bytes.to_vec())
542    }
543}
544
545impl ItemParser for Box<[u8]> {
546    fn parse(bytes: &[u8]) -> Option<Self> {
547        Some(bytes.into())
548    }
549}
550
551impl ItemParser for Box<str> {
552    fn parse(bytes: &[u8]) -> Option<Self> {
553        Some(std::str::from_utf8(bytes).ok()?.into())
554    }
555}
556
557impl ItemParser for String {
558    fn parse(bytes: &[u8]) -> Option<Self> {
559        Some(String::from_utf8_lossy(bytes).into_owned())
560    }
561}
562
563impl ItemParser for Cow<'_, str> {
564    fn parse(bytes: &[u8]) -> Option<Self> {
565        Some(
566            std::str::from_utf8(bytes)
567                .unwrap_or_default()
568                .to_string()
569                .into(),
570        )
571    }
572}