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