use std::net::Ipv4Addr;
use std::path::Path;
#[derive(Debug, Default, Clone)]
pub struct ChnRoute {
ranges: Vec<(u32, u32)>,
}
impl ChnRoute {
pub fn from_lines<I, S>(lines: I) -> Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
let mut ranges: Vec<(u32, u32)> = Vec::new();
for line in lines {
if let Some(r) = parse_cidr(line.as_ref()) {
ranges.push(r);
}
}
Self {
ranges: merge(ranges),
}
}
pub fn load(path: impl AsRef<Path>) -> std::io::Result<Self> {
let text = std::fs::read_to_string(path)?;
Ok(Self::from_lines(text.lines()))
}
pub fn from_ranges(ranges: Vec<(u32, u32)>) -> Self {
Self {
ranges: merge(ranges),
}
}
pub fn len(&self) -> usize {
self.ranges.len()
}
pub fn is_empty(&self) -> bool {
self.ranges.is_empty()
}
pub fn contains(&self, ip: Ipv4Addr) -> bool {
let v = u32::from(ip);
match self.ranges.binary_search_by(|&(start, _)| start.cmp(&v)) {
Ok(_) => true, Err(0) => false,
Err(idx) => {
let (_, end) = self.ranges[idx - 1];
v <= end
}
}
}
}
fn parse_cidr(line: &str) -> Option<(u32, u32)> {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
return None;
}
let (addr, len) = line.split_once('/')?;
let base = u32::from(addr.trim().parse::<Ipv4Addr>().ok()?);
let len: u32 = len.trim().parse().ok()?;
if len > 32 {
return None;
}
let mask = if len == 0 { 0 } else { u32::MAX << (32 - len) };
let start = base & mask;
let end = start | !mask;
Some((start, end))
}
fn merge(mut ranges: Vec<(u32, u32)>) -> Vec<(u32, u32)> {
ranges.sort_unstable();
let mut out: Vec<(u32, u32)> = Vec::with_capacity(ranges.len());
for (start, end) in ranges {
if let Some(last) = out.last_mut() {
if start <= last.1 || (last.1 != u32::MAX && start == last.1 + 1) {
if end > last.1 {
last.1 = end;
}
continue;
}
}
out.push((start, end));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn ip(s: &str) -> Ipv4Addr {
s.parse().unwrap()
}
#[test]
fn parses_and_merges() {
let r = ChnRoute::from_lines([
"# comment",
"1.0.1.0/24",
"1.0.2.0/23", "8.8.8.0/24",
"garbage",
"",
]);
assert_eq!(r.len(), 2);
}
#[test]
fn contains_china_and_excludes_others() {
let r = ChnRoute::from_lines(["1.0.1.0/24", "114.114.114.0/24"]);
assert!(r.contains(ip("1.0.1.0")));
assert!(r.contains(ip("1.0.1.255")));
assert!(r.contains(ip("114.114.114.114")));
assert!(!r.contains(ip("1.0.2.0")));
assert!(!r.contains(ip("8.8.8.8"))); assert!(!r.contains(ip("0.0.0.0")));
assert!(!r.contains(ip("255.255.255.255")));
}
#[test]
fn from_ranges_merges_and_looks_up() {
let r = ChnRoute::from_ranges(vec![
(u32::from(ip("1.0.0.0")), u32::from(ip("1.0.0.255"))),
(u32::from(ip("1.0.1.0")), u32::from(ip("1.0.1.255"))),
(u32::from(ip("8.8.8.0")), u32::from(ip("8.8.8.255"))),
]);
assert_eq!(r.len(), 2);
assert!(r.contains(ip("1.0.0.7")));
assert!(r.contains(ip("1.0.1.7")));
assert!(r.contains(ip("8.8.8.8")));
assert!(!r.contains(ip("1.0.2.0")));
}
#[test]
fn full_space_contains_everything() {
let r = ChnRoute::from_lines(["0.0.0.0/0"]);
assert!(r.contains(ip("0.0.0.0")));
assert!(r.contains(ip("255.255.255.255")));
assert!(r.contains(ip("123.45.67.89")));
}
}