use std::net::IpAddr;
use std::path::Path;
use maxminddb::Reader;
use crate::error::RulesError;
pub struct GeoipMatcher {
reader: Reader<Vec<u8>>,
}
impl GeoipMatcher {
pub fn from_file(path: &Path) -> Result<Self, RulesError> {
let reader = Reader::open_readfile(path).map_err(|e| {
RulesError::GeoIp(format!(
"failed to open GeoIP database {}: {e}",
path.display()
))
})?;
Ok(Self { reader })
}
pub fn from_bytes(data: Vec<u8>) -> Result<Self, RulesError> {
let reader = Reader::from_source(data)
.map_err(|e| RulesError::GeoIp(format!("failed to parse GeoIP database: {e}")))?;
Ok(Self { reader })
}
pub fn country_code(&self, ip: IpAddr) -> Option<String> {
if let Ok(result) = self.reader.lookup(ip)
&& let Ok(Some(country)) = result.decode::<maxminddb::geoip2::Country>()
&& let Some(code) = country.country.iso_code
{
return Some(code.to_uppercase());
}
if let Ok(result) = self.reader.lookup(ip)
&& let Ok(Some(city)) = result.decode::<maxminddb::geoip2::City>()
&& let Some(code) = city.country.iso_code
{
return Some(code.to_uppercase());
}
None
}
pub fn matches(&self, ip: IpAddr, code: &str) -> bool {
self.country_code(ip)
.is_some_and(|c| c.eq_ignore_ascii_case(code))
}
}
impl std::fmt::Debug for GeoipMatcher {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GeoipMatcher").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn geoip_matcher_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<GeoipMatcher>();
}
}