#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
#![warn(noop_method_call)]
#![warn(unreachable_pub)]
#![warn(clippy::all)]
#![deny(clippy::await_holding_lock)]
#![deny(clippy::cargo_common_metadata)]
#![deny(clippy::cast_lossless)]
#![deny(clippy::checked_conversions)]
#![warn(clippy::cognitive_complexity)]
#![deny(clippy::debug_assert_with_mut_call)]
#![deny(clippy::exhaustive_enums)]
#![deny(clippy::exhaustive_structs)]
#![deny(clippy::expl_impl_clone_on_copy)]
#![deny(clippy::fallible_impl_from)]
#![deny(clippy::implicit_clone)]
#![deny(clippy::large_stack_arrays)]
#![warn(clippy::manual_ok_or)]
#![deny(clippy::missing_docs_in_private_items)]
#![warn(clippy::needless_borrow)]
#![warn(clippy::needless_pass_by_value)]
#![warn(clippy::option_option)]
#![deny(clippy::print_stderr)]
#![deny(clippy::print_stdout)]
#![warn(clippy::rc_buffer)]
#![deny(clippy::ref_option_ref)]
#![warn(clippy::semicolon_if_nothing_returned)]
#![warn(clippy::trait_duplication_in_bounds)]
#![deny(clippy::unchecked_time_subtraction)]
#![deny(clippy::unnecessary_wraps)]
#![warn(clippy::unseparated_literal_suffix)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::mod_module_files)]
#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] #![allow(mismatched_lifetime_syntaxes)] #![allow(clippy::collapsible_if)] #![deny(clippy::unused_async)]
#![cfg_attr(not(all(feature = "full")), allow(unused))]
pub use crate::err::Error;
use rangemap::RangeInclusiveMap;
use std::fmt::{Debug, Display, Formatter};
use std::net::{IpAddr, Ipv6Addr};
use std::num::{NonZeroU8, NonZeroU32, TryFromIntError};
use std::str::FromStr;
use std::sync::{Arc, OnceLock};
mod err;
#[cfg(feature = "embedded-db")]
static EMBEDDED_DB_V4: &str = include_str!("../data/geoip");
#[cfg(feature = "embedded-db")]
static EMBEDDED_DB_V6: &str = include_str!("../data/geoip6");
#[cfg(feature = "embedded-db")]
static EMBEDDED_DB_PARSED: OnceLock<Arc<GeoipDb>> = OnceLock::new();
#[derive(Copy, Clone, Eq, PartialEq)]
pub struct CountryCode {
inner: [NonZeroU8; 2],
}
impl CountryCode {
fn new(cc_orig: &str) -> Result<Self, Error> {
#[inline]
fn try_cvt_to_nz(inp: [u8; 2]) -> Result<[NonZeroU8; 2], TryFromIntError> {
Ok([inp[0].try_into()?, inp[1].try_into()?])
}
let cc = cc_orig.to_ascii_uppercase();
let cc: [u8; 2] = cc
.as_bytes()
.try_into()
.map_err(|_| Error::BadCountryCode(cc))?;
if !cc.iter().all(|b| b.is_ascii() && !b.is_ascii_control()) {
return Err(Error::BadCountryCode(cc_orig.to_owned()));
}
if &cc == b"??" {
return Err(Error::NowhereNotSupported);
}
Ok(Self {
inner: try_cvt_to_nz(cc).map_err(|_| Error::BadCountryCode(cc_orig.to_owned()))?,
})
}
pub fn get(&self) -> &str {
self.as_ref()
}
}
impl Display for CountryCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl Debug for CountryCode {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "CountryCode(\"{}\")", self.as_ref())
}
}
impl AsRef<str> for CountryCode {
fn as_ref(&self) -> &str {
#[inline]
fn cvt_ref(inp: &[NonZeroU8; 2]) -> &[u8; 2] {
let ptr = inp.as_ptr() as *const u8;
let slice = unsafe { std::slice::from_raw_parts(ptr, inp.len()) };
slice
.try_into()
.expect("the resulting slice should have the correct length!")
}
std::str::from_utf8(cvt_ref(&self.inner)).expect("invalid country code in CountryCode")
}
}
impl FromStr for CountryCode {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
CountryCode::new(s)
}
}
#[derive(
Copy, Clone, Debug, Eq, PartialEq, derive_more::Into, derive_more::From, derive_more::AsRef,
)]
#[allow(clippy::exhaustive_structs)]
pub struct OptionCc(pub Option<CountryCode>);
impl FromStr for OptionCc {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match CountryCode::new(s) {
Err(Error::NowhereNotSupported) => Ok(None.into()),
Err(e) => Err(e),
Ok(cc) => Ok(Some(cc).into()),
}
}
}
impl Display for OptionCc {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self.0 {
Some(cc) => write!(f, "{}", cc),
None => write!(f, "??"),
}
}
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
struct NetDefn {
cc: Option<CountryCode>,
asn: Option<NonZeroU32>,
}
impl NetDefn {
fn new(cc: &str, asn: Option<u32>) -> Result<Self, Error> {
let asn = NonZeroU32::new(asn.unwrap_or(0));
let cc = cc.parse::<OptionCc>()?.into();
Ok(Self { cc, asn })
}
fn country_code(&self) -> Option<&CountryCode> {
self.cc.as_ref()
}
fn asn(&self) -> Option<u32> {
self.asn.as_ref().map(|x| x.get())
}
}
#[derive(Clone, Eq, PartialEq, Debug)]
pub struct GeoipDb {
map_v4: RangeInclusiveMap<u32, NetDefn>,
map_v6: RangeInclusiveMap<u128, NetDefn>,
}
impl GeoipDb {
#[cfg(feature = "embedded-db")]
pub fn new_embedded() -> Arc<Self> {
Arc::clone(EMBEDDED_DB_PARSED.get_or_init(|| {
Arc::new(
Self::new_from_legacy_format(EMBEDDED_DB_V4, EMBEDDED_DB_V6)
.expect("failed to parse embedded geoip database"),
)
}))
}
pub fn new_from_legacy_format(db_v4: &str, db_v6: &str) -> Result<Self, Error> {
let mut ret = GeoipDb {
map_v4: Default::default(),
map_v6: Default::default(),
};
for line in db_v4.lines() {
if line.starts_with('#') {
continue;
}
let line = line.trim();
if line.is_empty() {
continue;
}
let mut split = line.split(',');
let from = split
.next()
.ok_or(Error::BadFormat("empty line somehow?"))?
.parse::<u32>()?;
let to = split
.next()
.ok_or(Error::BadFormat("line with insufficient commas"))?
.parse::<u32>()?;
let cc = split
.next()
.ok_or(Error::BadFormat("line with insufficient commas"))?;
let asn = split.next().map(|x| x.parse::<u32>()).transpose()?;
let defn = NetDefn::new(cc, asn)?;
ret.map_v4.insert(from..=to, defn);
}
for line in db_v6.lines() {
if line.starts_with('#') {
continue;
}
let line = line.trim();
if line.is_empty() {
continue;
}
let mut split = line.split(',');
let from = split
.next()
.ok_or(Error::BadFormat("empty line somehow?"))?
.parse::<Ipv6Addr>()?;
let to = split
.next()
.ok_or(Error::BadFormat("line with insufficient commas"))?
.parse::<Ipv6Addr>()?;
let cc = split
.next()
.ok_or(Error::BadFormat("line with insufficient commas"))?;
let asn = split.next().map(|x| x.parse::<u32>()).transpose()?;
let defn = NetDefn::new(cc, asn)?;
ret.map_v6.insert(from.into()..=to.into(), defn);
}
Ok(ret)
}
fn lookup_defn(&self, ip: IpAddr) -> Option<&NetDefn> {
match ip {
IpAddr::V4(v4) => self.map_v4.get(&v4.into()),
IpAddr::V6(v6) => self.map_v6.get(&v6.into()),
}
}
pub fn lookup_country_code(&self, ip: IpAddr) -> Option<&CountryCode> {
self.lookup_defn(ip).and_then(|x| x.country_code())
}
pub fn lookup_country_code_multi<I>(&self, ips: I) -> Option<&CountryCode>
where
I: IntoIterator<Item = IpAddr>,
{
let mut ret = None;
for ip in ips {
if let Some(cc) = self.lookup_country_code(ip) {
if ret.is_some() && ret != Some(cc) {
return None;
}
ret = Some(cc);
}
}
ret
}
pub fn lookup_asn(&self, ip: IpAddr) -> Option<u32> {
self.lookup_defn(ip)?.asn()
}
}
pub trait HasCountryCode {
fn country_code(&self) -> Option<CountryCode>;
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use std::net::Ipv4Addr;
#[test]
#[cfg(feature = "embedded-db")]
fn embedded_db() {
let db = GeoipDb::new_embedded();
assert_eq!(
db.lookup_country_code(Ipv4Addr::new(8, 8, 8, 8).into())
.map(|x| x.as_ref()),
Some("US")
);
assert_eq!(
db.lookup_country_code("2001:4860:4860::8888".parse().unwrap())
.map(|x| x.as_ref()),
Some("US")
);
}
#[test]
fn basic_lookups() {
let src_v4 = r#"
16909056,16909311,GB
"#;
let src_v6 = r#"
fe80::,fe81::,US
dead:beef::,dead:ffff::,??
"#;
let db = GeoipDb::new_from_legacy_format(src_v4, src_v6).unwrap();
assert_eq!(
db.lookup_country_code(Ipv4Addr::new(1, 2, 3, 4).into())
.map(|x| x.as_ref()),
Some("GB")
);
assert_eq!(
db.lookup_country_code(Ipv4Addr::new(1, 1, 1, 1).into()),
None
);
assert_eq!(
db.lookup_country_code("fe80::dead:beef".parse().unwrap())
.map(|x| x.as_ref()),
Some("US")
);
assert_eq!(
db.lookup_country_code("fe81::dead:beef".parse().unwrap()),
None
);
assert_eq!(
db.lookup_country_code("dead:beef::1".parse().unwrap()),
None
);
}
#[test]
fn cc_parse() -> Result<(), Error> {
assert_eq!(CountryCode::from_str("us")?, CountryCode::from_str("US")?);
assert_eq!(CountryCode::from_str("UY")?, CountryCode::from_str("UY")?);
assert_eq!(CountryCode::from_str("A7")?, CountryCode::from_str("a7")?);
assert_eq!(CountryCode::from_str("xz")?, CountryCode::from_str("xz")?);
assert!(matches!(
CountryCode::from_str("z"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("🐻❄️"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("Sheboygan"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("\r\n"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("\0\0"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("¡"),
Err(Error::BadCountryCode(_))
));
assert!(matches!(
CountryCode::from_str("??"),
Err(Error::NowhereNotSupported)
));
Ok(())
}
#[test]
fn opt_cc_parse() -> Result<(), Error> {
assert_eq!(
CountryCode::from_str("br")?,
OptionCc::from_str("BR")?.0.unwrap()
);
assert!(OptionCc::from_str("??")?.0.is_none());
Ok(())
}
}