ys1r/
ipaggregator.rs

1use std::net::Ipv4Addr;
2
3/// Represents an IPv4 CIDR block (address + prefix length)
4#[derive(Debug, Clone)]
5pub struct Cidr {
6    pub addr: Ipv4Addr,
7    pub prefix: u8,
8}
9
10#[allow(unused)]
11fn ip_to_u32(ip: &str) -> u32 {
12    let addr: Ipv4Addr = ip.parse().unwrap();
13    u32::from(addr)
14}
15
16#[allow(unused)]
17fn u32_to_ip(n: u32) -> Ipv4Addr {
18    Ipv4Addr::from(n)
19}
20
21#[allow(unused)]
22fn is_continuous(block: &[u32]) -> bool {
23    block.windows(2).all(|w| w[1] == w[0] + 1)
24}
25
26#[allow(unused)]
27fn is_aligned(start: u32, size: usize) -> bool {
28    start.is_multiple_of(size as u32)
29}
30
31#[allow(unused)]
32/// Converts a CIDR block into ACL format:
33/// - /32 → "host x.x.x.x"
34/// - otherwise → "network subnet-mask"
35pub fn cidr_to_acl(cidr: &Cidr) -> String {
36    if cidr.prefix == 32 {
37        return format!("host {}", cidr.addr);
38    }
39
40    // Build subnet mask from prefix
41    let mask = u32::MAX << (32 - cidr.prefix);
42    let octets = mask.to_be_bytes();
43
44    format!(
45        "{} {}.{}.{}.{}",
46        cidr.addr, octets[0], octets[1], octets[2], octets[3]
47    )
48}
49
50#[allow(unused)]
51fn is_valid_segment(i: usize, next_size: usize, ints: &[u32]) -> bool {
52    i + next_size <= ints.len()
53        && is_aligned(ints[i], next_size)
54        && is_continuous(&ints[i..i + next_size])
55}
56
57fn build_cidr(start_ip: &u32, prefix: u8) -> Cidr {
58    let mask: u32 = if prefix == 0 {
59        0
60    } else {
61        (!0u32) << (32 - prefix)
62    };
63    let network = start_ip & mask;
64
65    Cidr {
66        addr: Ipv4Addr::from(network),
67        prefix,
68    }
69}
70
71/// Aggregate a list of IPv4 addresses into the smallest possible set of CIDR blocks.
72///
73/// This function takes a slice of IPv4 addresses and returns a `Vec<Cidr>`
74/// representing the minimal set of CIDR blocks that cover all the input IPs.
75/// It works by:
76/// 1. Converting IPs to their `u32` integer representation.
77/// 2. Sorting the integers to process them in order.
78/// 3. Iteratively finding the largest contiguous, properly aligned block
79///    starting at each IP.
80/// 4. Calculating the CIDR prefix for each block and creating a `Cidr` struct.
81///
82/// # Arguments
83///
84/// * `ip_list` - A slice of `Ipv4Addr` to aggregate.
85///
86/// # Returns
87///
88/// A `Vec<Cidr>` containing the aggregated CIDR blocks.
89///
90/// # Example
91///
92/// ```rust
93/// use std::net::Ipv4Addr;
94/// use ys1r::ipaggregator::aggregate_ips;
95/// use ys1r::ipaggregator::cidr_to_acl;
96///
97/// let mut ip_list: Vec<Ipv4Addr> = Vec::new();
98/// for i in 1..=254 {
99///     ip_list.push(Ipv4Addr::new(10, 0, 0, i));
100/// }
101///
102/// let cidrs = aggregate_ips(&ip_list);
103///
104/// for c in &cidrs {
105///     println!("{}\t<=\t{:?}", cidr_to_acl(c), &c);
106/// }
107/// ```
108///
109/// This will produce the minimal CIDR blocks covering 10.0.0.1–10.0.0.254.
110pub fn aggregate_ips(ip_list: &[Ipv4Addr]) -> Vec<Cidr> {
111    let mut ints: Vec<u32> = ip_list.iter().map(|ip| u32::from(*ip)).collect();
112    ints.sort();
113
114    let mut result = Vec::new();
115    let mut i = 0;
116
117    while i < ints.len() {
118        let mut size = 1;
119
120        loop {
121            let next_size = size * 2;
122            if !is_valid_segment(i, next_size, &ints) {
123                break;
124            }
125            size = next_size;
126        }
127
128        let prefix = (32 - size.trailing_zeros()) as u8;
129        result.push(build_cidr(&ints[i], prefix));
130        i += size;
131    }
132    result
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use std::net::Ipv4Addr;
139
140    #[test]
141    fn test_ip_to_u32_and_back() {
142        let ip_str = "192.168.1.1";
143        let ip_num = ip_to_u32(ip_str);
144        let ip_back = u32_to_ip(ip_num);
145        assert_eq!(ip_back, Ipv4Addr::new(192, 168, 1, 1));
146    }
147
148    #[test]
149    fn test_is_continuous() {
150        let block = vec![1, 2, 3, 4, 5];
151        assert!(is_continuous(&block));
152        let block2 = vec![1, 2, 4, 5];
153        assert!(!is_continuous(&block2));
154    }
155
156    #[test]
157    fn test_is_aligned() {
158        assert!(is_aligned(8, 4)); // 8 % 4 == 0
159        assert!(!is_aligned(7, 4)); // 7 % 4 != 0
160    }
161
162    #[test]
163    fn test_aggregate_ips_basic() {
164        // Input: 10.0.0.1 to 10.0.0.4
165        let mut ips: Vec<Ipv4Addr> = Vec::new();
166        for i in 1..=4 {
167            ips.push(Ipv4Addr::new(10, 0, 0, i));
168        }
169
170        let cidrs = aggregate_ips(&ips);
171
172        let expected = vec![
173            Cidr {
174                addr: Ipv4Addr::new(10, 0, 0, 1),
175                prefix: 32,
176            },
177            Cidr {
178                addr: Ipv4Addr::new(10, 0, 0, 2),
179                prefix: 31,
180            },
181            Cidr {
182                addr: Ipv4Addr::new(10, 0, 0, 4),
183                prefix: 32,
184            },
185        ];
186
187        assert_eq!(cidrs.len(), expected.len());
188        assert_eq!(cidrs[0].addr, expected[0].addr);
189        assert_eq!(cidrs[0].prefix, expected[0].prefix);
190    }
191
192    #[test]
193    fn test_aggregate_ips_full_range() {
194        // Generate 10.0.0.1 - 10.0.0.8
195        let mut ips: Vec<Ipv4Addr> = Vec::new();
196        for i in 1..=8 {
197            ips.push(Ipv4Addr::new(10, 0, 0, i));
198        }
199
200        let cidrs = aggregate_ips(&ips);
201
202        assert_eq!(cidrs.len(), 4);
203        assert_eq!(cidrs[0].addr, Ipv4Addr::new(10, 0, 0, 1));
204        assert_eq!(cidrs[0].prefix, 32);
205
206        assert_eq!(cidrs[1].addr, Ipv4Addr::new(10, 0, 0, 2));
207        assert_eq!(cidrs[1].prefix, 31);
208
209        assert_eq!(cidrs[2].addr, Ipv4Addr::new(10, 0, 0, 4));
210        assert_eq!(cidrs[2].prefix, 30);
211
212        assert_eq!(cidrs[3].addr, Ipv4Addr::new(10, 0, 0, 8));
213        assert_eq!(cidrs[3].prefix, 32);
214    }
215}