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