use std::net::Ipv4Addr;
use std::path::Path;
use anyhow::{Context, Result};
use ipnetwork::{IpNetwork, Ipv4Network};
use maxminddb::{geoip2, Reader, WithinOptions};
use super::chnroute::ChnRoute;
pub fn load_country_routes(path: impl AsRef<Path>, country: &str) -> Result<ChnRoute> {
let path = path.as_ref();
let reader = Reader::open_readfile(path)
.with_context(|| format!("opening GeoIP database {}", path.display()))?;
let all_v4 = IpNetwork::V4(
Ipv4Network::new(Ipv4Addr::UNSPECIFIED, 0).expect("0.0.0.0/0 is a valid network"),
);
let mut ranges: Vec<(u32, u32)> = Vec::new();
for item in reader
.within(all_v4, WithinOptions::default())
.context("iterating GeoIP networks")?
{
let item = item.context("decoding a GeoIP network")?;
let net = match item.network().context("reading GeoIP network")? {
IpNetwork::V4(v4) => v4,
IpNetwork::V6(_) => continue,
};
let record: Option<geoip2::Country> = item.decode().context("decoding GeoIP country")?;
if country_matches(record.as_ref(), country) {
ranges.push((u32::from(net.network()), u32::from(net.broadcast())));
}
}
Ok(ChnRoute::from_ranges(ranges))
}
fn country_matches(record: Option<&geoip2::Country>, want: &str) -> bool {
record
.and_then(|r| r.country.iso_code)
.is_some_and(|iso| iso.eq_ignore_ascii_case(want))
}
#[cfg(test)]
mod tests {
use super::*;
fn record(iso: Option<&'static str>) -> geoip2::Country<'static> {
let mut c = geoip2::Country::default();
c.country.iso_code = iso;
c
}
#[test]
fn matches_iso_code_case_insensitively() {
let cn = record(Some("CN"));
assert!(country_matches(Some(&cn), "CN"));
assert!(country_matches(Some(&cn), "cn")); assert!(!country_matches(Some(&cn), "US"));
assert!(!country_matches(Some(&record(None)), "CN")); assert!(!country_matches(None, "CN")); }
#[test]
fn missing_database_is_an_error() {
assert!(load_country_routes("/nonexistent/GeoLite2-Country.mmdb", "CN").is_err());
}
}