1use super::{
8 Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
9 RR_TEMP_PERM_ERROR, SpfRecord, Variable,
10};
11use crate::DnsError;
12use crate::{
13 Error,
14 parse::{TagParser, TxtRecordParser, V},
15 scan::{find_ascii_whitespace, find_le_space_or2},
16};
17use std::{
18 net::{Ipv4Addr, Ipv6Addr},
19 slice::Iter,
20};
21
22impl TxtRecordParser for SpfRecord {
23 fn parse(bytes: &[u8]) -> crate::Result<SpfRecord> {
24 let mut record = bytes.iter();
25 if !matches!(record.key(), Some(k) if k == V)
26 || !record.match_bytes(b"spf1")
27 || record.next().is_some_and(|v| !v.is_ascii_whitespace())
28 {
29 return Err(Error::Dns(DnsError::InvalidRecordType));
30 }
31
32 let mut redirect = None;
33 let mut exp = None;
34 let mut ra = None;
35 let mut rp = 100;
36 let mut rr = u8::MAX;
37 let mut directives = Vec::new();
38
39 while let Some((term, qualifier, mut stop_char)) = record.next_term() {
40 match term {
41 A | MX => {
42 let mut ip4_cidr_length = 32;
43 let mut ip6_cidr_length = 128;
44 let mut macro_string = Macro::None;
45
46 match stop_char {
47 b' ' => (),
48 b':' | b'=' => {
49 let (ds, stop_char) = record.macro_string(false)?;
50 macro_string = ds;
51 if stop_char == b'/' {
52 let (l1, l2) = record.dual_cidr_length()?;
53 ip4_cidr_length = l1;
54 ip6_cidr_length = l2;
55 } else if stop_char != b' ' {
56 return Err(Error::Parse);
57 }
58 }
59 b'/' => {
60 let (l1, l2) = record.dual_cidr_length()?;
61 ip4_cidr_length = l1;
62 ip6_cidr_length = l2;
63 }
64 _ => return Err(Error::Parse),
65 }
66
67 directives.push(Directive::new(
68 qualifier,
69 if term == A {
70 Mechanism::A {
71 macro_string,
72 ip4_mask: ip4_mask(ip4_cidr_length),
73 ip6_mask: ip6_mask(ip6_cidr_length),
74 }
75 } else {
76 Mechanism::Mx {
77 macro_string,
78 ip4_mask: ip4_mask(ip4_cidr_length),
79 ip6_mask: ip6_mask(ip6_cidr_length),
80 }
81 },
82 ));
83 }
84 ALL => {
85 if stop_char == b' ' {
86 directives.push(Directive::new(qualifier, Mechanism::All))
87 } else {
88 return Err(Error::Parse);
89 }
90 }
91 INCLUDE | EXISTS => {
92 if stop_char != b':' {
93 return Err(Error::Parse);
94 }
95 let (macro_string, stop_char) = record.macro_string(false)?;
96 if stop_char == b' ' {
97 directives.push(Directive::new(
98 qualifier,
99 if term == INCLUDE {
100 Mechanism::Include { macro_string }
101 } else {
102 Mechanism::Exists { macro_string }
103 },
104 ));
105 } else {
106 return Err(Error::Parse);
107 }
108 }
109 IP4 => {
110 if stop_char != b':' {
111 return Err(Error::Parse);
112 }
113 let mut cidr_length = 32;
114 let (addr, stop_char) = record.ip4()?;
115 if stop_char == b'/' {
116 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
117 } else if stop_char != b' ' {
118 return Err(Error::Parse);
119 }
120 directives.push(Directive::new(
121 qualifier,
122 Mechanism::Ip4 {
123 addr,
124 mask: ip4_mask(cidr_length),
125 },
126 ));
127 }
128 IP6 => {
129 if stop_char != b':' {
130 return Err(Error::Parse);
131 }
132 let mut cidr_length = 128;
133 let (addr, stop_char) = record.ip6()?;
134 if stop_char == b'/' {
135 cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
136 } else if stop_char != b' ' {
137 return Err(Error::Parse);
138 }
139 directives.push(Directive::new(
140 qualifier,
141 Mechanism::Ip6 {
142 addr,
143 mask: ip6_mask(cidr_length),
144 },
145 ));
146 }
147 PTR => {
148 let mut macro_string = Macro::None;
149 if stop_char == b':' {
150 let (ds, stop_char_) = record.macro_string(false)?;
151 macro_string = ds;
152 stop_char = stop_char_;
153 }
154
155 if stop_char == b' ' {
156 directives.push(Directive::new(qualifier, Mechanism::Ptr { macro_string }));
157 } else {
158 return Err(Error::Parse);
159 }
160 }
161 EXP | REDIRECT => {
162 if stop_char != b'=' {
163 return Err(Error::Parse);
164 }
165 let (macro_string, stop_char) = record.macro_string(false)?;
166 if stop_char != b' ' {
167 return Err(Error::Parse);
168 }
169 if term == REDIRECT {
170 if redirect.is_none() {
171 redirect = macro_string.into()
172 } else {
173 return Err(Error::Parse);
174 }
175 } else if exp.is_none() {
176 exp = macro_string.into()
177 } else {
178 return Err(Error::Parse);
179 };
180 }
181 RA => {
182 let ra_ = record.ra()?;
183 if !ra_.is_empty() {
184 ra = ra_.into_boxed_slice().into();
185 }
186 }
187 RP => {
188 rp = std::cmp::min(record.cidr_length()?, 100);
189 }
190 RR => {
191 rr = record.rr()?;
192 }
193 _ => {
194 let (_, stop_char) = record.macro_string(false)?;
195 if stop_char != b' ' {
196 return Err(Error::Parse);
197 }
198 }
199 }
200 }
201
202 Ok(SpfRecord {
203 directives: directives.into_boxed_slice(),
204 redirect,
205 exp,
206 ra,
207 rp,
208 rr,
209 })
210 }
211}
212
213#[inline(always)]
214fn ip4_mask(cidr_length: u8) -> u32 {
215 if cidr_length == 0 {
216 0
217 } else {
218 u32::MAX << (32 - cidr_length)
219 }
220}
221
222#[inline(always)]
223fn ip6_mask(cidr_length: u8) -> u128 {
224 if cidr_length == 0 {
225 0
226 } else {
227 u128::MAX << (128 - cidr_length)
228 }
229}
230
231const TERM_POISON: u8 = 0;
232const TERM_SPACE: u8 = 1;
233const TERM_STOP: u8 = 2;
234
235const TERM_CLASS: [u8; 256] = {
236 let mut table = [TERM_POISON; 256];
237 let mut ch = 0usize;
238 while ch < 256 {
239 table[ch] = match ch as u8 {
240 b'a'..=b'z' => ch as u8,
241 b'A'..=b'Z' => (ch as u8) - b'A' + b'a',
242 b'4' => b'4',
243 b'6' => b'6',
244 b':' | b'=' | b'/' => TERM_STOP,
245 b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => TERM_SPACE,
246 _ => TERM_POISON,
247 };
248 ch += 1;
249 }
250 table
251};
252
253const A: u64 = b'a' as u64;
254const ALL: u64 = ((b'l' as u64) << 16) | ((b'l' as u64) << 8) | (b'a' as u64);
255const EXISTS: u64 = ((b's' as u64) << 40)
256 | ((b't' as u64) << 32)
257 | ((b's' as u64) << 24)
258 | ((b'i' as u64) << 16)
259 | ((b'x' as u64) << 8)
260 | (b'e' as u64);
261const EXP: u64 = ((b'p' as u64) << 16) | ((b'x' as u64) << 8) | (b'e' as u64);
262const INCLUDE: u64 = ((b'e' as u64) << 48)
263 | ((b'd' as u64) << 40)
264 | ((b'u' as u64) << 32)
265 | ((b'l' as u64) << 24)
266 | ((b'c' as u64) << 16)
267 | ((b'n' as u64) << 8)
268 | (b'i' as u64);
269const IP4: u64 = ((b'4' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
270const IP6: u64 = ((b'6' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
271const MX: u64 = ((b'x' as u64) << 8) | (b'm' as u64);
272const PTR: u64 = ((b'r' as u64) << 16) | ((b't' as u64) << 8) | (b'p' as u64);
273const REDIRECT: u64 = ((b't' as u64) << 56)
274 | ((b'c' as u64) << 48)
275 | ((b'e' as u64) << 40)
276 | ((b'r' as u64) << 32)
277 | ((b'i' as u64) << 24)
278 | ((b'd' as u64) << 16)
279 | ((b'e' as u64) << 8)
280 | (b'r' as u64);
281const RA: u64 = ((b'a' as u64) << 8) | (b'r' as u64);
282const RP: u64 = ((b'p' as u64) << 8) | (b'r' as u64);
283const RR: u64 = ((b'r' as u64) << 8) | (b'r' as u64);
284
285pub(crate) trait SPFParser: Sized {
286 fn next_term(&mut self) -> Option<(u64, Qualifier, u8)>;
287 fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
288 fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)>;
289 fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)>;
290 fn cidr_length(&mut self) -> crate::Result<u8>;
291 fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)>;
292 fn rr(&mut self) -> crate::Result<u8>;
293 fn ra(&mut self) -> crate::Result<Vec<u8>>;
294}
295
296impl SPFParser for Iter<'_, u8> {
297 fn next_term(&mut self) -> Option<(u64, Qualifier, u8)> {
298 let input = self.as_slice();
299 let mut pos = 0;
300 let mut qualifier = Qualifier::Pass;
301
302 while let Some(&ch) = input.get(pos) {
303 match ch {
304 b'+' => qualifier = Qualifier::Pass,
305 b'-' => qualifier = Qualifier::Fail,
306 b'~' => qualifier = Qualifier::SoftFail,
307 b'?' => qualifier = Qualifier::Neutral,
308 b'\t' | b'\n' | b'\x0C' | b'\r' | b' ' => (),
309 _ => break,
310 }
311 pos += 1;
312 }
313
314 let mut d = 0u64;
315 let mut shift = 0;
316 let mut stop_char = b' ';
317
318 while let Some(&ch) = input.get(pos) {
319 pos += 1;
320 match TERM_CLASS[ch as usize] {
321 TERM_POISON => {
322 d = u64::MAX;
323 shift = 64;
324 }
325 TERM_SPACE => {
326 stop_char = b' ';
327 break;
328 }
329 TERM_STOP => {
330 stop_char = ch;
331 break;
332 }
333 lower if shift < 64 => {
334 d |= (lower as u64) << shift;
335 shift += 8;
336 }
337 _ => {
338 d = u64::MAX;
339 shift = 64;
340 }
341 }
342 }
343
344 *self = input.get(pos..).unwrap_or_default().iter();
345
346 if d != 0 {
347 (d, qualifier, stop_char).into()
348 } else {
349 None
350 }
351 }
352
353 fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
354 let input = self.as_slice();
355 let mut pos = 0;
356
357 loop {
358 let rest = input.get(pos..).unwrap_or_default();
359 let hit = pos
360 + if is_exp {
361 memchr::memchr(b'%', rest).unwrap_or(rest.len())
362 } else {
363 find_le_space_or2(rest, b'%', b'/')
364 };
365
366 match input.get(hit) {
367 None => {
368 *self = [].iter();
369 return literal_macro(input, b' ');
370 }
371 Some(&b'%') => break,
372 Some(&ch) if !is_exp && (ch == b'/' || ch.is_ascii_whitespace()) => {
373 *self = input.get(hit + 1..).unwrap_or_default().iter();
374 return literal_macro(
375 input.get(..hit).unwrap_or_default(),
376 if ch == b'/' { b'/' } else { b' ' },
377 );
378 }
379 Some(_) => pos = hit + 1,
380 }
381 }
382
383 self.macro_string_slow(is_exp)
384 }
385
386 fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)> {
387 let input = self.as_slice();
388 let mut pos = 0;
389 let mut stop_char = b' ';
390 let mut ip = [0u8; 4];
391 let mut octet = 0u8;
392 let mut group = 0usize;
393
394 while let Some(&ch) = input.get(pos) {
395 pos += 1;
396 match ch {
397 b'0'..=b'9' => {
398 octet = octet.saturating_mul(10).saturating_add(ch - b'0');
399 }
400 b'.' if group < 3 => {
401 ip[group] = octet;
402 octet = 0;
403 group += 1;
404 }
405 _ => {
406 stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
407 break;
408 }
409 }
410 }
411
412 *self = input.get(pos..).unwrap_or_default().iter();
413
414 if group == 3 {
415 let [a, b, c, _] = ip;
416 Ok((Ipv4Addr::new(a, b, c, octet), stop_char))
417 } else {
418 Err(Error::Parse)
419 }
420 }
421
422 fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)> {
423 let input = self.as_slice();
424 let (result, pos) = parse_ip6(input);
425 *self = input.get(pos..).unwrap_or_default().iter();
426 result
427 }
428
429 fn cidr_length(&mut self) -> crate::Result<u8> {
430 let input = self.as_slice();
431 let mut pos = 0;
432 let mut cidr_length = 0u8;
433
434 while let Some(&ch) = input.get(pos) {
435 pos += 1;
436 match ch {
437 b'0'..=b'9' => {
438 cidr_length = cidr_length.saturating_mul(10).saturating_add(ch - b'0');
439 }
440 _ => {
441 if !ch.is_ascii_whitespace() {
442 *self = input.get(pos..).unwrap_or_default().iter();
443 return Err(Error::Parse);
444 }
445 break;
446 }
447 }
448 }
449
450 *self = input.get(pos..).unwrap_or_default().iter();
451 Ok(cidr_length)
452 }
453
454 fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)> {
455 let input = self.as_slice();
456 let mut pos = 0;
457 let mut ip4_length = u8::MAX;
458 let mut ip6_length = u8::MAX;
459 let mut in_ip6 = false;
460
461 while let Some(&ch) = input.get(pos) {
462 pos += 1;
463 match ch {
464 b'0'..=b'9' => {
465 let digit = ch - b'0';
466 let length = if in_ip6 {
467 &mut ip6_length
468 } else {
469 &mut ip4_length
470 };
471 *length = if *length != u8::MAX {
472 length.saturating_mul(10).saturating_add(digit)
473 } else {
474 digit
475 };
476 }
477 b'/' => {
478 if !in_ip6 {
479 in_ip6 = true;
480 } else if ip6_length != u8::MAX {
481 *self = input.get(pos..).unwrap_or_default().iter();
482 return Err(Error::Parse);
483 }
484 }
485 _ => {
486 if !ch.is_ascii_whitespace() {
487 *self = input.get(pos..).unwrap_or_default().iter();
488 return Err(Error::Parse);
489 }
490 break;
491 }
492 }
493 }
494
495 *self = input.get(pos..).unwrap_or_default().iter();
496 Ok((
497 std::cmp::min(ip4_length, 32),
498 std::cmp::min(ip6_length, 128),
499 ))
500 }
501
502 fn rr(&mut self) -> crate::Result<u8> {
503 let mut flags: u8 = 0;
504
505 'outer: while let Some(&ch) = self.next() {
506 match ch {
507 b'a' | b'A' => {
508 for _ in 0..2 {
509 match self.next().unwrap_or(&0) {
510 b'l' | b'L' => {}
511 b' ' | b'\t' => {
512 return Ok(flags);
513 }
514 _ => {
515 continue 'outer;
516 }
517 }
518 }
519 flags = u8::MAX;
520 }
521 b'e' | b'E' => {
522 flags |= RR_TEMP_PERM_ERROR;
523 }
524 b'f' | b'F' => {
525 flags |= RR_FAIL;
526 }
527 b's' | b'S' => {
528 flags |= RR_SOFTFAIL;
529 }
530 b'n' | b'N' => {
531 flags |= RR_NEUTRAL_NONE;
532 }
533 b':' => {}
534 _ => {
535 if ch.is_ascii_whitespace() {
536 break;
537 } else if !ch.is_ascii_alphanumeric() {
538 return Err(Error::Parse);
539 }
540 }
541 }
542 }
543
544 Ok(flags)
545 }
546
547 fn ra(&mut self) -> crate::Result<Vec<u8>> {
548 let input = self.as_slice();
549 let end = find_ascii_whitespace(input);
550 let ra = input.get(..end).unwrap_or_default().to_vec();
551 *self = input.get(end + 1..).unwrap_or_default().iter();
552 Ok(ra)
553 }
554}
555
556fn parse_ip6(input: &[u8]) -> (crate::Result<(Ipv6Addr, u8)>, usize) {
557 let mut pos = 0;
558 let mut stop_char = b' ';
559 let mut ip = [0u16; 8];
560 let mut ip_pos = 0;
561 let mut ip4_pos = 0;
562 let mut part = Ip6Part::default();
563 let mut zero_group_pos = usize::MAX;
564
565 while let Some(&ch) = input.get(pos) {
566 pos += 1;
567 match ch {
568 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
569 if !part.push(ch) {
570 return (Err(Error::Parse), pos);
571 }
572 }
573 b':' => {
574 if ip_pos < 8 {
575 if !part.is_empty() {
576 ip[ip_pos] = part.take_hex();
577 ip_pos += 1;
578 } else if zero_group_pos == usize::MAX {
579 zero_group_pos = ip_pos;
580 } else if zero_group_pos != ip_pos {
581 return (Err(Error::Parse), pos);
582 }
583 } else {
584 return (Err(Error::Parse), pos);
585 }
586 }
587 b'.' => {
588 if ip_pos < 8 && !part.is_empty() {
589 let Some(qnum) = part.take_octet() else {
590 return (Err(Error::Parse), pos);
591 };
592 if ip4_pos % 2 == 1 {
593 ip[ip_pos] = (ip[ip_pos] << 8) | qnum;
594 ip_pos += 1;
595 } else {
596 ip[ip_pos] = qnum;
597 }
598 ip4_pos += 1;
599 } else {
600 return (Err(Error::Parse), pos);
601 }
602 }
603 _ => {
604 stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
605 break;
606 }
607 }
608 }
609
610 if !part.is_empty() {
611 if ip_pos < 8 {
612 ip[ip_pos] = if ip4_pos == 0 {
613 part.take_hex()
614 } else if ip4_pos == 3 {
615 match part.take_octet() {
616 Some(qnum) => (ip[ip_pos] << 8) | qnum,
617 None => return (Err(Error::Parse), pos),
618 }
619 } else {
620 return (Err(Error::Parse), pos);
621 };
622
623 ip_pos += 1;
624 } else {
625 return (Err(Error::Parse), pos);
626 }
627 }
628 if zero_group_pos != usize::MAX && zero_group_pos < ip_pos {
629 if ip_pos <= 7 {
630 ip.copy_within(zero_group_pos..ip_pos, zero_group_pos + 8 - ip_pos);
631 ip[zero_group_pos..zero_group_pos + 8 - ip_pos].fill(0);
632 } else {
633 return (Err(Error::Parse), pos);
634 }
635 }
636
637 if ip_pos != 0 || zero_group_pos != usize::MAX {
638 let [a, b, c, d, e, f, g, h] = ip;
639 (Ok((Ipv6Addr::new(a, b, c, d, e, f, g, h), stop_char)), pos)
640 } else {
641 (Err(Error::Parse), pos)
642 }
643}
644
645struct Ip6Part {
646 hex: u16,
647 decimal: u16,
648 len: u8,
649 is_decimal: bool,
650}
651
652impl Default for Ip6Part {
653 fn default() -> Self {
654 Ip6Part {
655 hex: 0,
656 decimal: 0,
657 len: 0,
658 is_decimal: true,
659 }
660 }
661}
662
663impl Ip6Part {
664 #[inline(always)]
665 fn push(&mut self, ch: u8) -> bool {
666 if self.len < 4 {
667 self.len += 1;
668 self.hex = (self.hex << 4) | HEX_NIBBLE[ch as usize] as u16;
669 if ch.is_ascii_digit() {
670 self.decimal = self.decimal * 10 + (ch - b'0') as u16;
671 } else {
672 self.is_decimal = false;
673 }
674 true
675 } else {
676 false
677 }
678 }
679
680 #[inline(always)]
681 fn is_empty(&self) -> bool {
682 self.len == 0
683 }
684
685 #[inline(always)]
686 fn take_hex(&mut self) -> u16 {
687 let value = self.hex;
688 *self = Ip6Part::default();
689 value
690 }
691
692 #[inline(always)]
693 fn take_octet(&mut self) -> Option<u16> {
694 let value = (self.is_decimal && self.decimal <= 255).then_some(self.decimal);
695 *self = Ip6Part::default();
696 value
697 }
698}
699
700const HEX_NIBBLE: [u8; 256] = {
701 let mut table = [0u8; 256];
702 let mut ch = 0usize;
703 while ch < 256 {
704 table[ch] = match ch as u8 {
705 b'0'..=b'9' => (ch as u8) - b'0',
706 b'a'..=b'f' => (ch as u8) - b'a' + 10,
707 b'A'..=b'F' => (ch as u8) - b'A' + 10,
708 _ => 0,
709 };
710 ch += 1;
711 }
712 table
713};
714
715#[inline(always)]
716fn literal_macro(literal: &[u8], stop_char: u8) -> crate::Result<(Macro, u8)> {
717 if literal.is_empty() {
718 Err(Error::Parse)
719 } else {
720 Ok((Macro::Literal(literal.into()), stop_char))
721 }
722}
723
724trait SPFMacroParser {
725 fn macro_string_slow(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
726}
727
728impl SPFMacroParser for Iter<'_, u8> {
729 #[inline(never)]
730 #[allow(clippy::while_let_on_iterator)]
731 fn macro_string_slow(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
732 let mut stop_char = b' ';
733 let mut last_is_pct = false;
734 let mut literal = Vec::with_capacity(16);
735 let mut macro_string = Vec::new();
736
737 while let Some(&ch) = self.next() {
738 match ch {
739 b'%' => {
740 if last_is_pct {
741 literal.push(b'%');
742 } else {
743 last_is_pct = true;
744 continue;
745 }
746 }
747 b'_' if last_is_pct => {
748 literal.push(b' ');
749 }
750 b'-' if last_is_pct => {
751 literal.extend_from_slice(b"%20");
752 }
753 b'{' if last_is_pct => {
754 if !literal.is_empty() {
755 macro_string.push(Macro::Literal(literal.as_slice().into()));
756 literal.clear();
757 }
758
759 let (letter, escape) = self
760 .next()
761 .copied()
762 .and_then(|l| {
763 if !is_exp {
764 Variable::parse(l)
765 } else {
766 Variable::parse_exp(l)
767 }
768 })
769 .ok_or(Error::Parse)?;
770 let mut num_parts: u32 = 0;
771 let mut reverse = false;
772 let mut delimiters = 0;
773
774 while let Some(&ch) = self.next() {
775 match ch {
776 b'0'..=b'9' => {
777 num_parts = num_parts
778 .saturating_mul(10)
779 .saturating_add((ch - b'0') as u32);
780 }
781 b'r' | b'R' => {
782 reverse = true;
783 }
784 b'}' => {
785 break;
786 }
787 b'.' | b'-' | b'+' | b',' | b'/' | b'_' | b'=' => {
788 delimiters |= 1u64 << (ch - b'+');
789 }
790 _ => {
791 return Err(Error::Parse);
792 }
793 }
794 }
795
796 if delimiters == 0 {
797 delimiters = 1u64 << (b'.' - b'+');
798 }
799
800 macro_string.push(Macro::Variable {
801 letter,
802 num_parts,
803 reverse,
804 escape,
805 delimiters,
806 });
807 }
808 b'/' if !is_exp => {
809 stop_char = ch;
810 break;
811 }
812 _ => {
813 if last_is_pct {
814 return Err(Error::Parse);
815 } else if !ch.is_ascii_whitespace() || is_exp {
816 literal.push(ch);
817 } else {
818 break;
819 }
820 }
821 }
822
823 last_is_pct = false;
824 }
825
826 if !literal.is_empty() {
827 macro_string.push(Macro::Literal(literal.into_boxed_slice()));
828 }
829
830 match macro_string.len() {
831 1 => macro_string
832 .pop()
833 .map(|m| (m, stop_char))
834 .ok_or(Error::Parse),
835 0 => Err(Error::Parse),
836 _ => Ok((Macro::List(macro_string.into_boxed_slice()), stop_char)),
837 }
838 }
839}
840
841impl Variable {
842 pub(crate) fn parse(ch: u8) -> Option<(Self, bool)> {
843 match ch {
844 b's' => (Variable::Sender, false),
845 b'l' => (Variable::SenderLocalPart, false),
846 b'o' => (Variable::SenderDomainPart, false),
847 b'd' => (Variable::Domain, false),
848 b'i' => (Variable::Ip, false),
849 b'p' => (Variable::ValidatedDomain, false),
850 b'v' => (Variable::IpVersion, false),
851 b'h' => (Variable::HeloDomain, false),
852
853 b'S' => (Variable::Sender, true),
854 b'L' => (Variable::SenderLocalPart, true),
855 b'O' => (Variable::SenderDomainPart, true),
856 b'D' => (Variable::Domain, true),
857 b'I' => (Variable::Ip, true),
858 b'P' => (Variable::ValidatedDomain, true),
859 b'V' => (Variable::IpVersion, true),
860 b'H' => (Variable::HeloDomain, true),
861 _ => return None,
862 }
863 .into()
864 }
865
866 pub(crate) fn parse_exp(ch: u8) -> Option<(Self, bool)> {
867 match ch {
868 b's' => (Variable::Sender, false),
869 b'l' => (Variable::SenderLocalPart, false),
870 b'o' => (Variable::SenderDomainPart, false),
871 b'd' => (Variable::Domain, false),
872 b'i' => (Variable::Ip, false),
873 b'p' => (Variable::ValidatedDomain, false),
874 b'v' => (Variable::IpVersion, false),
875 b'h' => (Variable::HeloDomain, false),
876 b'c' => (Variable::SmtpIp, false),
877 b'r' => (Variable::HostDomain, false),
878 b't' => (Variable::CurrentTime, false),
879
880 b'S' => (Variable::Sender, true),
881 b'L' => (Variable::SenderLocalPart, true),
882 b'O' => (Variable::SenderDomainPart, true),
883 b'D' => (Variable::Domain, true),
884 b'I' => (Variable::Ip, true),
885 b'P' => (Variable::ValidatedDomain, true),
886 b'V' => (Variable::IpVersion, true),
887 b'H' => (Variable::HeloDomain, true),
888 b'C' => (Variable::SmtpIp, true),
889 b'R' => (Variable::HostDomain, true),
890 b'T' => (Variable::CurrentTime, true),
891 _ => return None,
892 }
893 .into()
894 }
895}
896
897impl TxtRecordParser for Macro {
898 fn parse(record: &[u8]) -> crate::Result<Self> {
899 record.iter().macro_string(true).map(|(m, _)| m)
900 }
901}
902
903#[cfg(test)]
904mod tests;