#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
#[cfg(all(feature = "simdutf8", feature = "unsafe-str-decode"))]
compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusive");
mod decoder;
mod error;
pub mod geoip2;
mod metadata;
mod reader;
mod result;
mod within;
pub use error::MaxMindDbError;
pub use metadata::Metadata;
pub use reader::Reader;
pub use result::{LookupResult, PathElement};
pub use within::{Within, WithinOptions};
#[cfg(feature = "mmap")]
pub use memmap2::Mmap;
#[cfg(test)]
mod reader_test;
#[cfg(test)]
mod tests {
use super::*;
use std::net::IpAddr;
#[test]
fn test_lookup_network() {
use std::collections::HashMap;
struct TestCase {
ip: &'static str,
db_file: &'static str,
expected_network: &'static str,
expected_found: bool,
}
let test_cases = [
TestCase {
ip: "1.1.1.1",
db_file: "test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb",
expected_network: "1.0.0.0/8",
expected_found: false,
},
TestCase {
ip: "::1:ffff:ffff",
db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
expected_network: "::1:ffff:ffff/128",
expected_found: true,
},
TestCase {
ip: "::2:0:1",
db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
expected_network: "::2:0:0/122",
expected_found: true,
},
TestCase {
ip: "1.1.1.1",
db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
expected_network: "1.1.1.1/32",
expected_found: true,
},
TestCase {
ip: "1.1.1.3",
db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
expected_network: "1.1.1.2/31",
expected_found: true,
},
TestCase {
ip: "1.1.1.3",
db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
expected_network: "1.1.1.0/24",
expected_found: true,
},
TestCase {
ip: "::ffff:1.1.1.128",
db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
expected_network: "::ffff:1.1.1.0/120",
expected_found: true,
},
TestCase {
ip: "::1.1.1.128",
db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
expected_network: "::101:100/120",
expected_found: true,
},
TestCase {
ip: "200.0.2.1",
db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
expected_network: "::/64",
expected_found: true,
},
TestCase {
ip: "::200.0.2.1",
db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
expected_network: "::/64",
expected_found: true,
},
TestCase {
ip: "0:0:0:0:ffff:ffff:ffff:ffff",
db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
expected_network: "::/64",
expected_found: true,
},
TestCase {
ip: "ef00::",
db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
expected_network: "8000::/1",
expected_found: false,
},
];
let mut readers: HashMap<&str, Reader<Vec<u8>>> = HashMap::new();
for test in &test_cases {
let reader = readers
.entry(test.db_file)
.or_insert_with(|| Reader::open_readfile(test.db_file).unwrap());
let ip: IpAddr = test.ip.parse().unwrap();
let result = reader.lookup(ip).unwrap();
assert_eq!(
result.has_data(),
test.expected_found,
"IP {} in {}: expected has_data={}, got has_data={}",
test.ip,
test.db_file,
test.expected_found,
result.has_data()
);
let network = result.network().unwrap();
assert_eq!(
network.to_string(),
test.expected_network,
"IP {} in {}: expected network {}, got {}",
test.ip,
test.db_file,
test.expected_network,
network
);
}
}
#[test]
fn test_lookup_with_geoip_data() {
let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
let ip: IpAddr = "89.160.20.128".parse().unwrap();
let result = reader.lookup(ip).unwrap();
assert!(result.has_data(), "lookup should find known IP");
let city: geoip2::City = result.decode().unwrap().unwrap();
assert!(!city.city.is_empty(), "Expected city data");
let network = result.network().unwrap();
assert_eq!(
network.to_string(),
"89.160.20.128/25",
"Expected network 89.160.20.128/25"
);
assert!(
result.offset().is_some(),
"Expected offset to be Some for found IP"
);
}
#[test]
fn test_lookup_network_uses_measured_ipv4_subtree_depth() {
let mut reader =
Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb").unwrap();
assert_eq!(reader.metadata.ip_version, 6);
reader.ipv4_start_bit_depth = 16;
let result = reader.lookup("1.1.1.1".parse().unwrap()).unwrap();
assert_eq!(result.network().unwrap().to_string(), "1.0.0.0/8");
}
#[test]
fn test_lookup_offset_is_stable_for_shared_record() {
let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
let first = reader.lookup("89.160.20.128".parse().unwrap()).unwrap();
let second = reader.lookup("89.160.20.129".parse().unwrap()).unwrap();
assert!(first.has_data());
assert!(second.has_data());
assert_eq!(first.network().unwrap(), second.network().unwrap());
assert_eq!(
first.offset(),
second.offset(),
"IPs in the same record should share a cacheable offset"
);
}
#[test]
fn test_decode_path() {
let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
let ip: IpAddr = "89.160.20.128".parse().unwrap();
let result = reader.lookup(ip).unwrap();
let iso_code: Option<String> = result
.decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
.unwrap();
assert_eq!(iso_code, Some("SE".to_owned()));
let missing: Option<String> = result
.decode_path(&[PathElement::Key("nonexistent")])
.unwrap();
assert!(missing.is_none());
}
#[test]
fn test_decode_path_on_not_found_lookup() {
let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
let ip: IpAddr = "2c0f:ff00::1".parse().unwrap();
let result = reader.lookup(ip).unwrap();
assert!(!result.has_data());
assert!(result.offset().is_none());
assert!(result.decode::<geoip2::City>().unwrap().is_none());
let country_code: Option<String> = result
.decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
.unwrap();
assert!(country_code.is_none());
}
#[test]
fn test_ipv6_in_ipv4_database() {
let reader =
Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb").unwrap();
let ip: IpAddr = "2001::".parse().unwrap();
let result = reader.lookup(ip);
match result {
Err(MaxMindDbError::InvalidInput { message }) => {
assert!(
message.contains("IPv6") && message.contains("IPv4"),
"Expected error message about IPv6 in IPv4 database, got: {}",
message
);
}
Err(e) => panic!(
"Expected InvalidInput error for IPv6 in IPv4 database, got: {:?}",
e
),
Ok(_) => panic!("Expected error for IPv6 lookup in IPv4-only database"),
}
}
#[test]
fn test_decode_path_comprehensive() {
let reader =
Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap();
let ip: IpAddr = "::1.1.1.0".parse().unwrap();
let result = reader.lookup(ip).unwrap();
assert!(result.has_data());
let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
assert_eq!(u16_val, Some(100));
let arr_first: Option<u32> = result
.decode_path(&[PathElement::Key("array"), PathElement::Index(0)])
.unwrap();
assert_eq!(arr_first, Some(1));
let arr_last: Option<u32> = result
.decode_path(&[PathElement::Key("array"), PathElement::Index(2)])
.unwrap();
assert_eq!(arr_last, Some(3));
let arr_oob: Option<u32> = result
.decode_path(&[PathElement::Key("array"), PathElement::Index(3)])
.unwrap();
assert!(arr_oob.is_none());
let arr_last: Option<u32> = result
.decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(0)])
.unwrap();
assert_eq!(arr_last, Some(3));
let arr_first: Option<u32> = result
.decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(2)])
.unwrap();
assert_eq!(arr_first, Some(1));
let nested: Option<u32> = result
.decode_path(&[
PathElement::Key("map"),
PathElement::Key("mapX"),
PathElement::Key("arrayX"),
PathElement::Index(1),
])
.unwrap();
assert_eq!(nested, Some(8));
let missing: Option<u32> = result
.decode_path(&[PathElement::Key("does-not-exist"), PathElement::Index(1)])
.unwrap();
assert!(missing.is_none());
let utf8: Option<String> = result
.decode_path(&[PathElement::Key("utf8_string")])
.unwrap();
assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
}
}