From 3f3744a35f4a561395d3bb751f93884559824458 Mon Sep 17 00:00:00 2001 From: Jialun Cai Date: Sun, 25 Aug 2024 10:14:23 +0800 Subject: [PATCH] Add `aggregate()` function for CIDR merging and collapsing (#15) --- Cargo.toml | 6 +- benches/aggregate.rs | 28 +++ src/aggregate.rs | 342 ++++++++++++++++++++++++++ src/cidr.rs | 241 +++++++++++++++++- src/lib.rs | 3 + src/routing_table/tree_bitmap/node.rs | 4 +- tests/aggregate.rs | 58 +++++ 7 files changed, 666 insertions(+), 16 deletions(-) create mode 100644 benches/aggregate.rs create mode 100644 src/aggregate.rs create mode 100644 tests/aggregate.rs diff --git a/Cargo.toml b/Cargo.toml index 28398d3..fd8d7ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,13 +18,17 @@ routing-table = [] thiserror = "1.0" [dev-dependencies] -criterion = { version = "0.5.1" } +criterion = { version = "0.5.1", features = ["html_reports"] } [[bench]] name = "routing_table" harness = false required-features = ["routing-table"] +[[bench]] +name = "aggregate" +harness = false + [[example]] name = "routing_table" required-features = ["routing-table"] diff --git a/benches/aggregate.rs b/benches/aggregate.rs new file mode 100644 index 0000000..b6c2015 --- /dev/null +++ b/benches/aggregate.rs @@ -0,0 +1,28 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use cidrs::Ipv4Cidr; + +fn ipv4_fixture() -> Vec { + (0..255) + .map(|i| { + (0..=255) + .map(|j| Ipv4Cidr::new([i, j, 0, 0], 16).unwrap()) + .collect::>() + }) + .flatten() + .collect() +} + +fn aggregate_ipv4(cidrs: &[Ipv4Cidr]) -> Vec { + cidrs::aggregate_ipv4(cidrs) +} + +fn aggregate_ipv4_benchmark(c: &mut Criterion) { + let cidrs: Vec<_> = ipv4_fixture(); + c.bench_function("aggregate_ipv4", |b| { + b.iter(|| aggregate_ipv4(black_box(&cidrs))) + }); +} + +criterion_group!(benches, aggregate_ipv4_benchmark,); +criterion_main!(benches); diff --git a/src/aggregate.rs b/src/aggregate.rs new file mode 100644 index 0000000..3cf3084 --- /dev/null +++ b/src/aggregate.rs @@ -0,0 +1,342 @@ +use core::fmt; +use core::net::{Ipv4Addr, Ipv6Addr}; +use core::ptr::NonNull; + +use crate::{Cidr, Ipv4Cidr, Ipv6Cidr}; + +/// Partitions a slice of `Cidr` into separate vectors of `Ipv4Cidr` and `Ipv6Cidr`. +/// +/// This function takes a slice of `Cidr` and separates them into two vectors: +/// one for IPv4 CIDRs and another for IPv6 CIDRs. +/// +/// # Examples +/// +/// ``` +/// use cidrs::{Cidr, Ipv4Cidr, Ipv6Cidr, partition_by_ip_family}; +/// +/// let cidrs = vec![ +/// Cidr::V4("192.168.0.0/24".parse().unwrap()), +/// Cidr::V6("2001:db8::/32".parse().unwrap()), +/// Cidr::V4("10.0.0.0/8".parse().unwrap()), +/// ]; +/// +/// let (ipv4_cidrs, ipv6_cidrs) = partition_by_ip_family(&cidrs); +/// +/// assert_eq!(ipv4_cidrs.len(), 2); +/// assert_eq!(ipv6_cidrs.len(), 1); +/// assert_eq!(ipv4_cidrs[0], "192.168.0.0/24".parse::().unwrap()); +/// assert_eq!(ipv4_cidrs[1], "10.0.0.0/8".parse::().unwrap()); +/// assert_eq!(ipv6_cidrs[0], "2001:db8::/32".parse::().unwrap()); +/// ``` +#[inline] +pub fn partition_by_ip_family(cidrs: &[Cidr]) -> (Vec, Vec) { + let (mut v4, mut v6) = (Vec::new(), Vec::new()); + + for cidr in cidrs { + match cidr { + Cidr::V4(v) => v4.push(*v), + Cidr::V6(v) => v6.push(*v), + } + } + (v4, v6) +} + +/// Aggregates a list of CIDR ranges into a minimal set of non-overlapping ranges. +/// +/// This function takes a slice of `Cidr` (which can be either IPv4 or IPv6) and returns +/// a new `Vec` containing the aggregated ranges. +/// +/// # Examples +/// +/// ``` +/// use cidrs::{Cidr, aggregate}; +/// +/// let cidrs = vec![ +/// "192.168.0.0/24".parse().unwrap(), +/// "192.168.1.0/24".parse().unwrap(), +/// "10.0.0.0/8".parse().unwrap(), +/// "2001:db8::/32".parse().unwrap(), +/// "2001:db8:1::/48".parse().unwrap(), +/// ]; +/// +/// let aggregated = aggregate(&cidrs); +/// let expected: Vec = vec![ +/// "10.0.0.0/8".parse().unwrap(), +/// "192.168.0.0/23".parse().unwrap(), +/// "2001:db8::/32".parse().unwrap(), +/// ]; +/// assert_eq!(aggregated, expected); +/// ``` +#[inline] +pub fn aggregate(cidrs: &[Cidr]) -> Vec { + let (v4, v6) = partition_by_ip_family(cidrs); + + let v4 = aggregate_ipv4(&v4).into_iter().map(Cidr::V4); + let v6 = aggregate_ipv6(&v6).into_iter().map(Cidr::V6); + + v4.chain(v6).collect() +} + +/// Aggregates a list of IPv4 CIDR ranges into a minimal set of non-overlapping ranges. +/// +/// # Examples +/// +/// ``` +/// use cidrs::{Ipv4Cidr, aggregate_ipv4}; +/// +/// let cidrs = vec![ +/// "192.168.0.0/24".parse().unwrap(), +/// "192.168.1.0/24".parse().unwrap(), +/// "10.0.0.0/8".parse().unwrap(), +/// ]; +/// +/// let aggregated = aggregate_ipv4(&cidrs); +/// assert_eq!(aggregated.len(), 2); +/// assert!(aggregated.contains(&"192.168.0.0/23".parse().unwrap())); +/// assert!(aggregated.contains(&"10.0.0.0/8".parse().unwrap())); +/// ``` +pub fn aggregate_ipv4(cidrs: &[Ipv4Cidr]) -> Vec { + if cidrs.len() <= 1 { + return cidrs.to_vec(); + } + + let mut tree = Tree::::new(); + let mut cidrs = cidrs.to_vec(); + cidrs.sort_unstable(); + for cidr in cidrs { + tree.insert(cidr); + } + tree.list() +} + +/// Aggregates a list of IPv6 CIDR ranges into a minimal set of non-overlapping ranges. +/// +/// # Examples +/// +/// ``` +/// use cidrs::{Ipv6Cidr, aggregate_ipv6}; +/// +/// let cidrs = vec![ +/// "2001:db8::/32".parse().unwrap(), +/// "2001:db8:1::/48".parse().unwrap(), +/// "2001:db8:2::/48".parse().unwrap(), +/// ]; +/// +/// let aggregated = aggregate_ipv6(&cidrs); +/// assert_eq!(aggregated.len(), 1); +/// assert!(aggregated.contains(&"2001:db8::/32".parse().unwrap())); +/// ``` +pub fn aggregate_ipv6(cidrs: &[Ipv6Cidr]) -> Vec { + if cidrs.len() <= 1 { + return cidrs.to_vec(); + } + + let mut tree = Tree::::new(); + let mut cidrs = cidrs.to_vec(); + cidrs.sort_unstable(); + for cidr in cidrs { + tree.insert(cidr); + } + tree.list() +} + +struct Node { + cidr: T, + is_masked: bool, + parent: Option>>, + left: Option>>, + right: Option>>, +} + +impl Node { + #[inline] + fn new(parent: Option>>, cidr: T) -> NonNull { + let boxed = Box::new(Self { + parent, + cidr, + is_masked: false, + left: None, + right: None, + }); + + let ptr = Box::into_raw(boxed); + NonNull::new(ptr).unwrap() + } + + #[inline] + fn get_or_new_left_child(&mut self, f: F) -> NonNull + where + F: FnOnce() -> NonNull, + { + *self.left.get_or_insert_with(f) + } + + #[inline] + fn get_or_new_right_child(&mut self, f: F) -> NonNull + where + F: FnOnce() -> NonNull, + { + *self.right.get_or_insert_with(f) + } + + #[inline] + fn clear_children(&mut self) { + if let Some(left) = self.left.take() { + let _ = unsafe { Box::from_raw(left.as_ptr()) }; + } + if let Some(right) = self.right.take() { + let _ = unsafe { Box::from_raw(right.as_ptr()) }; + } + } +} + +impl Drop for Node { + fn drop(&mut self) { + self.clear_children(); + } +} + +struct Tree { + root: NonNull>, +} + +impl Drop for Tree { + fn drop(&mut self) { + unsafe { + let _ = Box::from_raw(self.root.as_ptr()); + } + } +} + +impl Tree +where + T: Copy + fmt::Debug, +{ + fn pruning(node: NonNull>) { + let mut parent = { + let p = unsafe { node.as_ref() }; + p.parent + }; + + while let Some(mut node) = parent { + let p = unsafe { node.as_mut() }; + let mut masked = 0; + if let Some(left) = p.left { + let l = unsafe { left.as_ref() }; + if l.is_masked { + masked += 1; + } + } + if let Some(right) = p.right { + let r = unsafe { right.as_ref() }; + if r.is_masked { + masked += 1; + } + } + + if masked < 2 { + break; + } + p.is_masked = true; + parent = p.parent; + } + } + + pub fn list(&self) -> Vec { + use std::collections::VecDeque; + + let mut rv = vec![]; + let mut q = VecDeque::new(); + + q.push_back(self.root); + + while let Some(node) = q.pop_front() { + let p = unsafe { node.as_ref() }; + if p.is_masked { + rv.push(p.cidr); + continue; + } + if let Some(left) = p.left { + q.push_back(left); + } + if let Some(right) = p.right { + q.push_back(right); + } + } + rv + } +} + +impl Tree { + #[inline] + pub fn new() -> Self { + Self { + root: Node::new(None, Ipv4Cidr::from_ip(Ipv4Addr::UNSPECIFIED, 0).unwrap()), + } + } + + pub fn insert(&mut self, cidr: Ipv4Cidr) { + let bytes = u32::from_be_bytes(cidr.octets()); + + let mut node = self.root; + for i in 0..cidr.bits() { + let p = unsafe { node.as_mut() }; + + if p.is_masked { + break; + } + + let bit = (bytes >> (31 - i)) & 1; + let f = || Node::new(Some(node), Ipv4Cidr::new(cidr.octets(), i + 1).unwrap()); + node = if bit == 0 { + p.get_or_new_left_child(f) + } else { + p.get_or_new_right_child(f) + } + } + + let p = unsafe { node.as_mut() }; + p.is_masked = true; + p.clear_children(); + Self::pruning(node); + } +} + +impl Tree { + #[inline] + pub fn new() -> Self { + Self { + root: Node::new(None, Ipv6Cidr::from_ip(Ipv6Addr::UNSPECIFIED, 0).unwrap()), + } + } + + pub fn insert(&mut self, cidr: Ipv6Cidr) { + let bytes = u128::from_be_bytes(cidr.octets()); + + let mut node = self.root; + for i in 0..cidr.bits() { + let p = unsafe { node.as_mut() }; + if p.is_masked { + break; + } + + let bit = (bytes >> (31 - i)) & 1; + let f = || { + Node::new( + Some(node), + Ipv6Cidr::from_ip(cidr.network_addr(), i + 1).unwrap(), + ) + }; + node = if bit == 0 { + p.get_or_new_left_child(f) + } else { + p.get_or_new_right_child(f) + } + } + + let p = unsafe { node.as_mut() }; + p.is_masked = true; + p.clear_children(); + Self::pruning(node); + } +} diff --git a/src/cidr.rs b/src/cidr.rs index 8b68d3a..14aea3f 100644 --- a/src/cidr.rs +++ b/src/cidr.rs @@ -1,3 +1,4 @@ +use core::cmp::Ordering; use core::fmt; use core::hash::{Hash, Hasher}; use core::net::{IpAddr, Ipv4Addr, Ipv6Addr}; @@ -184,6 +185,33 @@ impl Ipv4Cidr { } } } + + /// Returns the supernet of this CIDR block, if possible. + /// + /// The supernet is the next larger network that contains this CIDR block. + /// If the current CIDR block has 0 bits (representing the entire IPv4 address space), + /// this method returns `None` as there is no larger network possible. + /// + /// # Examples + /// + /// ``` + /// use cidrs::Ipv4Cidr; + /// + /// let cidr = Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(); + /// let supernet = cidr.supernet().unwrap(); + /// assert_eq!(supernet, Ipv4Cidr::new([192, 168, 0, 0], 23).unwrap()); + /// + /// let entire_space = Ipv4Cidr::new([0, 0, 0, 0], 0).unwrap(); + /// assert_eq!(entire_space.supernet(), None); + /// ``` + #[inline] + pub fn supernet(&self) -> Option { + match self.bits() { + 0 => None, + bits => Some(Ipv4Cidr::new(self.octets(), bits - 1).unwrap()), + } + } + /// /// # Examples /// @@ -354,11 +382,97 @@ impl FromStr for Ipv4Cidr { } } +/// Implements partial ordering for `Ipv4Cidr`. +/// +/// This implementation delegates to the `Ord` implementation. +/// +/// # Examples +/// +/// ``` +/// use cidrs::Ipv4Cidr; +/// use std::cmp::Ordering; +/// +/// let cidr1 = Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(); +/// let cidr2 = Ipv4Cidr::new([192, 168, 0, 0], 16).unwrap(); +/// let cidr3 = Ipv4Cidr::new([192, 168, 1, 0], 24).unwrap(); +/// +/// assert_eq!(cidr1.partial_cmp(&cidr2), Some(Ordering::Greater)); +/// assert_eq!(cidr1.partial_cmp(&cidr3), Some(Ordering::Less)); +/// assert_eq!(cidr3.partial_cmp(&cidr1), Some(Ordering::Greater)); +/// ``` +impl PartialOrd for Ipv4Cidr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Implements total ordering for `Ipv4Cidr`. +/// +/// CIDRs are first compared by their network address, then by their prefix length. +/// +/// # Examples +/// +/// ``` +/// use cidrs::Ipv4Cidr; +/// use std::cmp::Ordering; +/// +/// let cidr1 = Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(); +/// let cidr2 = Ipv4Cidr::new([192, 168, 0, 0], 16).unwrap(); +/// let cidr3 = Ipv4Cidr::new([192, 168, 1, 0], 24).unwrap(); +/// +/// assert_eq!(cidr1.cmp(&cidr2), Ordering::Greater); +/// assert_eq!(cidr1.cmp(&cidr3), Ordering::Less); +/// assert_eq!(cidr3.cmp(&cidr1), Ordering::Greater); +/// ``` +impl Ord for Ipv4Cidr { + fn cmp(&self, other: &Self) -> Ordering { + let a = u32::from_be_bytes(self.octets); + let b = u32::from_be_bytes(other.octets); + + match a.cmp(&b) { + Ordering::Equal => self.bits.cmp(&other.bits), + ord => ord, + } + } +} + pub struct Ipv4Hosts { cursor: u32, end: Option, } +impl Ipv4Hosts { + /// Returns the number of IPv4 addresses in the range. + /// + /// This method calculates the total number of IPv4 addresses between the current cursor + /// position and the end of the range. If there's no defined end (i.e., the range extends + /// to the maximum possible IPv4 address), it returns the number of addresses from the + /// cursor to the end of the IPv4 address space. + /// + /// # Examples + /// + /// ``` + /// use cidrs::Ipv4Cidr; + /// + /// let cidr: Ipv4Cidr = "192.168.0.0/24".parse().unwrap(); + /// let hosts = cidr.hosts(); + /// assert_eq!(hosts.len(), 254); + /// + /// let cidr: Ipv4Cidr = "10.0.0.0/8".parse().unwrap(); + /// let hosts = cidr.hosts(); + /// assert_eq!(hosts.len(), 16777214); + /// ``` + #[inline] + pub const fn len(&self) -> u32 { + debug_assert!(!(self.end.is_none() && self.cursor == 0)); + + match self.end { + Some(end) => end - self.cursor, + None => u32::MAX - self.cursor + 1, + } + } +} + impl Iterator for Ipv4Hosts { type Item = Ipv4Addr; @@ -379,12 +493,7 @@ impl Iterator for Ipv4Hosts { } fn size_hint(&self) -> (usize, Option) { - let n = match self.end { - Some(end) => end - self.cursor, - None if self.cursor > 0 => u32::MAX - self.cursor + 1, - None => return (usize::MAX, None), - }; - + let n: u32 = self.len(); usize::try_from(n).map_or((usize::MAX, None), |n| (n, Some(n))) } @@ -585,6 +694,33 @@ impl Ipv6Cidr { } } } + + /// Returns the supernet of this CIDR block, if possible. + /// + /// The supernet is the next larger network that contains this CIDR block. + /// If the current CIDR block has 0 bits (representing the entire IPv6 address space), + /// this method returns `None` as there is no larger network possible. + /// + /// # Examples + /// + /// ``` + /// use cidrs::Ipv6Cidr; + /// + /// let cidr = Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 48).unwrap(); + /// let supernet = cidr.supernet().unwrap(); + /// assert_eq!(supernet, Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 47).unwrap()); + /// + /// let entire_space = Ipv6Cidr::new([0, 0, 0, 0, 0, 0, 0, 0], 0).unwrap(); + /// assert_eq!(entire_space.supernet(), None); + /// ``` + #[inline] + pub fn supernet(&self) -> Option { + match self.bits() { + 0 => None, + bits => Some(Ipv6Cidr::from_ip(self.network_addr(), bits - 1).unwrap()), + } + } + /// Returns the subnet mask of the CIDR block. /// /// # Examples @@ -754,11 +890,97 @@ impl FromStr for Ipv6Cidr { } } +/// Implements partial ordering for `Ipv6Cidr`. +/// +/// This implementation delegates to the `Ord` implementation. +/// +/// # Examples +/// +/// ``` +/// use cidrs::Ipv6Cidr; +/// use std::cmp::Ordering; +/// +/// let cidr1 = Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 48).unwrap(); +/// let cidr2 = Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 64).unwrap(); +/// let cidr3 = Ipv6Cidr::new([0x2001, 0xdb9, 0, 0, 0, 0, 0, 0], 48).unwrap(); +/// +/// assert_eq!(cidr1.partial_cmp(&cidr2), Some(Ordering::Less)); +/// assert_eq!(cidr1.partial_cmp(&cidr3), Some(Ordering::Less)); +/// assert_eq!(cidr3.partial_cmp(&cidr1), Some(Ordering::Greater)); +/// ``` +impl PartialOrd for Ipv6Cidr { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Implements total ordering for `Ipv6Cidr`. +/// +/// CIDRs are first compared by their network address, then by their prefix length. +/// +/// # Examples +/// +/// ``` +/// use cidrs::Ipv6Cidr; +/// use std::cmp::Ordering; +/// +/// let cidr1 = Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 48).unwrap(); +/// let cidr2 = Ipv6Cidr::new([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0], 64).unwrap(); +/// let cidr3 = Ipv6Cidr::new([0x2001, 0xdb9, 0, 0, 0, 0, 0, 0], 48).unwrap(); +/// +/// assert_eq!(cidr1.cmp(&cidr2), Ordering::Less); +/// assert_eq!(cidr1.cmp(&cidr3), Ordering::Less); +/// assert_eq!(cidr3.cmp(&cidr1), Ordering::Greater); +/// ``` +impl Ord for Ipv6Cidr { + fn cmp(&self, other: &Self) -> Ordering { + let a = u128::from_be_bytes(self.octets); + let b = u128::from_be_bytes(other.octets); + + match a.cmp(&b) { + Ordering::Equal => self.bits.cmp(&other.bits), + ord => ord, + } + } +} + pub struct Ipv6Hosts { cursor: u128, end: Option, } +impl Ipv6Hosts { + /// Returns the number of IPv6 addresses in the range. + /// + /// This method calculates the total number of IPv6 addresses between the current cursor + /// position and the end of the range. If there's no defined end (i.e., the range extends + /// to the maximum possible IPv6 address), it returns the number of addresses from the + /// cursor to the end of the IPv6 address space. + /// + /// # Examples + /// + /// ``` + /// use cidrs::Ipv6Cidr; + /// + /// let cidr: Ipv6Cidr = "2001:db8::/120".parse().unwrap(); + /// let hosts = cidr.hosts(); + /// assert_eq!(hosts.len(), 254); + /// + /// let cidr: Ipv6Cidr = "2001:db8::/64".parse().unwrap(); + /// let hosts = cidr.hosts(); + /// assert_eq!(hosts.len(), 18446744073709551614); + /// ``` + #[inline] + pub const fn len(&self) -> u128 { + debug_assert!(!(self.end.is_none() && self.cursor == 0)); + + match self.end { + Some(end) => end - self.cursor, + None => u128::MAX - self.cursor + 1, + } + } +} + impl Iterator for Ipv6Hosts { type Item = Ipv6Addr; @@ -779,12 +1001,7 @@ impl Iterator for Ipv6Hosts { } fn size_hint(&self) -> (usize, Option) { - let n = match self.end { - Some(end) => end - self.cursor, - None if self.cursor > 0 => u128::MAX - self.cursor + 1, - None => return (usize::MAX, None), - }; - + let n: u128 = self.len(); usize::try_from(n).map_or((usize::MAX, None), |n| (n, Some(n))) } diff --git a/src/lib.rs b/src/lib.rs index cccbf54..a6686e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ mod cidr; mod error; +mod aggregate; #[cfg(feature = "routing-table")] mod routing_table; @@ -36,3 +37,5 @@ pub use routing_table::{CidrRoutingTable, Ipv4CidrRoutingTable, Ipv6CidrRoutingT pub use cidr::{Cidr, Ipv4Cidr, Ipv6Cidr}; pub use error::{Error, Result}; + +pub use aggregate::{aggregate, aggregate_ipv4, aggregate_ipv6, partition_by_ip_family}; diff --git a/src/routing_table/tree_bitmap/node.rs b/src/routing_table/tree_bitmap/node.rs index f96b522..be6f557 100644 --- a/src/routing_table/tree_bitmap/node.rs +++ b/src/routing_table/tree_bitmap/node.rs @@ -248,9 +248,7 @@ impl Drop for Node { fn drop(&mut self) { for child in self.children.iter_mut() { if let Some(child) = child.take() { - unsafe { - let _ = Box::from_raw(child.as_ptr()); - } + let _ = unsafe { Box::from_raw(child.as_ptr()) }; } } } diff --git a/tests/aggregate.rs b/tests/aggregate.rs new file mode 100644 index 0000000..618233b --- /dev/null +++ b/tests/aggregate.rs @@ -0,0 +1,58 @@ +use cidrs::{aggregate_ipv4, Ipv4Cidr}; + +#[test] +fn ipv4_basic() { + let tests = [ + ( + vec![Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap()], + vec![Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap()], + ), + ( + vec![ + Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(), + ], + vec![Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap()], + ), + ( + vec![ + Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 1, 0], 24).unwrap(), + ], + vec![Ipv4Cidr::new([192, 168, 0, 0], 23).unwrap()], + ), + ( + vec![ + Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 1, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 2, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 3, 0], 24).unwrap(), + ], + vec![Ipv4Cidr::new([192, 168, 0, 0], 22).unwrap()], + ), + ( + vec![ + Ipv4Cidr::new([10, 0, 0, 0], 8).unwrap(), + Ipv4Cidr::new([172, 16, 0, 0], 12).unwrap(), + Ipv4Cidr::new([192, 168, 0, 0], 16).unwrap(), + ], + vec![ + Ipv4Cidr::new([10, 0, 0, 0], 8).unwrap(), + Ipv4Cidr::new([172, 16, 0, 0], 12).unwrap(), + Ipv4Cidr::new([192, 168, 0, 0], 16).unwrap(), + ], + ), + ( + vec![ + Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap(), + Ipv4Cidr::new([192, 168, 0, 128], 25).unwrap(), + ], + vec![Ipv4Cidr::new([192, 168, 0, 0], 24).unwrap()], + ), + ]; + + for (input, expected) in tests { + let actual = aggregate_ipv4(&input); + assert_eq!(actual, expected, "input: {input:?}"); + } +}