Skip to main content

mail_auth/spf/
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 super::{
8    Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
9    RR_TEMP_PERM_ERROR, Spf, Variable,
10};
11use crate::{
12    Error, Version,
13    common::parse::{TagParser, TxtRecordParser, V},
14};
15use std::{
16    net::{Ipv4Addr, Ipv6Addr},
17    slice::Iter,
18};
19
20impl TxtRecordParser for Spf {
21    fn parse(bytes: &[u8]) -> crate::Result<Spf> {
22        let mut record = bytes.iter();
23        if !matches!(record.key(), Some(k) if k == V)
24            || !record.match_bytes(b"spf1")
25            || record.next().is_some_and(|v| !v.is_ascii_whitespace())
26        {
27            return Err(Error::InvalidRecordType);
28        }
29
30        let mut redirect = None;
31        let mut exp = None;
32        let mut ra = None;
33        let mut rp = 100;
34        let mut rr = u8::MAX;
35        let mut directives = Vec::new();
36
37        while let Some((term, qualifier, mut stop_char)) = record.next_term() {
38            match term {
39                A | MX => {
40                    let mut ip4_cidr_length = 32;
41                    let mut ip6_cidr_length = 128;
42                    let mut macro_string = Macro::None;
43
44                    match stop_char {
45                        b' ' => (),
46                        b':' | b'=' => {
47                            let (ds, stop_char) = record.macro_string(false)?;
48                            macro_string = ds;
49                            if stop_char == b'/' {
50                                let (l1, l2) = record.dual_cidr_length()?;
51                                ip4_cidr_length = l1;
52                                ip6_cidr_length = l2;
53                            } else if stop_char != b' ' {
54                                return Err(Error::ParseError);
55                            }
56                        }
57                        b'/' => {
58                            let (l1, l2) = record.dual_cidr_length()?;
59                            ip4_cidr_length = l1;
60                            ip6_cidr_length = l2;
61                        }
62                        _ => return Err(Error::ParseError),
63                    }
64
65                    directives.push(Directive::new(
66                        qualifier,
67                        if term == A {
68                            Mechanism::A {
69                                macro_string,
70                                ip4_mask: u32::MAX << (32 - ip4_cidr_length),
71                                ip6_mask: u128::MAX << (128 - ip6_cidr_length),
72                            }
73                        } else {
74                            Mechanism::Mx {
75                                macro_string,
76                                ip4_mask: u32::MAX << (32 - ip4_cidr_length),
77                                ip6_mask: u128::MAX << (128 - ip6_cidr_length),
78                            }
79                        },
80                    ));
81                }
82                ALL => {
83                    if stop_char == b' ' {
84                        directives.push(Directive::new(qualifier, Mechanism::All))
85                    } else {
86                        return Err(Error::ParseError);
87                    }
88                }
89                INCLUDE | EXISTS => {
90                    if stop_char != b':' {
91                        return Err(Error::ParseError);
92                    }
93                    let (macro_string, stop_char) = record.macro_string(false)?;
94                    if stop_char == b' ' {
95                        directives.push(Directive::new(
96                            qualifier,
97                            if term == INCLUDE {
98                                Mechanism::Include { macro_string }
99                            } else {
100                                Mechanism::Exists { macro_string }
101                            },
102                        ));
103                    } else {
104                        return Err(Error::ParseError);
105                    }
106                }
107                IP4 => {
108                    if stop_char != b':' {
109                        return Err(Error::ParseError);
110                    }
111                    let mut cidr_length = 32;
112                    let (addr, stop_char) = record.ip4()?;
113                    if stop_char == b'/' {
114                        cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
115                    } else if stop_char != b' ' {
116                        return Err(Error::ParseError);
117                    }
118                    directives.push(Directive::new(
119                        qualifier,
120                        Mechanism::Ip4 {
121                            addr,
122                            mask: u32::MAX << (32 - cidr_length),
123                        },
124                    ));
125                }
126                IP6 => {
127                    if stop_char != b':' {
128                        return Err(Error::ParseError);
129                    }
130                    let mut cidr_length = 128;
131                    let (addr, stop_char) = record.ip6()?;
132                    if stop_char == b'/' {
133                        cidr_length = std::cmp::min(cidr_length, record.cidr_length()?);
134                    } else if stop_char != b' ' {
135                        return Err(Error::ParseError);
136                    }
137                    directives.push(Directive::new(
138                        qualifier,
139                        Mechanism::Ip6 {
140                            addr,
141                            mask: u128::MAX << (128 - cidr_length),
142                        },
143                    ));
144                }
145                PTR => {
146                    let mut macro_string = Macro::None;
147                    if stop_char == b':' {
148                        let (ds, stop_char_) = record.macro_string(false)?;
149                        macro_string = ds;
150                        stop_char = stop_char_;
151                    }
152
153                    if stop_char == b' ' {
154                        directives.push(Directive::new(qualifier, Mechanism::Ptr { macro_string }));
155                    } else {
156                        return Err(Error::ParseError);
157                    }
158                }
159                EXP | REDIRECT => {
160                    if stop_char != b'=' {
161                        return Err(Error::ParseError);
162                    }
163                    let (macro_string, stop_char) = record.macro_string(false)?;
164                    if stop_char != b' ' {
165                        return Err(Error::ParseError);
166                    }
167                    if term == REDIRECT {
168                        if redirect.is_none() {
169                            redirect = macro_string.into()
170                        } else {
171                            return Err(Error::ParseError);
172                        }
173                    } else if exp.is_none() {
174                        exp = macro_string.into()
175                    } else {
176                        return Err(Error::ParseError);
177                    };
178                }
179                RA => {
180                    let ra_ = record.ra()?;
181                    if !ra_.is_empty() {
182                        ra = ra_.into_boxed_slice().into();
183                    }
184                }
185                RP => {
186                    rp = std::cmp::min(record.cidr_length()?, 100);
187                }
188                RR => {
189                    rr = record.rr()?;
190                }
191                _ => {
192                    let (_, stop_char) = record.macro_string(false)?;
193                    if stop_char != b' ' {
194                        return Err(Error::ParseError);
195                    }
196                }
197            }
198        }
199
200        Ok(Spf {
201            version: Version::V1,
202            directives: directives.into_boxed_slice(),
203            redirect,
204            exp,
205            ra,
206            rp,
207            rr,
208        })
209    }
210}
211
212const A: u64 = b'a' as u64;
213const ALL: u64 = ((b'l' as u64) << 16) | ((b'l' as u64) << 8) | (b'a' as u64);
214const EXISTS: u64 = ((b's' as u64) << 40)
215    | ((b't' as u64) << 32)
216    | ((b's' as u64) << 24)
217    | ((b'i' as u64) << 16)
218    | ((b'x' as u64) << 8)
219    | (b'e' as u64);
220const EXP: u64 = ((b'p' as u64) << 16) | ((b'x' as u64) << 8) | (b'e' as u64);
221const INCLUDE: u64 = ((b'e' as u64) << 48)
222    | ((b'd' as u64) << 40)
223    | ((b'u' as u64) << 32)
224    | ((b'l' as u64) << 24)
225    | ((b'c' as u64) << 16)
226    | ((b'n' as u64) << 8)
227    | (b'i' as u64);
228const IP4: u64 = ((b'4' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
229const IP6: u64 = ((b'6' as u64) << 16) | ((b'p' as u64) << 8) | (b'i' as u64);
230const MX: u64 = ((b'x' as u64) << 8) | (b'm' as u64);
231const PTR: u64 = ((b'r' as u64) << 16) | ((b't' as u64) << 8) | (b'p' as u64);
232const REDIRECT: u64 = ((b't' as u64) << 56)
233    | ((b'c' as u64) << 48)
234    | ((b'e' as u64) << 40)
235    | ((b'r' as u64) << 32)
236    | ((b'i' as u64) << 24)
237    | ((b'd' as u64) << 16)
238    | ((b'e' as u64) << 8)
239    | (b'r' as u64);
240const RA: u64 = ((b'a' as u64) << 8) | (b'r' as u64);
241const RP: u64 = ((b'p' as u64) << 8) | (b'r' as u64);
242const RR: u64 = ((b'r' as u64) << 8) | (b'r' as u64);
243
244pub(crate) trait SPFParser: Sized {
245    fn next_term(&mut self) -> Option<(u64, Qualifier, u8)>;
246    fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)>;
247    fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)>;
248    fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)>;
249    fn cidr_length(&mut self) -> crate::Result<u8>;
250    fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)>;
251    fn rr(&mut self) -> crate::Result<u8>;
252    fn ra(&mut self) -> crate::Result<Vec<u8>>;
253}
254
255impl SPFParser for Iter<'_, u8> {
256    fn next_term(&mut self) -> Option<(u64, Qualifier, u8)> {
257        let mut qualifier = Qualifier::Pass;
258        let mut stop_char = b' ';
259        let mut d = 0;
260        let mut shift = 0;
261
262        for &ch in self {
263            match ch {
264                b'a'..=b'z' | b'4' | b'6' if shift < 64 => {
265                    d |= (ch as u64) << shift;
266                    shift += 8;
267                }
268                b'A'..=b'Z' if shift < 64 => {
269                    d |= ((ch - b'A' + b'a') as u64) << shift;
270                    shift += 8;
271                }
272                b'+' if shift == 0 => {
273                    qualifier = Qualifier::Pass;
274                }
275                b'-' if shift == 0 => {
276                    qualifier = Qualifier::Fail;
277                }
278                b'~' if shift == 0 => {
279                    qualifier = Qualifier::SoftFail;
280                }
281                b'?' if shift == 0 => {
282                    qualifier = Qualifier::Neutral;
283                }
284                b':' | b'=' | b'/' => {
285                    stop_char = ch;
286                    break;
287                }
288                _ => {
289                    if ch.is_ascii_whitespace() {
290                        if shift != 0 {
291                            stop_char = b' ';
292                            break;
293                        }
294                    } else {
295                        d = u64::MAX;
296                        shift = 64;
297                    }
298                }
299            }
300        }
301
302        if d != 0 {
303            (d, qualifier, stop_char).into()
304        } else {
305            None
306        }
307    }
308
309    #[allow(clippy::while_let_on_iterator)]
310    fn macro_string(&mut self, is_exp: bool) -> crate::Result<(Macro, u8)> {
311        let mut stop_char = b' ';
312        let mut last_is_pct = false;
313        let mut literal = Vec::with_capacity(16);
314        let mut macro_string = Vec::new();
315
316        while let Some(&ch) = self.next() {
317            match ch {
318                b'%' => {
319                    if last_is_pct {
320                        literal.push(b'%');
321                    } else {
322                        last_is_pct = true;
323                        continue;
324                    }
325                }
326                b'_' if last_is_pct => {
327                    literal.push(b' ');
328                }
329                b'-' if last_is_pct => {
330                    literal.extend_from_slice(b"%20");
331                }
332                b'{' if last_is_pct => {
333                    if !literal.is_empty() {
334                        macro_string.push(Macro::Literal(literal.as_slice().into()));
335                        literal.clear();
336                    }
337
338                    let (letter, escape) = self
339                        .next()
340                        .copied()
341                        .and_then(|l| {
342                            if !is_exp {
343                                Variable::parse(l)
344                            } else {
345                                Variable::parse_exp(l)
346                            }
347                        })
348                        .ok_or(Error::ParseError)?;
349                    let mut num_parts: u32 = 0;
350                    let mut reverse = false;
351                    let mut delimiters = 0;
352
353                    while let Some(&ch) = self.next() {
354                        match ch {
355                            b'0'..=b'9' => {
356                                num_parts = num_parts
357                                    .saturating_mul(10)
358                                    .saturating_add((ch - b'0') as u32);
359                            }
360                            b'r' | b'R' => {
361                                reverse = true;
362                            }
363                            b'}' => {
364                                break;
365                            }
366                            b'.' | b'-' | b'+' | b',' | b'/' | b'_' | b'=' => {
367                                delimiters |= 1u64 << (ch - b'+');
368                            }
369                            _ => {
370                                return Err(Error::ParseError);
371                            }
372                        }
373                    }
374
375                    if delimiters == 0 {
376                        delimiters = 1u64 << (b'.' - b'+');
377                    }
378
379                    macro_string.push(Macro::Variable {
380                        letter,
381                        num_parts,
382                        reverse,
383                        escape,
384                        delimiters,
385                    });
386                }
387                b'/' if !is_exp => {
388                    stop_char = ch;
389                    break;
390                }
391                _ => {
392                    if last_is_pct {
393                        return Err(Error::ParseError);
394                    } else if !ch.is_ascii_whitespace() || is_exp {
395                        literal.push(ch);
396                    } else {
397                        break;
398                    }
399                }
400            }
401
402            last_is_pct = false;
403        }
404
405        if !literal.is_empty() {
406            macro_string.push(Macro::Literal(literal.into_boxed_slice()));
407        }
408
409        match macro_string.len() {
410            1 => Ok((macro_string.pop().unwrap(), stop_char)),
411            0 => Err(Error::ParseError),
412            _ => Ok((Macro::List(macro_string.into_boxed_slice()), stop_char)),
413        }
414    }
415
416    fn ip4(&mut self) -> crate::Result<(Ipv4Addr, u8)> {
417        let mut stop_char = b' ';
418        let mut pos = 0;
419        let mut ip = [0u8; 4];
420
421        for &ch in self {
422            match ch {
423                b'0'..=b'9' => {
424                    ip[pos] = (ip[pos].saturating_mul(10)).saturating_add(ch - b'0');
425                }
426                b'.' if pos < 3 => {
427                    pos += 1;
428                }
429                _ => {
430                    stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
431                    break;
432                }
433            }
434        }
435
436        if pos == 3 {
437            Ok((Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]), stop_char))
438        } else {
439            Err(Error::ParseError)
440        }
441    }
442
443    fn ip6(&mut self) -> crate::Result<(Ipv6Addr, u8)> {
444        let mut stop_char = b' ';
445        let mut ip = [0u16; 8];
446        let mut ip_pos = 0;
447        let mut ip4_pos = 0;
448        let mut ip_part = [0u8; 8];
449        let mut ip_part_pos = 0;
450        let mut zero_group_pos = usize::MAX;
451
452        for &ch in self {
453            match ch {
454                b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
455                    if ip_part_pos < 4 {
456                        ip_part[ip_part_pos] = ch;
457                        ip_part_pos += 1;
458                    } else {
459                        return Err(Error::ParseError);
460                    }
461                }
462                b':' => {
463                    if ip_pos < 8 {
464                        if ip_part_pos != 0 {
465                            ip[ip_pos] = u16::from_str_radix(
466                                std::str::from_utf8(&ip_part[..ip_part_pos]).unwrap(),
467                                16,
468                            )
469                            .map_err(|_| Error::ParseError)?;
470                            ip_part_pos = 0;
471                            ip_pos += 1;
472                        } else if zero_group_pos == usize::MAX {
473                            zero_group_pos = ip_pos;
474                        } else if zero_group_pos != ip_pos {
475                            return Err(Error::ParseError);
476                        }
477                    } else {
478                        return Err(Error::ParseError);
479                    }
480                }
481                b'.' => {
482                    if ip_pos < 8 && ip_part_pos > 0 {
483                        let qnum = std::str::from_utf8(&ip_part[..ip_part_pos])
484                            .unwrap()
485                            .parse::<u8>()
486                            .map_err(|_| Error::ParseError)?
487                            as u16;
488                        ip_part_pos = 0;
489                        if ip4_pos % 2 == 1 {
490                            ip[ip_pos] = (ip[ip_pos] << 8) | qnum;
491                            ip_pos += 1;
492                        } else {
493                            ip[ip_pos] = qnum;
494                        }
495                        ip4_pos += 1;
496                    } else {
497                        return Err(Error::ParseError);
498                    }
499                }
500                _ => {
501                    stop_char = if ch.is_ascii_whitespace() { b' ' } else { ch };
502                    break;
503                }
504            }
505        }
506
507        if ip_part_pos != 0 {
508            if ip_pos < 8 {
509                ip[ip_pos] = if ip4_pos == 0 {
510                    u16::from_str_radix(std::str::from_utf8(&ip_part[..ip_part_pos]).unwrap(), 16)
511                        .map_err(|_| Error::ParseError)?
512                } else if ip4_pos == 3 {
513                    (ip[ip_pos] << 8)
514                        | std::str::from_utf8(&ip_part[..ip_part_pos])
515                            .unwrap()
516                            .parse::<u8>()
517                            .map_err(|_| Error::ParseError)? as u16
518                } else {
519                    return Err(Error::ParseError);
520                };
521
522                ip_pos += 1;
523            } else {
524                return Err(Error::ParseError);
525            }
526        }
527        if zero_group_pos != usize::MAX && zero_group_pos < ip_pos {
528            if ip_pos <= 7 {
529                ip.copy_within(zero_group_pos..ip_pos, zero_group_pos + 8 - ip_pos);
530                ip[zero_group_pos..zero_group_pos + 8 - ip_pos].fill(0);
531            } else {
532                return Err(Error::ParseError);
533            }
534        }
535
536        if ip_pos != 0 || zero_group_pos != usize::MAX {
537            Ok((
538                Ipv6Addr::new(ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]),
539                stop_char,
540            ))
541        } else {
542            Err(Error::ParseError)
543        }
544    }
545
546    fn cidr_length(&mut self) -> crate::Result<u8> {
547        let mut cidr_length: u8 = 0;
548        for &ch in self {
549            match ch {
550                b'0'..=b'9' => {
551                    cidr_length = (cidr_length.saturating_mul(10)).saturating_add(ch - b'0');
552                }
553                _ => {
554                    if ch.is_ascii_whitespace() {
555                        break;
556                    } else {
557                        return Err(Error::ParseError);
558                    }
559                }
560            }
561        }
562
563        Ok(cidr_length)
564    }
565
566    fn dual_cidr_length(&mut self) -> crate::Result<(u8, u8)> {
567        let mut ip4_length: u8 = u8::MAX;
568        let mut ip6_length: u8 = u8::MAX;
569        let mut in_ip6 = false;
570
571        for &ch in self {
572            match ch {
573                b'0'..=b'9' => {
574                    if in_ip6 {
575                        ip6_length = if ip6_length != u8::MAX {
576                            (ip6_length.saturating_mul(10)).saturating_add(ch - b'0')
577                        } else {
578                            ch - b'0'
579                        };
580                    } else {
581                        ip4_length = if ip4_length != u8::MAX {
582                            (ip4_length.saturating_mul(10)).saturating_add(ch - b'0')
583                        } else {
584                            ch - b'0'
585                        };
586                    }
587                }
588                b'/' => {
589                    if !in_ip6 {
590                        in_ip6 = true;
591                    } else if ip6_length != u8::MAX {
592                        return Err(Error::ParseError);
593                    }
594                }
595                _ => {
596                    if ch.is_ascii_whitespace() {
597                        break;
598                    } else {
599                        return Err(Error::ParseError);
600                    }
601                }
602            }
603        }
604
605        Ok((
606            std::cmp::min(ip4_length, 32),
607            std::cmp::min(ip6_length, 128),
608        ))
609    }
610
611    fn rr(&mut self) -> crate::Result<u8> {
612        let mut flags: u8 = 0;
613
614        'outer: while let Some(&ch) = self.next() {
615            match ch {
616                b'a' | b'A' => {
617                    for _ in 0..2 {
618                        match self.next().unwrap_or(&0) {
619                            b'l' | b'L' => {}
620                            b' ' | b'\t' => {
621                                return Ok(flags);
622                            }
623                            _ => {
624                                continue 'outer;
625                            }
626                        }
627                    }
628                    flags = u8::MAX;
629                }
630                b'e' | b'E' => {
631                    flags |= RR_TEMP_PERM_ERROR;
632                }
633                b'f' | b'F' => {
634                    flags |= RR_FAIL;
635                }
636                b's' | b'S' => {
637                    flags |= RR_SOFTFAIL;
638                }
639                b'n' | b'N' => {
640                    flags |= RR_NEUTRAL_NONE;
641                }
642                b':' => {}
643                _ => {
644                    if ch.is_ascii_whitespace() {
645                        break;
646                    } else if !ch.is_ascii_alphanumeric() {
647                        return Err(Error::ParseError);
648                    }
649                }
650            }
651        }
652
653        Ok(flags)
654    }
655
656    fn ra(&mut self) -> crate::Result<Vec<u8>> {
657        let mut ra = Vec::new();
658        for &ch in self {
659            if !ch.is_ascii_whitespace() {
660                ra.push(ch);
661            } else {
662                break;
663            }
664        }
665        Ok(ra)
666    }
667}
668
669impl Variable {
670    fn parse(ch: u8) -> Option<(Self, bool)> {
671        match ch {
672            b's' => (Variable::Sender, false),
673            b'l' => (Variable::SenderLocalPart, false),
674            b'o' => (Variable::SenderDomainPart, false),
675            b'd' => (Variable::Domain, false),
676            b'i' => (Variable::Ip, false),
677            b'p' => (Variable::ValidatedDomain, false),
678            b'v' => (Variable::IpVersion, false),
679            b'h' => (Variable::HeloDomain, false),
680
681            b'S' => (Variable::Sender, true),
682            b'L' => (Variable::SenderLocalPart, true),
683            b'O' => (Variable::SenderDomainPart, true),
684            b'D' => (Variable::Domain, true),
685            b'I' => (Variable::Ip, true),
686            b'P' => (Variable::ValidatedDomain, true),
687            b'V' => (Variable::IpVersion, true),
688            b'H' => (Variable::HeloDomain, true),
689            _ => return None,
690        }
691        .into()
692    }
693
694    fn parse_exp(ch: u8) -> Option<(Self, bool)> {
695        match ch {
696            b's' => (Variable::Sender, false),
697            b'l' => (Variable::SenderLocalPart, false),
698            b'o' => (Variable::SenderDomainPart, false),
699            b'd' => (Variable::Domain, false),
700            b'i' => (Variable::Ip, false),
701            b'p' => (Variable::ValidatedDomain, false),
702            b'v' => (Variable::IpVersion, false),
703            b'h' => (Variable::HeloDomain, false),
704            b'c' => (Variable::SmtpIp, false),
705            b'r' => (Variable::HostDomain, false),
706            b't' => (Variable::CurrentTime, false),
707
708            b'S' => (Variable::Sender, true),
709            b'L' => (Variable::SenderLocalPart, true),
710            b'O' => (Variable::SenderDomainPart, true),
711            b'D' => (Variable::Domain, true),
712            b'I' => (Variable::Ip, true),
713            b'P' => (Variable::ValidatedDomain, true),
714            b'V' => (Variable::IpVersion, true),
715            b'H' => (Variable::HeloDomain, true),
716            b'C' => (Variable::SmtpIp, true),
717            b'R' => (Variable::HostDomain, true),
718            b'T' => (Variable::CurrentTime, true),
719            _ => return None,
720        }
721        .into()
722    }
723}
724
725impl TxtRecordParser for Macro {
726    fn parse(record: &[u8]) -> crate::Result<Self> {
727        record.iter().macro_string(true).map(|(m, _)| m)
728    }
729}
730
731#[cfg(test)]
732mod test {
733    use std::net::{Ipv4Addr, Ipv6Addr};
734
735    use crate::{
736        common::parse::TxtRecordParser,
737        spf::{
738            Directive, Macro, Mechanism, Qualifier, RR_FAIL, RR_NEUTRAL_NONE, RR_SOFTFAIL,
739            RR_TEMP_PERM_ERROR, Spf, Variable, Version,
740        },
741    };
742
743    use super::SPFParser;
744
745    #[test]
746    fn parse_spf() {
747        for (record, expected_result) in [
748            (
749                "v=spf1 +mx a:colo.example.com/28 -all",
750                Spf {
751                    version: Version::V1,
752                    ra: None,
753                    rp: 100,
754                    rr: u8::MAX,
755                    exp: None,
756                    redirect: None,
757                    directives: Box::new([
758                        Directive::new(
759                            Qualifier::Pass,
760                            Mechanism::Mx {
761                                macro_string: Macro::None,
762                                ip4_mask: u32::MAX,
763                                ip6_mask: u128::MAX,
764                            },
765                        ),
766                        Directive::new(
767                            Qualifier::Pass,
768                            Mechanism::A {
769                                macro_string: Macro::Literal(b"colo.example.com".as_slice().into()),
770                                ip4_mask: u32::MAX << (32 - 28),
771                                ip6_mask: u128::MAX,
772                            },
773                        ),
774                        Directive::new(Qualifier::Fail, Mechanism::All),
775                    ]),
776                },
777            ),
778            (
779                "v=spf1 a:A.EXAMPLE.COM -all",
780                Spf {
781                    version: Version::V1,
782                    ra: None,
783                    rp: 100,
784                    rr: u8::MAX,
785                    exp: None,
786                    redirect: None,
787                    directives: Box::new([
788                        Directive::new(
789                            Qualifier::Pass,
790                            Mechanism::A {
791                                macro_string: Macro::Literal(b"A.EXAMPLE.COM".as_slice().into()),
792                                ip4_mask: u32::MAX,
793                                ip6_mask: u128::MAX,
794                            },
795                        ),
796                        Directive::new(Qualifier::Fail, Mechanism::All),
797                    ]),
798                },
799            ),
800            (
801                "v=spf1 +mx -all",
802                Spf {
803                    version: Version::V1,
804                    ra: None,
805                    rp: 100,
806                    rr: u8::MAX,
807                    exp: None,
808                    redirect: None,
809                    directives: Box::new([
810                        Directive::new(
811                            Qualifier::Pass,
812                            Mechanism::Mx {
813                                macro_string: Macro::None,
814                                ip4_mask: u32::MAX,
815                                ip6_mask: u128::MAX,
816                            },
817                        ),
818                        Directive::new(Qualifier::Fail, Mechanism::All),
819                    ]),
820                },
821            ),
822            (
823                "v=spf1 +mx redirect=_spf.example.com",
824                Spf {
825                    version: Version::V1,
826                    ra: None,
827                    rp: 100,
828                    rr: u8::MAX,
829                    redirect: Macro::Literal(b"_spf.example.com".as_slice().into()).into(),
830                    exp: None,
831                    directives: Box::new([Directive::new(
832                        Qualifier::Pass,
833                        Mechanism::Mx {
834                            macro_string: Macro::None,
835                            ip4_mask: u32::MAX,
836                            ip6_mask: u128::MAX,
837                        },
838                    )]),
839                },
840            ),
841            (
842                "v=spf1 a mx -all",
843                Spf {
844                    version: Version::V1,
845                    ra: None,
846                    rp: 100,
847                    rr: u8::MAX,
848                    exp: None,
849                    redirect: None,
850                    directives: Box::new([
851                        Directive::new(
852                            Qualifier::Pass,
853                            Mechanism::A {
854                                macro_string: Macro::None,
855                                ip4_mask: u32::MAX,
856                                ip6_mask: u128::MAX,
857                            },
858                        ),
859                        Directive::new(
860                            Qualifier::Pass,
861                            Mechanism::Mx {
862                                macro_string: Macro::None,
863                                ip4_mask: u32::MAX,
864                                ip6_mask: u128::MAX,
865                            },
866                        ),
867                        Directive::new(Qualifier::Fail, Mechanism::All),
868                    ]),
869                },
870            ),
871            (
872                "v=spf1 include:example.com include:example.org -all",
873                Spf {
874                    version: Version::V1,
875                    ra: None,
876                    rp: 100,
877                    rr: u8::MAX,
878                    exp: None,
879                    redirect: None,
880                    directives: Box::new([
881                        Directive::new(
882                            Qualifier::Pass,
883                            Mechanism::Include {
884                                macro_string: Macro::Literal(b"example.com".as_slice().into()),
885                            },
886                        ),
887                        Directive::new(
888                            Qualifier::Pass,
889                            Mechanism::Include {
890                                macro_string: Macro::Literal(b"example.org".as_slice().into()),
891                            },
892                        ),
893                        Directive::new(Qualifier::Fail, Mechanism::All),
894                    ]),
895                },
896            ),
897            (
898                "v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all",
899                Spf {
900                    version: Version::V1,
901                    ra: None,
902                    rp: 100,
903                    rr: u8::MAX,
904                    exp: None,
905                    redirect: None,
906                    directives: Box::new([
907                        Directive::new(
908                            Qualifier::Pass,
909                            Mechanism::Exists {
910                                macro_string: Macro::List(Box::new([
911                                    Macro::Variable {
912                                        letter: Variable::Ip,
913                                        num_parts: 0,
914                                        reverse: true,
915                                        escape: false,
916                                        delimiters: 1u64 << (b'.' - b'+'),
917                                    },
918                                    Macro::Literal(b".".as_slice().into()),
919                                    Macro::Variable {
920                                        letter: Variable::SenderLocalPart,
921                                        num_parts: 1,
922                                        reverse: true,
923                                        escape: false,
924                                        delimiters: (1u64 << (b'+' - b'+'))
925                                            | (1u64 << (b'-' - b'+')),
926                                    },
927                                    Macro::Literal(b"._spf.".as_slice().into()),
928                                    Macro::Variable {
929                                        letter: Variable::Domain,
930                                        num_parts: 0,
931                                        reverse: false,
932                                        escape: false,
933                                        delimiters: 1u64 << (b'.' - b'+'),
934                                    },
935                                ])),
936                            },
937                        ),
938                        Directive::new(Qualifier::Fail, Mechanism::All),
939                    ]),
940                },
941            ),
942            (
943                "v=spf1 mx -all exp=explain._spf.%{d}",
944                Spf {
945                    version: Version::V1,
946                    ra: None,
947                    rp: 100,
948                    rr: u8::MAX,
949                    exp: Macro::List(Box::new([
950                        Macro::Literal(b"explain._spf.".as_slice().into()),
951                        Macro::Variable {
952                            letter: Variable::Domain,
953                            num_parts: 0,
954                            reverse: false,
955                            escape: false,
956                            delimiters: 1u64 << (b'.' - b'+'),
957                        },
958                    ]))
959                    .into(),
960                    redirect: None,
961                    directives: Box::new([
962                        Directive::new(
963                            Qualifier::Pass,
964                            Mechanism::Mx {
965                                macro_string: Macro::None,
966                                ip4_mask: u32::MAX,
967                                ip6_mask: u128::MAX,
968                            },
969                        ),
970                        Directive::new(Qualifier::Fail, Mechanism::All),
971                    ]),
972                },
973            ),
974            (
975                "v=spf1 ip4:192.0.2.1 ip4:192.0.2.129 -all",
976                Spf {
977                    version: Version::V1,
978                    ra: None,
979                    rp: 100,
980                    rr: u8::MAX,
981                    exp: None,
982                    redirect: None,
983                    directives: Box::new([
984                        Directive::new(
985                            Qualifier::Pass,
986                            Mechanism::Ip4 {
987                                addr: "192.0.2.1".parse().unwrap(),
988                                mask: u32::MAX,
989                            },
990                        ),
991                        Directive::new(
992                            Qualifier::Pass,
993                            Mechanism::Ip4 {
994                                addr: "192.0.2.129".parse().unwrap(),
995                                mask: u32::MAX,
996                            },
997                        ),
998                        Directive::new(Qualifier::Fail, Mechanism::All),
999                    ]),
1000                },
1001            ),
1002            (
1003                "v=spf1 ip4:192.0.2.0/24 mx -all",
1004                Spf {
1005                    version: Version::V1,
1006                    ra: None,
1007                    rp: 100,
1008                    rr: u8::MAX,
1009                    exp: None,
1010                    redirect: None,
1011                    directives: Box::new([
1012                        Directive::new(
1013                            Qualifier::Pass,
1014                            Mechanism::Ip4 {
1015                                addr: "192.0.2.0".parse().unwrap(),
1016                                mask: u32::MAX << (32 - 24),
1017                            },
1018                        ),
1019                        Directive::new(
1020                            Qualifier::Pass,
1021                            Mechanism::Mx {
1022                                macro_string: Macro::None,
1023                                ip4_mask: u32::MAX,
1024                                ip6_mask: u128::MAX,
1025                            },
1026                        ),
1027                        Directive::new(Qualifier::Fail, Mechanism::All),
1028                    ]),
1029                },
1030            ),
1031            (
1032                "v=spf1 mx/30 mx:example.org/30 -all",
1033                Spf {
1034                    version: Version::V1,
1035                    ra: None,
1036                    rp: 100,
1037                    rr: u8::MAX,
1038                    exp: None,
1039                    redirect: None,
1040                    directives: Box::new([
1041                        Directive::new(
1042                            Qualifier::Pass,
1043                            Mechanism::Mx {
1044                                macro_string: Macro::None,
1045                                ip4_mask: u32::MAX << (32 - 30),
1046                                ip6_mask: u128::MAX,
1047                            },
1048                        ),
1049                        Directive::new(
1050                            Qualifier::Pass,
1051                            Mechanism::Mx {
1052                                macro_string: Macro::Literal(b"example.org".as_slice().into()),
1053                                ip4_mask: u32::MAX << (32 - 30),
1054                                ip6_mask: u128::MAX,
1055                            },
1056                        ),
1057                        Directive::new(Qualifier::Fail, Mechanism::All),
1058                    ]),
1059                },
1060            ),
1061            (
1062                "v=spf1 ptr -all",
1063                Spf {
1064                    version: Version::V1,
1065                    ra: None,
1066                    rp: 100,
1067                    rr: u8::MAX,
1068                    exp: None,
1069                    redirect: None,
1070                    directives: Box::new([
1071                        Directive::new(
1072                            Qualifier::Pass,
1073                            Mechanism::Ptr {
1074                                macro_string: Macro::None,
1075                            },
1076                        ),
1077                        Directive::new(Qualifier::Fail, Mechanism::All),
1078                    ]),
1079                },
1080            ),
1081            (
1082                "v=spf1 exists:%{l1r+}.%{d}",
1083                Spf {
1084                    version: Version::V1,
1085                    ra: None,
1086                    rp: 100,
1087                    rr: u8::MAX,
1088                    exp: None,
1089                    redirect: None,
1090                    directives: Box::new([Directive::new(
1091                        Qualifier::Pass,
1092                        Mechanism::Exists {
1093                            macro_string: Macro::List(Box::new([
1094                                Macro::Variable {
1095                                    letter: Variable::SenderLocalPart,
1096                                    num_parts: 1,
1097                                    reverse: true,
1098                                    escape: false,
1099                                    delimiters: 1u64 << (b'+' - b'+'),
1100                                },
1101                                Macro::Literal(b".".as_slice().into()),
1102                                Macro::Variable {
1103                                    letter: Variable::Domain,
1104                                    num_parts: 0,
1105                                    reverse: false,
1106                                    escape: false,
1107                                    delimiters: 1u64 << (b'.' - b'+'),
1108                                },
1109                            ])),
1110                        },
1111                    )]),
1112                },
1113            ),
1114            (
1115                "v=spf1 exists:%{ir}.%{l1r+}.%{d}",
1116                Spf {
1117                    version: Version::V1,
1118                    ra: None,
1119                    rp: 100,
1120                    rr: u8::MAX,
1121                    exp: None,
1122                    redirect: None,
1123                    directives: Box::new([Directive::new(
1124                        Qualifier::Pass,
1125                        Mechanism::Exists {
1126                            macro_string: Macro::List(Box::new([
1127                                Macro::Variable {
1128                                    letter: Variable::Ip,
1129                                    num_parts: 0,
1130                                    reverse: true,
1131                                    escape: false,
1132                                    delimiters: 1u64 << (b'.' - b'+'),
1133                                },
1134                                Macro::Literal(b".".as_slice().into()),
1135                                Macro::Variable {
1136                                    letter: Variable::SenderLocalPart,
1137                                    num_parts: 1,
1138                                    reverse: true,
1139                                    escape: false,
1140                                    delimiters: 1u64 << (b'+' - b'+'),
1141                                },
1142                                Macro::Literal(b".".as_slice().into()),
1143                                Macro::Variable {
1144                                    letter: Variable::Domain,
1145                                    num_parts: 0,
1146                                    reverse: false,
1147                                    escape: false,
1148                                    delimiters: 1u64 << (b'.' - b'+'),
1149                                },
1150                            ])),
1151                        },
1152                    )]),
1153                },
1154            ),
1155            (
1156                "v=spf1 exists:_h.%{h}._l.%{l}._o.%{o}._i.%{i}._spf.%{d} ?all",
1157                Spf {
1158                    version: Version::V1,
1159                    ra: None,
1160                    rp: 100,
1161                    rr: u8::MAX,
1162                    exp: None,
1163                    redirect: None,
1164                    directives: Box::new([
1165                        Directive::new(
1166                            Qualifier::Pass,
1167                            Mechanism::Exists {
1168                                macro_string: Macro::List(Box::new([
1169                                    Macro::Literal(b"_h.".as_slice().into()),
1170                                    Macro::Variable {
1171                                        letter: Variable::HeloDomain,
1172                                        num_parts: 0,
1173                                        reverse: false,
1174                                        escape: false,
1175                                        delimiters: 1u64 << (b'.' - b'+'),
1176                                    },
1177                                    Macro::Literal(b"._l.".as_slice().into()),
1178                                    Macro::Variable {
1179                                        letter: Variable::SenderLocalPart,
1180                                        num_parts: 0,
1181                                        reverse: false,
1182                                        escape: false,
1183                                        delimiters: 1u64 << (b'.' - b'+'),
1184                                    },
1185                                    Macro::Literal(b"._o.".as_slice().into()),
1186                                    Macro::Variable {
1187                                        letter: Variable::SenderDomainPart,
1188                                        num_parts: 0,
1189                                        reverse: false,
1190                                        escape: false,
1191                                        delimiters: 1u64 << (b'.' - b'+'),
1192                                    },
1193                                    Macro::Literal(b"._i.".as_slice().into()),
1194                                    Macro::Variable {
1195                                        letter: Variable::Ip,
1196                                        num_parts: 0,
1197                                        reverse: false,
1198                                        escape: false,
1199                                        delimiters: 1u64 << (b'.' - b'+'),
1200                                    },
1201                                    Macro::Literal(b"._spf.".as_slice().into()),
1202                                    Macro::Variable {
1203                                        letter: Variable::Domain,
1204                                        num_parts: 0,
1205                                        reverse: false,
1206                                        escape: false,
1207                                        delimiters: 1u64 << (b'.' - b'+'),
1208                                    },
1209                                ])),
1210                            },
1211                        ),
1212                        Directive::new(Qualifier::Neutral, Mechanism::All),
1213                    ]),
1214                },
1215            ),
1216            (
1217                "v=spf1 mx ?exists:%{ir}.whitelist.example.org -all",
1218                Spf {
1219                    version: Version::V1,
1220                    ra: None,
1221                    rp: 100,
1222                    rr: u8::MAX,
1223                    exp: None,
1224                    redirect: None,
1225                    directives: Box::new([
1226                        Directive::new(
1227                            Qualifier::Pass,
1228                            Mechanism::Mx {
1229                                macro_string: Macro::None,
1230                                ip4_mask: u32::MAX,
1231                                ip6_mask: u128::MAX,
1232                            },
1233                        ),
1234                        Directive::new(
1235                            Qualifier::Neutral,
1236                            Mechanism::Exists {
1237                                macro_string: Macro::List(Box::new([
1238                                    Macro::Variable {
1239                                        letter: Variable::Ip,
1240                                        num_parts: 0,
1241                                        reverse: true,
1242                                        escape: false,
1243                                        delimiters: 1u64 << (b'.' - b'+'),
1244                                    },
1245                                    Macro::Literal(b".whitelist.example.org".as_slice().into()),
1246                                ])),
1247                            },
1248                        ),
1249                        Directive::new(Qualifier::Fail, Mechanism::All),
1250                    ]),
1251                },
1252            ),
1253            (
1254                "v=spf1 mx exists:%{l}._%-spf_%_verify%%.%{d} -all",
1255                Spf {
1256                    version: Version::V1,
1257                    ra: None,
1258                    rp: 100,
1259                    rr: u8::MAX,
1260                    exp: None,
1261                    redirect: None,
1262                    directives: Box::new([
1263                        Directive::new(
1264                            Qualifier::Pass,
1265                            Mechanism::Mx {
1266                                macro_string: Macro::None,
1267                                ip4_mask: u32::MAX,
1268                                ip6_mask: u128::MAX,
1269                            },
1270                        ),
1271                        Directive::new(
1272                            Qualifier::Pass,
1273                            Mechanism::Exists {
1274                                macro_string: Macro::List(Box::new([
1275                                    Macro::Variable {
1276                                        letter: Variable::SenderLocalPart,
1277                                        num_parts: 0,
1278                                        reverse: false,
1279                                        escape: false,
1280                                        delimiters: 1u64 << (b'.' - b'+'),
1281                                    },
1282                                    Macro::Literal(b"._%20spf_ verify%.".as_slice().into()),
1283                                    Macro::Variable {
1284                                        letter: Variable::Domain,
1285                                        num_parts: 0,
1286                                        reverse: false,
1287                                        escape: false,
1288                                        delimiters: 1u64 << (b'.' - b'+'),
1289                                    },
1290                                ])),
1291                            },
1292                        ),
1293                        Directive::new(Qualifier::Fail, Mechanism::All),
1294                    ]),
1295                },
1296            ),
1297            (
1298                "v=spf1 mx redirect=%{l1r+}._at_.%{o,=_/}._spf.%{d}",
1299                Spf {
1300                    version: Version::V1,
1301                    ra: None,
1302                    rp: 100,
1303                    rr: u8::MAX,
1304                    exp: None,
1305                    redirect: Macro::List(Box::new([
1306                        Macro::Variable {
1307                            letter: Variable::SenderLocalPart,
1308                            num_parts: 1,
1309                            reverse: true,
1310                            escape: false,
1311                            delimiters: 1u64 << (b'+' - b'+'),
1312                        },
1313                        Macro::Literal(b"._at_.".as_slice().into()),
1314                        Macro::Variable {
1315                            letter: Variable::SenderDomainPart,
1316                            num_parts: 0,
1317                            reverse: false,
1318                            escape: false,
1319                            delimiters: (1u64 << (b',' - b'+'))
1320                                | (1u64 << (b'=' - b'+'))
1321                                | (1u64 << (b'_' - b'+'))
1322                                | (1u64 << (b'/' - b'+')),
1323                        },
1324                        Macro::Literal(b"._spf.".as_slice().into()),
1325                        Macro::Variable {
1326                            letter: Variable::Domain,
1327                            num_parts: 0,
1328                            reverse: false,
1329                            escape: false,
1330                            delimiters: 1u64 << (b'.' - b'+'),
1331                        },
1332                    ]))
1333                    .into(),
1334                    directives: Box::new([Directive::new(
1335                        Qualifier::Pass,
1336                        Mechanism::Mx {
1337                            macro_string: Macro::None,
1338                            ip4_mask: u32::MAX,
1339                            ip6_mask: u128::MAX,
1340                        },
1341                    )]),
1342                },
1343            ),
1344            (
1345                "v=spf1 -ip4:192.0.2.0/24 a//96 +all",
1346                Spf {
1347                    version: Version::V1,
1348                    ra: None,
1349                    rp: 100,
1350                    rr: u8::MAX,
1351                    exp: None,
1352                    redirect: None,
1353                    directives: Box::new([
1354                        Directive::new(
1355                            Qualifier::Fail,
1356                            Mechanism::Ip4 {
1357                                addr: "192.0.2.0".parse().unwrap(),
1358                                mask: u32::MAX << (32 - 24),
1359                            },
1360                        ),
1361                        Directive::new(
1362                            Qualifier::Pass,
1363                            Mechanism::A {
1364                                macro_string: Macro::None,
1365                                ip4_mask: u32::MAX,
1366                                ip6_mask: u128::MAX << (128 - 96),
1367                            },
1368                        ),
1369                        Directive::new(Qualifier::Pass, Mechanism::All),
1370                    ]),
1371                },
1372            ),
1373            (
1374                concat!(
1375                    "v=spf1 +mx/11//100 ~a:domain.com/12/123 ?ip6:::1 ",
1376                    "-ip6:a::b/111 ip6:1080::8:800:68.0.3.1/96 "
1377                ),
1378                Spf {
1379                    version: Version::V1,
1380                    ra: None,
1381                    rp: 100,
1382                    rr: u8::MAX,
1383                    exp: None,
1384                    redirect: None,
1385                    directives: Box::new([
1386                        Directive::new(
1387                            Qualifier::Pass,
1388                            Mechanism::Mx {
1389                                macro_string: Macro::None,
1390                                ip4_mask: u32::MAX << (32 - 11),
1391                                ip6_mask: u128::MAX << (128 - 100),
1392                            },
1393                        ),
1394                        Directive::new(
1395                            Qualifier::SoftFail,
1396                            Mechanism::A {
1397                                macro_string: Macro::Literal(b"domain.com".as_slice().into()),
1398                                ip4_mask: u32::MAX << (32 - 12),
1399                                ip6_mask: u128::MAX << (128 - 123),
1400                            },
1401                        ),
1402                        Directive::new(
1403                            Qualifier::Neutral,
1404                            Mechanism::Ip6 {
1405                                addr: "::1".parse().unwrap(),
1406                                mask: u128::MAX,
1407                            },
1408                        ),
1409                        Directive::new(
1410                            Qualifier::Fail,
1411                            Mechanism::Ip6 {
1412                                addr: "a::b".parse().unwrap(),
1413                                mask: u128::MAX << (128 - 111),
1414                            },
1415                        ),
1416                        Directive::new(
1417                            Qualifier::Pass,
1418                            Mechanism::Ip6 {
1419                                addr: "1080::8:800:68.0.3.1".parse().unwrap(),
1420                                mask: u128::MAX << (128 - 96),
1421                            },
1422                        ),
1423                    ]),
1424                },
1425            ),
1426            (
1427                "v=spf1 mx:example.org -all ra=postmaster rp=15 rr=e:f:s:n",
1428                Spf {
1429                    version: Version::V1,
1430                    ra: Some(b"postmaster".as_slice().into()),
1431                    rp: 15,
1432                    rr: RR_FAIL | RR_NEUTRAL_NONE | RR_SOFTFAIL | RR_TEMP_PERM_ERROR,
1433                    exp: None,
1434                    redirect: None,
1435                    directives: Box::new([
1436                        Directive::new(
1437                            Qualifier::Pass,
1438                            Mechanism::Mx {
1439                                macro_string: Macro::Literal(b"example.org".as_slice().into()),
1440                                ip4_mask: u32::MAX,
1441                                ip6_mask: u128::MAX,
1442                            },
1443                        ),
1444                        Directive::new(Qualifier::Fail, Mechanism::All),
1445                    ]),
1446                },
1447            ),
1448            (
1449                "v=spf1 ip6:fe80:0000:0000::0000:0000:0000:1 -all",
1450                Spf {
1451                    version: Version::V1,
1452                    ra: None,
1453                    rp: 100,
1454                    rr: u8::MAX,
1455                    exp: None,
1456                    redirect: None,
1457                    directives: Box::new([
1458                        Directive::new(
1459                            Qualifier::Pass,
1460                            Mechanism::Ip6 {
1461                                addr: "fe80:0000:0000::0000:0000:0000:1".parse().unwrap(),
1462                                mask: u128::MAX,
1463                            },
1464                        ),
1465                        Directive::new(Qualifier::Fail, Mechanism::All),
1466                    ]),
1467                },
1468            ),
1469        ] {
1470            assert_eq!(
1471                Spf::parse(record.as_bytes()).unwrap_or_else(|err| panic!("{record:?} : {err:?}")),
1472                expected_result,
1473                "{record}"
1474            );
1475        }
1476    }
1477
1478    #[test]
1479    fn parse_ip6() {
1480        for test in [
1481            "ABCD:EF01:2345:6789:ABCD:EF01:2345:6789",
1482            "2001:DB8:0:0:8:800:200C:417A",
1483            "FF01:0:0:0:0:0:0:101",
1484            "0:0:0:0:0:0:0:1",
1485            "0:0:0:0:0:0:0:0",
1486            "2001:DB8::8:800:200C:417A",
1487            "2001:DB8:0:0:8:800:200C::",
1488            "FF01::101",
1489            "1234::",
1490            "::1",
1491            "::",
1492            "a:b::c:d",
1493            "a::c:d",
1494            "a:b:c::d",
1495            "::c:d",
1496            "0:0:0:0:0:0:13.1.68.3",
1497            "0:0:0:0:0:FFFF:129.144.52.38",
1498            "::13.1.68.3",
1499            "::FFFF:129.144.52.38",
1500            "fe80::1",
1501            "fe80::0000:1",
1502            "fe80:0000::0000:1",
1503            "fe80:0000:0000:0000::1",
1504            "fe80:0000:0000:0000::0000:1",
1505            "fe80:0000:0000::0000:0000:0000:1",
1506            "fe80::0000:0000:0000:0000:0000:1",
1507            "fe80:0000:0000:0000:0000:0000:0000:1",
1508        ] {
1509            for test in [test.to_string(), format!("{test} ")] {
1510                let (ip, stop_char) = test
1511                    .as_bytes()
1512                    .iter()
1513                    .ip6()
1514                    .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
1515                assert_eq!(stop_char, b' ', "{test}");
1516                assert_eq!(ip, test.trim_end().parse::<Ipv6Addr>().unwrap())
1517            }
1518        }
1519
1520        for invalid_test in [
1521            "0:0:0:0:0:0:0:1:1",
1522            "0:0:0:0:0:0:13.1.68.3.4",
1523            "::0:0:0:0:0:0:0:0",
1524            "0:0:0:0::0:0:0:0",
1525            " ",
1526            "",
1527        ] {
1528            assert!(
1529                invalid_test.as_bytes().iter().ip6().is_err(),
1530                "{}",
1531                invalid_test
1532            );
1533        }
1534    }
1535
1536    #[test]
1537    fn parse_ip4() {
1538        for test in ["0.0.0.0", "255.255.255.255", "13.1.68.3", "129.144.52.38"] {
1539            for test in [test.to_string(), format!("{test} ")] {
1540                let (ip, stop_char) = test
1541                    .as_bytes()
1542                    .iter()
1543                    .ip4()
1544                    .unwrap_or_else(|err| panic!("{test:?} : {err:?}"));
1545                assert_eq!(stop_char, b' ', "{test}");
1546                assert_eq!(ip, test.trim_end().parse::<Ipv4Addr>().unwrap());
1547            }
1548        }
1549    }
1550}