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