use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::net::IpAddr;
use std::sync::Mutex;
use crate::geo::{Found, GeoLookup};
use crate::js::{self, Object, Value};
use crate::{BoxError, BoxFuture};
const METADATA_MARKER: &[u8] = b"\xAB\xCD\xEFMaxMind.com";
const METADATA_MAX: u64 = 131_072;
const PAGE: u64 = 4096;
const PAGES_KEPT: usize = 256;
enum Source {
Bytes(Vec<u8>),
File(Mutex<(File, HashMap<u64, Vec<u8>>)>),
}
pub struct Mmdb {
source: Source,
size: u64,
pub metadata: Value,
node_count: u64,
record_size: u64,
node_bytes: u64,
data_start: u64,
ipv4_start: Mutex<Option<u64>>,
}
#[derive(Debug)]
pub struct MmdbError(pub String);
impl std::fmt::Display for MmdbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for MmdbError {}
fn bad(what: &str) -> MmdbError {
MmdbError(what.to_string())
}
impl Mmdb {
pub fn from_bytes(bytes: Vec<u8>) -> Result<Mmdb, MmdbError> {
let size = bytes.len() as u64;
Mmdb::with(Source::Bytes(bytes), size)
}
pub fn open(path: impl AsRef<std::path::Path>) -> Result<Mmdb, MmdbError> {
let path = path.as_ref();
let file = File::open(path).map_err(|_| MmdbError(format!("Could not read {}", path.display())))?;
let size = file.metadata().map_err(|e| MmdbError(e.to_string()))?.len();
Mmdb::with(Source::File(Mutex::new((file, HashMap::new()))), size)
}
fn with(source: Source, size: u64) -> Result<Mmdb, MmdbError> {
let mut db = Mmdb {
source,
size,
metadata: Value::Null,
node_count: 0,
record_size: 0,
node_bytes: 0,
data_start: 0,
ipv4_start: Mutex::new(None),
};
let tail_start = size.saturating_sub(METADATA_MAX);
let tail = db.read(tail_start, size - tail_start)?;
let at = tail
.windows(METADATA_MARKER.len())
.rposition(|w| w == METADATA_MARKER)
.ok_or_else(|| bad("Not a MaxMind DB file: no metadata"))?;
let start = tail_start + at as u64 + METADATA_MARKER.len() as u64;
let (metadata, _) = db.decode(start, start, 0)?;
let num = |k: &str| metadata.get(k).and_then(Value::as_f64);
let (Some(nodes), Some(record), Some(_)) = (num("node_count"), num("record_size"), num("ip_version")) else {
return Err(bad("Not a MaxMind DB file: bad metadata"));
};
if ![24.0, 28.0, 32.0].contains(&record) {
return Err(MmdbError(format!("Unsupported record size {record}")));
}
db.node_count = nodes as u64;
db.record_size = record as u64;
db.node_bytes = db.record_size / 4;
db.data_start = db.node_count * db.node_bytes + 16;
db.metadata = metadata;
Ok(db)
}
fn ip_version(&self) -> u64 {
self.metadata.get("ip_version").and_then(Value::as_f64).unwrap_or(0.0) as u64
}
fn read(&self, at: u64, length: u64) -> Result<Vec<u8>, MmdbError> {
let end = (at + length).min(self.size);
match &self.source {
Source::Bytes(b) => Ok(b.get(at as usize..end as usize).unwrap_or(&[]).to_vec()),
Source::File(file) => {
let mut guard = file.lock().unwrap_or_else(|e| e.into_inner());
let (handle, pages) = &mut *guard;
let mut out = Vec::new();
let mut at = at;
while at < end {
let number = at / PAGE;
if !pages.contains_key(&number) {
if pages.len() >= PAGES_KEPT {
pages.clear();
}
let mut page = Vec::with_capacity(PAGE as usize);
handle.seek(SeekFrom::Start(number * PAGE)).map_err(|e| MmdbError(e.to_string()))?;
handle.by_ref().take(PAGE).read_to_end(&mut page).map_err(|e| MmdbError(e.to_string()))?;
pages.insert(number, page);
}
let page = &pages[&number];
let offset = (at - number * PAGE) as usize;
let take = ((end - at) as usize).min(page.len().saturating_sub(offset));
if take == 0 {
break;
}
out.extend_from_slice(&page[offset..offset + take]);
at += take as u64;
}
Ok(out)
}
}
}
fn byte(&self, at: u64) -> Result<u64, MmdbError> {
self.read(at, 1)?.first().map(|b| u64::from(*b)).ok_or_else(|| bad("Invalid MaxMind DB: read past the end"))
}
pub fn get(&self, ip: &str) -> Result<Option<Value>, MmdbError> {
let address: IpAddr = ip.parse().map_err(|_| MmdbError(format!("Not an IP address: {ip}")))?;
let packed: Vec<u8> = match address {
IpAddr::V4(a) => a.octets().to_vec(),
IpAddr::V6(a) => a.octets().to_vec(),
};
let v6 = packed.len() == 16;
if v6 && self.ip_version() == 4 {
return Err(MmdbError(format!("An IPv6 address cannot be looked up in an IPv4-only database: {ip}")));
}
let mut node = if v6 || self.ip_version() == 4 { 0 } else { self.ipv4_start()? };
let bits = packed.len() * 8;
let mut i = 0;
while i < bits && node < self.node_count {
let bit = (packed[i >> 3] >> (7 - (i & 7))) & 1;
node = self.record(node, u64::from(bit))?;
i += 1;
}
if node <= self.node_count {
return Ok(None);
}
let (value, _) = self.decode(self.data_start + node - self.node_count - 16, self.data_start, 0)?;
Ok(Some(value))
}
fn ipv4_start(&self) -> Result<u64, MmdbError> {
let mut cached = self.ipv4_start.lock().unwrap_or_else(|e| e.into_inner());
if let Some(n) = *cached {
return Ok(n);
}
let mut node = 0;
let mut i = 0;
while i < 96 && node < self.node_count {
node = self.record(node, 0)?;
i += 1;
}
*cached = Some(node);
Ok(node)
}
fn record(&self, node: u64, right: u64) -> Result<u64, MmdbError> {
let b = self.read(node * self.node_bytes, self.node_bytes)?;
if (b.len() as u64) < self.node_bytes {
return Err(bad("Invalid MaxMind DB: read past the end"));
}
let b: Vec<u64> = b.into_iter().map(u64::from).collect();
Ok(match self.record_size {
24 => {
let at = (right * 3) as usize;
(b[at] << 16) | (b[at + 1] << 8) | b[at + 2]
}
28 => {
if right == 0 {
((b[3] & 0xF0) << 20) | (b[0] << 16) | (b[1] << 8) | b[2]
} else {
((b[3] & 0x0F) << 24) | (b[4] << 16) | (b[5] << 8) | b[6]
}
}
_ => {
let at = (right * 4) as usize;
(b[at] << 24) | (b[at + 1] << 16) | (b[at + 2] << 8) | b[at + 3]
}
})
}
fn decode(&self, at: u64, base: u64, depth: usize) -> Result<(Value, u64), MmdbError> {
if depth > 64 {
return Err(bad("Invalid MaxMind DB: nested too deeply"));
}
let mut at = at;
let control = self.byte(at)?;
at += 1;
let mut kind = control >> 5;
if kind == 1 {
let ss = (control >> 3) & 3;
let vvv = control & 7;
let pointer = match ss {
0 => (vvv << 8) | self.byte(at)?,
1 => ((vvv << 16) | (self.byte(at)? << 8) | self.byte(at + 1)?) + 2048,
2 => ((vvv << 24) | (self.byte(at)? << 16) | (self.byte(at + 1)? << 8) | self.byte(at + 2)?) + 526_336,
_ => unsigned(&self.read(at, 4)?),
};
let (value, _) = self.decode(base + pointer, base, depth + 1)?;
return Ok((value, at + ss + 1));
}
if kind == 0 {
kind = 7 + self.byte(at)?;
at += 1;
}
let mut size = control & 0x1F;
if size >= 29 {
let extra = size - 28;
let mut n = 0;
for i in 0..extra {
n = (n << 8) | self.byte(at + i)?;
}
size = match size {
29 => 29,
30 => 285,
_ => 65_821,
} + n;
at += extra;
}
Ok(match kind {
2 => (Value::String(String::from_utf8_lossy(&self.read(at, size)?).into_owned()), at + size),
3 => {
let b = self.read(at, 8)?;
let bytes: [u8; 8] = b.try_into().map_err(|_| bad("Invalid MaxMind DB: read past the end"))?;
(Value::Number(f64::from_be_bytes(bytes)), at + 8)
}
4 => (Value::String(String::from_utf8_lossy(&self.read(at, size)?).into_owned()), at + size),
5 | 6 => (Value::Number(unsigned(&self.read(at, size)?) as f64), at + size),
7 => {
let mut map = Object::new();
for _ in 0..size {
let (key, next) = self.decode(at, base, depth + 1)?;
let (value, next) = self.decode(next, base, depth + 1)?;
map.set(js::js_string(&key), value);
at = next;
}
(Value::Object(map), at)
}
8 => {
let mut n = unsigned(&self.read(at, size)?) as i64;
if size == 4 && n >= 0x8000_0000 {
n -= 0x1_0000_0000;
}
(Value::Number(n as f64), at + size)
}
9 | 10 => {
let bytes = self.read(at, size)?;
let n = bytes.iter().fold(0u128, |n, b| (n << 8) | u128::from(*b));
let v = if n <= 1u128 << 53 { Value::Number(n as f64) } else { Value::String(n.to_string()) };
(v, at + size)
}
11 => {
let mut list = Vec::new();
for _ in 0..size {
let (value, next) = self.decode(at, base, depth + 1)?;
list.push(value);
at = next;
}
(Value::Array(list), at)
}
14 => (Value::Bool(size != 0), at),
15 => {
let b = self.read(at, 4)?;
let bytes: [u8; 4] = b.try_into().map_err(|_| bad("Invalid MaxMind DB: read past the end"))?;
(Value::Number(f64::from(f32::from_be_bytes(bytes))), at + 4)
}
_ => return Err(MmdbError(format!("Invalid MaxMind DB: unknown data type {kind}"))),
})
}
}
fn unsigned(bytes: &[u8]) -> u64 {
bytes.iter().fold(0u64, |n, b| (n << 8) | u64::from(*b))
}
pub fn city_name(name: &str) -> String {
let re = crate::re::uni_re!(
r"[\t\n\x{0B}\f\r \x{A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}]*\([^)]*\)[\t\n\x{0B}\f\r \x{A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}]*$"
);
js::trim(&re.replace(name, "")).to_string()
}
pub fn found_in(record: &Value) -> Option<Found> {
let country = record.get("country").and_then(|c| c.get("iso_code"))?;
if !js::truthy(country) {
return None;
}
let sub = record.get("subdivisions").and_then(|s| s.as_array()).and_then(|a| a.first());
let region = sub
.and_then(|s| s.get("iso_code").filter(|v| !v.is_null()))
.or_else(|| sub.and_then(|s| s.get("names")).and_then(|n| n.get("en")).filter(|v| !v.is_null()));
let city = record.get("city").and_then(|c| c.get("names")).and_then(|n| n.get("en")).filter(|v| !v.is_null());
Some(Found {
country: Some(js::js_string(country)),
region: Some(region.map(js::js_string).unwrap_or_default()),
city: Some(match city {
Some(Value::String(s)) => city_name(s),
Some(other) => js::js_string(other),
None => String::new(),
}),
})
}
impl GeoLookup for Mmdb {
fn lookup<'a>(&'a self, ip: &'a str) -> BoxFuture<'a, Result<Option<Found>, BoxError>> {
Box::pin(async move {
Ok(match self.get(ip) {
Ok(Some(record)) => found_in(&record),
_ => None,
})
})
}
}