mail_auth/common/
parse.rs1use 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}