Skip to main content

route_summarization/
lib.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2//! Aggregate IPv4/IPv6 CIDR prefixes into their minimal covering set.
3//!
4//! Inputs are parsed with [`parse_entry`] and collected in an [`Aggregator`],
5//! which merges overlapping and adjacent ranges and emits the smallest list
6//! of CIDR blocks that covers exactly the same addresses.
7//!
8//! ```
9//! use route_summarization::Aggregator;
10//!
11//! let mut agg = Aggregator::new();
12//! for line in ["10.0.0.0/24", "10.0.1.0/24", "192.0.2.7", "2001:db8::/33", "2001:db8:8000::/33"] {
13//!     agg.add_str(line, false).unwrap();
14//! }
15//! let out: Vec<String> = agg.summarize().iter().map(|p| p.to_string()).collect();
16//! assert_eq!(out, ["10.0.0.0/23", "192.0.2.7", "2001:db8::/32"]);
17//! ```
18
19use std::fmt;
20use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
21
22/// Address family of a prefix or range.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24pub enum Family {
25    V4,
26    V6,
27}
28
29impl Family {
30    /// Address width in bits.
31    pub const fn bits(self) -> u8 {
32        match self {
33            Family::V4 => 32,
34            Family::V6 => 128,
35        }
36    }
37
38    const fn max(self) -> u128 {
39        match self {
40            Family::V4 => u32::MAX as u128,
41            Family::V6 => u128::MAX,
42        }
43    }
44}
45
46/// An inclusive address range within one family. Always valid: `start <= end`,
47/// both within the family's address space.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct Range {
50    family: Family,
51    start: u128,
52    end: u128,
53}
54
55impl Range {
56    /// A range of numeric addresses; `None` unless `start <= end` and both fit
57    /// the family (IPv4 values must be at most `u32::MAX`).
58    pub fn new(family: Family, start: u128, end: u128) -> Option<Self> {
59        (start <= end && end <= family.max()).then_some(Self { family, start, end })
60    }
61
62    /// The range between two addresses of the same family, in either order.
63    pub fn from_ips(a: IpAddr, b: IpAddr) -> Option<Self> {
64        let ((fa, a), (fb, b)) = (from_ip(a), from_ip(b));
65        (fa == fb).then(|| Self {
66            family: fa,
67            start: a.min(b),
68            end: a.max(b),
69        })
70    }
71
72    pub fn family(&self) -> Family {
73        self.family
74    }
75
76    /// First address, as an integer.
77    pub fn start(&self) -> u128 {
78        self.start
79    }
80
81    /// Last address (inclusive), as an integer.
82    pub fn end(&self) -> u128 {
83        self.end
84    }
85}
86
87/// A CIDR prefix with no host bits set. Displays host routes (/32, /128) as a
88/// bare address.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
90pub struct Prefix {
91    family: Family,
92    addr: u128,
93    len: u8,
94}
95
96impl Prefix {
97    /// `None` if `len` exceeds the family's width, `addr` does not fit the
98    /// family, or `addr` has bits set beyond the prefix length.
99    pub fn new(family: Family, addr: u128, len: u8) -> Option<Self> {
100        (len <= family.bits() && addr <= family.max() && addr & host_mask(family, len) == 0)
101            .then_some(Self { family, addr, len })
102    }
103
104    pub fn family(&self) -> Family {
105        self.family
106    }
107
108    /// The prefix's network address.
109    pub fn network(&self) -> IpAddr {
110        to_ip(self.family, self.addr)
111    }
112
113    /// Prefix length in bits.
114    pub fn prefix_len(&self) -> u8 {
115        self.len
116    }
117
118    /// True when the prefix covers a single address.
119    pub fn is_host(&self) -> bool {
120        self.len == self.family.bits()
121    }
122
123    /// The addresses the prefix covers.
124    pub fn range(&self) -> Range {
125        Range {
126            family: self.family,
127            start: self.addr,
128            end: self.addr | host_mask(self.family, self.len),
129        }
130    }
131}
132
133impl fmt::Display for Prefix {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        // std formats IPv6 per RFC 5952: lowercase, no leading zeros, `::` only
136        // for the longest run of two or more zero groups.
137        if self.is_host() {
138            write!(f, "{}", self.network())
139        } else {
140            write!(f, "{}/{}", self.network(), self.len)
141        }
142    }
143}
144
145/// Why an input line was rejected.
146#[derive(Debug, Clone, PartialEq, Eq)]
147#[non_exhaustive]
148pub enum ParseError {
149    /// Not an IPv4 or IPv6 address.
150    InvalidAddress,
151    /// Prefix length is not a plain decimal number within the family's width.
152    InvalidPrefixLength,
153    /// Range ends belong to different families, or start is after end.
154    InvalidRange,
155    /// SPF `ip4:` given an IPv6 value, or `ip6:` given an IPv4 value.
156    SpfFamilyMismatch,
157}
158
159impl fmt::Display for ParseError {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.write_str(match self {
162            ParseError::InvalidAddress => "not a valid IPv4/IPv6 address",
163            ParseError::InvalidPrefixLength => "invalid prefix length",
164            ParseError::InvalidRange => "invalid address range",
165            ParseError::SpfFamilyMismatch => "SPF mechanism does not match address family",
166        })
167    }
168}
169
170impl std::error::Error for ParseError {}
171
172/// Parse one input entry into an address range.
173///
174/// Accepted forms, for both IPv4 and IPv6:
175/// * `addr` — a single host
176/// * `addr/len` — a prefix; host bits are masked off (`10.1.2.3/8` is `10.0.0.0/8`)
177/// * `start-end` — an inclusive range, split into CIDRs on output
178///
179/// With `spf`, a leading `ip4:` or `ip6:` (any case) is stripped and must match
180/// the address family.
181///
182/// Rejected: IPv4 octets with leading zeros (ambiguous octal), IPv6 zone IDs
183/// (`fe80::1%eth0`, meaningless in a route), and prefix lengths with a sign or
184/// leading zeros (`/032`).
185pub fn parse_entry(input: &str, spf: bool) -> Result<Range, ParseError> {
186    let mut s = input.trim();
187    let mut want = None;
188    if spf {
189        if let Some(rest) = strip_prefix_ci(s, "ip4:") {
190            want = Some(Family::V4);
191            s = rest;
192        } else if let Some(rest) = strip_prefix_ci(s, "ip6:") {
193            want = Some(Family::V6);
194            s = rest;
195        }
196    }
197
198    let range = if let Some((a, b)) = s.split_once('-') {
199        let (fa, start) = parse_addr(a.trim())?;
200        let (fb, end) = parse_addr(b.trim())?;
201        if fa != fb {
202            return Err(ParseError::InvalidRange);
203        }
204        Range::new(fa, start, end).ok_or(ParseError::InvalidRange)?
205    } else if let Some((a, len)) = s.split_once('/') {
206        let (family, addr) = parse_addr(a)?;
207        let len = parse_len(len, family)?;
208        let host = host_mask(family, len);
209        Range {
210            family,
211            start: addr & !host,
212            end: (addr & !host) | host,
213        }
214    } else {
215        let (family, addr) = parse_addr(s)?;
216        Range {
217            family,
218            start: addr,
219            end: addr,
220        }
221    };
222
223    match want {
224        Some(f) if f != range.family => Err(ParseError::SpfFamilyMismatch),
225        _ => Ok(range),
226    }
227}
228
229/// Collects ranges and produces the minimal covering prefix list.
230///
231/// Exclusions are subtracted address by address: the summary covers exactly
232/// the added addresses minus the excluded ones, whatever the order of calls
233/// and however the inputs overlap. A prefix that partly overlaps an exclusion
234/// is split around it.
235///
236/// ```
237/// use route_summarization::Aggregator;
238///
239/// let mut agg = Aggregator::new();
240/// agg.add_str("192.0.2.0/24", false).unwrap();
241/// agg.exclude_str("192.0.2.0/25", false).unwrap();
242/// agg.exclude_str("192.0.2.255", false).unwrap();
243/// let out: Vec<String> = agg.summarize().iter().map(|p| p.to_string()).collect();
244/// assert_eq!(out, ["192.0.2.128/26", "192.0.2.192/27", "192.0.2.224/28",
245///                  "192.0.2.240/29", "192.0.2.248/30", "192.0.2.252/31", "192.0.2.254"]);
246/// ```
247#[derive(Debug, Default, Clone)]
248pub struct Aggregator {
249    include: Ranges,
250    exclude: Ranges,
251}
252
253#[derive(Debug, Default, Clone)]
254struct Ranges {
255    v4: Vec<(u128, u128)>,
256    v6: Vec<(u128, u128)>,
257}
258
259impl Ranges {
260    fn push(&mut self, r: Range) {
261        match r.family {
262            Family::V4 => self.v4.push((r.start, r.end)),
263            Family::V6 => self.v6.push((r.start, r.end)),
264        }
265    }
266
267    fn get(&self, family: Family) -> &[(u128, u128)] {
268        match family {
269            Family::V4 => &self.v4,
270            Family::V6 => &self.v6,
271        }
272    }
273}
274
275impl Aggregator {
276    pub fn new() -> Self {
277        Self::default()
278    }
279
280    pub fn add(&mut self, r: Range) {
281        self.include.push(r);
282    }
283
284    /// Parse and add one entry; see [`parse_entry`].
285    pub fn add_str(&mut self, input: &str, spf: bool) -> Result<(), ParseError> {
286        self.add(parse_entry(input, spf)?);
287        Ok(())
288    }
289
290    /// Remove these addresses from the summary, wherever they appear.
291    pub fn exclude(&mut self, r: Range) {
292        self.exclude.push(r);
293    }
294
295    /// Parse and exclude one entry; see [`parse_entry`].
296    pub fn exclude_str(&mut self, input: &str, spf: bool) -> Result<(), ParseError> {
297        self.exclude(parse_entry(input, spf)?);
298        Ok(())
299    }
300
301    /// True when nothing has been added (exclusions alone don't count).
302    pub fn is_empty(&self) -> bool {
303        self.include.v4.is_empty() && self.include.v6.is_empty()
304    }
305
306    /// Minimal CIDR list: all IPv4 first, then IPv6, each in address order.
307    pub fn summarize(&self) -> Vec<Prefix> {
308        let mut out = Vec::new();
309        for family in [Family::V4, Family::V6] {
310            let include = merge(self.include.get(family));
311            let exclude = merge(self.exclude.get(family));
312            for (start, end) in subtract(&include, &exclude) {
313                split(family, start, end, &mut out);
314            }
315        }
316        out
317    }
318}
319
320/// Sort and merge overlapping or adjacent ranges.
321fn merge(ranges: &[(u128, u128)]) -> Vec<(u128, u128)> {
322    let mut sorted = ranges.to_vec();
323    sorted.sort_unstable();
324    let mut merged: Vec<(u128, u128)> = Vec::with_capacity(sorted.len());
325    for (start, end) in sorted {
326        match merged.last_mut() {
327            Some(last) if start <= last.1.saturating_add(1) => last.1 = last.1.max(end),
328            _ => merged.push((start, end)),
329        }
330    }
331    merged
332}
333
334/// `include` minus `exclude`; both sorted, merged and disjoint.
335fn subtract(include: &[(u128, u128)], exclude: &[(u128, u128)]) -> Vec<(u128, u128)> {
336    let mut out = Vec::with_capacity(include.len());
337    let mut first = 0; // first exclusion that can still overlap
338    for &(mut start, end) in include {
339        while first < exclude.len() && exclude[first].1 < start {
340            first += 1;
341        }
342        let mut alive = true;
343        for &(x_start, x_end) in exclude[first..].iter().take_while(|x| x.0 <= end) {
344            if x_start > start {
345                out.push((start, x_start - 1));
346            }
347            if x_end >= end {
348                alive = false;
349                break;
350            }
351            start = x_end + 1; // x_end < end, so no overflow
352        }
353        if alive {
354            out.push((start, end));
355        }
356    }
357    out
358}
359
360/// Append the fewest aligned CIDR blocks covering `start..=end`.
361fn split(family: Family, mut start: u128, end: u128, out: &mut Vec<Prefix>) {
362    let width = u32::from(family.bits());
363    loop {
364        let align = if start == 0 {
365            width
366        } else {
367            start.trailing_zeros().min(width)
368        };
369        let span = end - start; // block size minus one
370        let fit = if span == u128::MAX {
371            128
372        } else {
373            127 - (span + 1).leading_zeros()
374        };
375        let block = align.min(fit);
376        out.push(Prefix {
377            family,
378            addr: start,
379            len: (width - block) as u8,
380        });
381        match 1u128
382            .checked_shl(block)
383            .and_then(|size| start.checked_add(size))
384        {
385            Some(next) if next <= end && next <= family.max() => start = next,
386            _ => break,
387        }
388    }
389}
390
391fn parse_addr(s: &str) -> Result<(Family, u128), ParseError> {
392    // std rejects IPv4 leading zeros and IPv6 zone IDs; accepts embedded IPv4
393    // in IPv6 (`::ffff:192.0.2.1`).
394    s.parse::<IpAddr>()
395        .map(from_ip)
396        .map_err(|_| ParseError::InvalidAddress)
397}
398
399fn from_ip(ip: IpAddr) -> (Family, u128) {
400    match ip {
401        IpAddr::V4(a) => (Family::V4, u128::from(u32::from(a))),
402        IpAddr::V6(a) => (Family::V6, u128::from(a)),
403    }
404}
405
406fn parse_len(s: &str, family: Family) -> Result<u8, ParseError> {
407    let plain = !s.is_empty()
408        && s.len() <= 3
409        && s.bytes().all(|b| b.is_ascii_digit())
410        && (s == "0" || !s.starts_with('0'));
411    match s.parse::<u8>() {
412        Ok(n) if plain && n <= family.bits() => Ok(n),
413        _ => Err(ParseError::InvalidPrefixLength),
414    }
415}
416
417fn host_mask(family: Family, len: u8) -> u128 {
418    let host_bits = u32::from(family.bits() - len);
419    if host_bits == 128 {
420        u128::MAX
421    } else {
422        (1u128 << host_bits) - 1
423    }
424}
425
426fn to_ip(family: Family, v: u128) -> IpAddr {
427    match family {
428        Family::V4 => IpAddr::V4(Ipv4Addr::from(v as u32)),
429        Family::V6 => IpAddr::V6(Ipv6Addr::from(v)),
430    }
431}
432
433fn strip_prefix_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
434    let head = s.get(..prefix.len())?;
435    head.eq_ignore_ascii_case(prefix)
436        .then(|| &s[prefix.len()..])
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    fn agg(lines: &[&str]) -> Vec<String> {
444        let mut a = Aggregator::new();
445        for l in lines {
446            a.add_str(l, false).unwrap();
447        }
448        a.summarize().iter().map(ToString::to_string).collect()
449    }
450
451    fn rejects(s: &str) -> ParseError {
452        parse_entry(s, false).unwrap_err()
453    }
454
455    #[test]
456    fn merges_adjacent_and_contained() {
457        assert_eq!(
458            agg(&["10.0.0.0/24", "10.0.1.0/24", "10.0.0.128/25"]),
459            ["10.0.0.0/23"]
460        );
461        assert_eq!(
462            agg(&["10.0.0.0/24", "10.0.2.0/24"]),
463            ["10.0.0.0/24", "10.0.2.0/24"]
464        );
465    }
466
467    #[test]
468    fn masks_host_bits() {
469        assert_eq!(agg(&["10.1.2.3/8"]), ["10.0.0.0/8"]);
470        assert_eq!(agg(&["2001:db8::1/32"]), ["2001:db8::/32"]);
471    }
472
473    #[test]
474    fn host_routes_print_bare_in_both_families() {
475        assert_eq!(agg(&["1.2.3.4", "1.2.3.4/32"]), ["1.2.3.4"]);
476        assert_eq!(agg(&["2001:db8::1", "2001:db8::1/128"]), ["2001:db8::1"]);
477    }
478
479    #[test]
480    fn full_address_spaces() {
481        assert_eq!(agg(&["0.0.0.0/0", "10.0.0.0/8"]), ["0.0.0.0/0"]);
482        assert_eq!(agg(&["::/0", "2001:db8::/32"]), ["::/0"]);
483        assert_eq!(agg(&["::/1", "8000::/1"]), ["::/0"]);
484        assert_eq!(
485            agg(&["255.255.255.255", "255.255.255.254"]),
486            ["255.255.255.254/31"]
487        );
488        assert_eq!(
489            agg(&["ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"]),
490            ["ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"]
491        );
492    }
493
494    #[test]
495    fn ranges_in_both_families() {
496        assert_eq!(agg(&["10.0.0.0-10.0.0.255"]), ["10.0.0.0/24"]);
497        assert_eq!(
498            agg(&["10.0.0.1 - 10.0.0.6"]),
499            ["10.0.0.1", "10.0.0.2/31", "10.0.0.4/31", "10.0.0.6"]
500        );
501        assert_eq!(agg(&["2001:db8::-2001:db8::ffff"]), ["2001:db8::/112"]);
502        assert_eq!(rejects("10.0.0.9-10.0.0.1"), ParseError::InvalidRange);
503        assert_eq!(rejects("10.0.0.1-::1"), ParseError::InvalidRange);
504    }
505
506    #[test]
507    fn rfc5952_output() {
508        // The Perl/Net::CIDR::Lite version printed these as `0001::/16`,
509        // `00ab::/16`, `2001:db8::1:1:1:1:1` and `...:28::/120`.
510        assert_eq!(agg(&["1::/16"]), ["1::/16"]);
511        assert_eq!(agg(&["ab::/16"]), ["ab::/16"]);
512        assert_eq!(agg(&["2001:db8:0:1:1:1:1:1"]), ["2001:db8:0:1:1:1:1:1"]);
513        assert_eq!(
514            agg(&["2001:558:fe14:76:68:87:28:0/120"]),
515            ["2001:558:fe14:76:68:87:28:0/120"]
516        );
517        assert_eq!(agg(&["2001:DB8::/32"]), ["2001:db8::/32"]);
518        assert_eq!(agg(&["2001:0:0:1:0:0:0:1"]), ["2001:0:0:1::1"]);
519        assert_eq!(agg(&["::ffff:1.2.3.0/120"]), ["::ffff:1.2.3.0/120"]);
520    }
521
522    #[test]
523    fn rejects_ambiguous_input() {
524        assert_eq!(rejects("010.1.2.3"), ParseError::InvalidAddress);
525        assert_eq!(rejects("555.2.2.2/30"), ParseError::InvalidAddress);
526        assert_eq!(rejects("fe80::1%eth0"), ParseError::InvalidAddress);
527        assert_eq!(rejects("2001:db8::%1/64"), ParseError::InvalidAddress);
528        assert_eq!(rejects("garbage:"), ParseError::InvalidAddress);
529        assert_eq!(rejects("192.168.8.0/33"), ParseError::InvalidPrefixLength);
530        assert_eq!(rejects("2001:db8::/129"), ParseError::InvalidPrefixLength);
531        assert_eq!(rejects("10.0.0.0/01"), ParseError::InvalidPrefixLength);
532        assert_eq!(rejects("2001:db8::/032"), ParseError::InvalidPrefixLength);
533        assert_eq!(rejects("10.0.0.0/+8"), ParseError::InvalidPrefixLength);
534        assert_eq!(rejects("10.0.0.0/"), ParseError::InvalidPrefixLength);
535    }
536
537    #[test]
538    fn spf_prefixes() {
539        assert!(parse_entry("ip4:192.0.2.0/24", true).is_ok());
540        assert!(parse_entry("IP6:2001:db8::/32", true).is_ok());
541        assert_eq!(
542            parse_entry("ip4:2001:db8::/32", true),
543            Err(ParseError::SpfFamilyMismatch)
544        );
545        assert_eq!(
546            parse_entry("ip6:192.0.2.0/24", true),
547            Err(ParseError::SpfFamilyMismatch)
548        );
549        // Without --spf the mechanism is not stripped.
550        assert!(parse_entry("ip4:192.0.2.0/24", false).is_err());
551    }
552
553    #[test]
554    fn output_is_minimal_and_exact() {
555        // Brute-force check over a small IPv4 window: the summary must cover
556        // exactly the input addresses, and no two output blocks may be mergeable.
557        let mut seed = 0x9e3779b97f4a7c15u64;
558        let mut rand = move || {
559            seed ^= seed << 13;
560            seed ^= seed >> 7;
561            seed ^= seed << 17;
562            seed
563        };
564        for _ in 0..500 {
565            let mut a = Aggregator::new();
566            let mut want = [false; 1024];
567            for _ in 0..(rand() % 20 + 1) {
568                let len = 22 + (rand() % 11) as u8;
569                let addr = (rand() % 1024) as u128;
570                let host = host_mask(Family::V4, len);
571                let (s, e) = (addr & !host, addr | host);
572                a.add(Range {
573                    family: Family::V4,
574                    start: s,
575                    end: e,
576                });
577                want[s as usize..=e as usize]
578                    .iter_mut()
579                    .for_each(|w| *w = true);
580            }
581            let out = a.summarize();
582            let mut got = [false; 1024];
583            for p in &out {
584                let e = p.addr | host_mask(Family::V4, p.len);
585                got[p.addr as usize..=e as usize]
586                    .iter_mut()
587                    .for_each(|g| *g = true);
588            }
589            assert_eq!(want, got);
590            for w in out.windows(2) {
591                let (x, y) = (w[0], w[1]);
592                let sibling = x.len == y.len && x.addr ^ y.addr == 1u128 << (32 - x.len);
593                assert!(!sibling, "mergeable siblings {x} {y}");
594            }
595        }
596    }
597
598    #[test]
599    fn constructors_reject_invalid_values() {
600        assert!(Range::new(Family::V4, 0, u128::from(u32::MAX)).is_some());
601        assert!(Range::new(Family::V4, 1 << 40, 1 << 40).is_none());
602        assert!(Range::new(Family::V6, 10, 5).is_none());
603        assert!(Range::new(Family::V6, 0, u128::MAX).is_some());
604
605        let (a, b) = ("10.0.0.9".parse().unwrap(), "10.0.0.1".parse().unwrap());
606        assert_eq!(
607            Range::from_ips(a, b),
608            Range::new(Family::V4, 0x0a00_0001, 0x0a00_0009)
609        );
610        assert!(Range::from_ips(a, "::1".parse().unwrap()).is_none());
611
612        assert!(Prefix::new(Family::V4, 0x0a00_0000, 8).is_some());
613        assert!(Prefix::new(Family::V4, 0x0a00_0001, 8).is_none()); // host bits
614        assert!(Prefix::new(Family::V4, 0, 33).is_none());
615        assert!(Prefix::new(Family::V4, 1 << 32, 32).is_none());
616        assert!(Prefix::new(Family::V6, 0, 129).is_none());
617        assert_eq!(
618            Prefix::new(Family::V4, 0x0a00_0000, 24).unwrap().range(),
619            Range::new(Family::V4, 0x0a00_0000, 0x0a00_00ff).unwrap()
620        );
621    }
622
623    fn agg_excluding(include: &[&str], exclude: &[&str]) -> Vec<String> {
624        let mut a = Aggregator::new();
625        include.iter().for_each(|l| a.add_str(l, false).unwrap());
626        exclude
627            .iter()
628            .for_each(|l| a.exclude_str(l, false).unwrap());
629        a.summarize().iter().map(ToString::to_string).collect()
630    }
631
632    #[test]
633    fn exclusion_splits_overlapping_prefixes() {
634        assert_eq!(
635            agg_excluding(&["10.0.0.0/24"], &["10.0.0.0/25"]),
636            ["10.0.0.128/25"]
637        );
638        assert_eq!(
639            agg_excluding(&["10.0.0.0/30"], &["10.0.0.1"]),
640            ["10.0.0.0", "10.0.0.2/31"]
641        );
642        // Merged first, then subtracted: the /31 from two hosts loses one.
643        assert_eq!(
644            agg_excluding(&["140.82.112.2", "140.82.112.3"], &["140.82.112.3"]),
645            ["140.82.112.2"]
646        );
647        // One exclusion spanning several separate inputs.
648        assert_eq!(
649            agg_excluding(
650                &["10.0.0.0/24", "10.0.2.0/24", "10.0.4.0/24"],
651                &["10.0.0.128-10.0.4.127"]
652            ),
653            ["10.0.0.0/25", "10.0.4.128/25"]
654        );
655        // Several exclusions inside one input.
656        assert_eq!(
657            agg_excluding(&["10.0.0.0/29"], &["10.0.0.1", "10.0.0.6"]),
658            ["10.0.0.0", "10.0.0.2/31", "10.0.0.4/31", "10.0.0.7"]
659        );
660    }
661
662    #[test]
663    fn exclusion_boundaries() {
664        // Adjacent but not overlapping: no effect.
665        assert_eq!(
666            agg_excluding(&["10.0.0.0/24"], &["10.0.1.0/24", "9.255.255.255"]),
667            ["10.0.0.0/24"]
668        );
669        // Superset or equal: everything goes.
670        assert!(agg_excluding(&["10.1.0.0/16"], &["10.0.0.0/8"]).is_empty());
671        assert!(agg_excluding(&["10.0.0.0/8"], &["10.0.0.0/8"]).is_empty());
672        // Exclusions alone produce nothing.
673        assert!(agg_excluding(&[], &["10.0.0.0/8"]).is_empty());
674        // Other family is untouched.
675        assert_eq!(
676            agg_excluding(&["2001:db8::/32"], &["0.0.0.0/0"]),
677            ["2001:db8::/32"]
678        );
679        assert_eq!(agg_excluding(&["10.0.0.0/8"], &["::/0"]), ["10.0.0.0/8"]);
680    }
681
682    #[test]
683    fn exclusion_at_address_space_edges() {
684        let v4 = agg_excluding(&["0.0.0.0/0"], &["0.0.0.0", "255.255.255.255"]);
685        assert_eq!(v4.first().unwrap(), "0.0.0.1");
686        assert_eq!(v4.last().unwrap(), "255.255.255.254");
687        assert_eq!(v4.len(), 62); // 31 blocks climbing up, 31 back down
688
689        let v6 = agg_excluding(
690            &["::/0"],
691            &["::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"],
692        );
693        assert_eq!(v6.first().unwrap(), "::1");
694        assert_eq!(
695            v6.last().unwrap(),
696            "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe"
697        );
698        assert_eq!(v6.len(), 254);
699
700        assert_eq!(agg_excluding(&["::/0"], &["::/1"]), ["8000::/1"]);
701        assert_eq!(agg_excluding(&["::/0"], &["8000::/1"]), ["::/1"]);
702    }
703
704    #[test]
705    fn exclusion_is_order_independent() {
706        let mut a = Aggregator::new();
707        a.exclude_str("10.0.0.5", false).unwrap();
708        a.add_str("10.0.0.0/29", false).unwrap();
709        let mut b = Aggregator::new();
710        b.add_str("10.0.0.0/29", false).unwrap();
711        b.exclude_str("10.0.0.5", false).unwrap();
712        assert_eq!(a.summarize(), b.summarize());
713        assert!(!a.is_empty());
714        let mut only_excludes = Aggregator::new();
715        only_excludes.exclude_str("10.0.0.0/8", false).unwrap();
716        assert!(only_excludes.is_empty());
717    }
718}