maxminddb/lib.rs
1#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
2//! # MaxMind DB Reader
3//!
4//! This library reads the MaxMind DB format, including the GeoIP2 and GeoLite2 databases.
5//!
6//! ## Features
7//!
8//! This crate provides several optional features for performance and functionality:
9//!
10//! - **`mmap`** (default: disabled): Enable memory-mapped file access for
11//! better performance in long-running applications
12//! - **`simdutf8`** (default: disabled): Use SIMD instructions for faster
13//! UTF-8 validation during string decoding
14//! - **`unsafe-str-decode`** (default: disabled): Skip UTF-8 validation
15//! entirely for maximum performance (~20% faster lookups)
16//!
17//! **Note**: `simdutf8` and `unsafe-str-decode` are mutually exclusive.
18//!
19//! ## Database Compatibility
20//!
21//! This library supports all MaxMind DB format databases:
22//! - **GeoIP2** databases (City, Country, Enterprise, ISP, etc.)
23//! - **GeoLite2** databases (free versions)
24//! - Custom MaxMind DB format databases
25//!
26//! ## Thread Safety
27//!
28//! The `Reader` is `Send` and `Sync`, making it safe to share across threads.
29//! This makes it ideal for web servers and other concurrent applications.
30//!
31//! ## Quick Start
32//!
33//! ```rust
34//! use maxminddb::{Reader, geoip2};
35//! use std::net::IpAddr;
36//!
37//! fn main() -> Result<(), Box<dyn std::error::Error>> {
38//! // Open database file
39//! # let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
40//! # /*
41//! let reader = Reader::open_readfile("/path/to/GeoIP2-City.mmdb")?;
42//! # */
43//!
44//! // Look up an IP address
45//! let ip: IpAddr = "89.160.20.128".parse()?;
46//! if let Some(city) = reader.lookup::<geoip2::City>(ip)? {
47//! if let Some(country) = city.country {
48//! println!("Country: {}", country.iso_code.unwrap_or("Unknown"));
49//! }
50//! }
51//!
52//! Ok(())
53//! }
54//! ```
55
56use std::cmp::Ordering;
57use std::collections::BTreeMap;
58use std::fmt::Display;
59use std::fs;
60use std::io;
61use std::marker::PhantomData;
62use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
63use std::path::Path;
64
65use ipnetwork::{IpNetwork, IpNetworkError};
66use serde::{de, Deserialize, Serialize};
67use thiserror::Error;
68
69#[cfg(feature = "mmap")]
70pub use memmap2::Mmap;
71#[cfg(feature = "mmap")]
72use memmap2::MmapOptions;
73#[cfg(feature = "mmap")]
74use std::fs::File;
75
76#[cfg(all(feature = "simdutf8", feature = "unsafe-str-decode"))]
77compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusive");
78
79#[derive(Error, Debug)]
80pub enum MaxMindDbError {
81 #[error("Invalid database: {0}")]
82 InvalidDatabase(String),
83
84 #[error("I/O error: {0}")]
85 Io(
86 #[from]
87 #[source]
88 io::Error,
89 ),
90
91 #[cfg(feature = "mmap")]
92 #[error("Memory map error: {0}")]
93 Mmap(#[source] io::Error),
94
95 #[error("Decoding error: {0}")]
96 Decoding(String),
97
98 #[error("Invalid network: {0}")]
99 InvalidNetwork(
100 #[from]
101 #[source]
102 IpNetworkError,
103 ),
104}
105
106impl de::Error for MaxMindDbError {
107 fn custom<T: Display>(msg: T) -> Self {
108 MaxMindDbError::Decoding(format!("{msg}"))
109 }
110}
111
112#[derive(Deserialize, Serialize, Clone, Debug)]
113pub struct Metadata {
114 pub binary_format_major_version: u16,
115 pub binary_format_minor_version: u16,
116 pub build_epoch: u64,
117 pub database_type: String,
118 pub description: BTreeMap<String, String>,
119 pub ip_version: u16,
120 pub languages: Vec<String>,
121 pub node_count: u32,
122 pub record_size: u16,
123}
124
125#[derive(Debug)]
126struct WithinNode {
127 node: usize,
128 ip_int: IpInt,
129 prefix_len: usize,
130}
131
132#[derive(Debug)]
133pub struct Within<'de, T: Deserialize<'de>, S: AsRef<[u8]>> {
134 reader: &'de Reader<S>,
135 node_count: usize,
136 stack: Vec<WithinNode>,
137 phantom: PhantomData<&'de T>,
138}
139
140#[derive(Debug)]
141pub struct WithinItem<T> {
142 pub ip_net: IpNetwork,
143 pub info: T,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147enum IpInt {
148 V4(u32),
149 V6(u128),
150}
151
152impl IpInt {
153 fn new(ip_addr: IpAddr) -> Self {
154 match ip_addr {
155 IpAddr::V4(v4) => IpInt::V4(v4.into()),
156 IpAddr::V6(v6) => IpInt::V6(v6.into()),
157 }
158 }
159
160 #[inline(always)]
161 fn get_bit(&self, index: usize) -> bool {
162 match self {
163 IpInt::V4(ip) => (ip >> (31 - index)) & 1 == 1,
164 IpInt::V6(ip) => (ip >> (127 - index)) & 1 == 1,
165 }
166 }
167
168 fn bit_count(&self) -> usize {
169 match self {
170 IpInt::V4(_) => 32,
171 IpInt::V6(_) => 128,
172 }
173 }
174
175 fn is_ipv4_in_ipv6(&self) -> bool {
176 match self {
177 IpInt::V4(_) => false,
178 IpInt::V6(ip) => *ip <= 0xFFFFFFFF,
179 }
180 }
181}
182
183impl<'de, T: Deserialize<'de>, S: AsRef<[u8]>> Iterator for Within<'de, T, S> {
184 type Item = Result<WithinItem<T>, MaxMindDbError>;
185
186 fn next(&mut self) -> Option<Self::Item> {
187 while let Some(current) = self.stack.pop() {
188 let bit_count = current.ip_int.bit_count();
189
190 // Skip networks that are aliases for the IPv4 network
191 if self.reader.ipv4_start != 0
192 && current.node == self.reader.ipv4_start
193 && bit_count == 128
194 && !current.ip_int.is_ipv4_in_ipv6()
195 {
196 continue;
197 }
198
199 match current.node.cmp(&self.node_count) {
200 Ordering::Greater => {
201 // This is a data node, emit it and we're done (until the following next call)
202 let ip_net =
203 match bytes_and_prefix_to_net(¤t.ip_int, current.prefix_len as u8) {
204 Ok(ip_net) => ip_net,
205 Err(e) => return Some(Err(e)),
206 };
207
208 // Call the new helper method to decode data
209 return match self.reader.decode_data_at_pointer(current.node) {
210 Ok(info) => Some(Ok(WithinItem { ip_net, info })),
211 Err(e) => Some(Err(e)),
212 };
213 }
214 Ordering::Equal => {
215 // Dead end, nothing to do
216 }
217 Ordering::Less => {
218 // In order traversal of our children
219 // right/1-bit
220 let mut right_ip_int = current.ip_int;
221
222 if current.prefix_len < bit_count {
223 let bit = current.prefix_len;
224 match &mut right_ip_int {
225 IpInt::V4(ip) => *ip |= 1 << (31 - bit),
226 IpInt::V6(ip) => *ip |= 1 << (127 - bit),
227 };
228 }
229
230 let node = match self.reader.read_node(current.node, 1) {
231 Ok(node) => node,
232 Err(e) => return Some(Err(e)),
233 };
234 self.stack.push(WithinNode {
235 node,
236 ip_int: right_ip_int,
237 prefix_len: current.prefix_len + 1,
238 });
239 // left/0-bit
240 let node = match self.reader.read_node(current.node, 0) {
241 Ok(node) => node,
242 Err(e) => return Some(Err(e)),
243 };
244 self.stack.push(WithinNode {
245 node,
246 ip_int: current.ip_int,
247 prefix_len: current.prefix_len + 1,
248 });
249 }
250 }
251 }
252 None
253 }
254}
255
256/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the
257/// lifetime of the underlying buffer holding the contents of the database file.
258///
259/// The `Reader` supports both file-based and memory-mapped access to MaxMind
260/// DB files, including GeoIP2 and GeoLite2 databases.
261///
262/// # Features
263///
264/// - **`mmap`**: Enable memory-mapped file access for better performance
265/// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string
266/// decoding)
267/// - **`unsafe-str-decode`**: Skip UTF-8 validation entirely (unsafe, but
268/// ~20% faster)
269#[derive(Debug)]
270pub struct Reader<S: AsRef<[u8]>> {
271 buf: S,
272 pub metadata: Metadata,
273 ipv4_start: usize,
274 pointer_base: usize,
275}
276
277#[cfg(feature = "mmap")]
278impl Reader<Mmap> {
279 /// Open a MaxMind DB database file by memory mapping it.
280 ///
281 /// # Example
282 ///
283 /// ```
284 /// # #[cfg(feature = "mmap")]
285 /// # {
286 /// let reader = maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
287 /// # }
288 /// ```
289 pub fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
290 let file_read = File::open(database)?;
291 let mmap = unsafe { MmapOptions::new().map(&file_read) }.map_err(MaxMindDbError::Mmap)?;
292 Reader::from_source(mmap)
293 }
294}
295
296impl Reader<Vec<u8>> {
297 /// Open a MaxMind DB database file by loading it into memory.
298 ///
299 /// # Example
300 ///
301 /// ```
302 /// let reader = maxminddb::Reader::open_readfile(
303 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
304 /// ```
305 pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
306 let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
307 Reader::from_source(buf)
308 }
309}
310
311impl<'de, S: AsRef<[u8]>> Reader<S> {
312 /// Open a MaxMind DB database from anything that implements AsRef<[u8]>
313 ///
314 /// # Example
315 ///
316 /// ```
317 /// use std::fs;
318 /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
319 /// let reader = maxminddb::Reader::from_source(buf).unwrap();
320 /// ```
321 pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
322 let data_section_separator_size = 16;
323
324 let metadata_start = find_metadata_start(buf.as_ref())?;
325 let mut type_decoder = decoder::Decoder::new(&buf.as_ref()[metadata_start..], 0);
326 let metadata = Metadata::deserialize(&mut type_decoder)?;
327
328 let search_tree_size = (metadata.node_count as usize) * (metadata.record_size as usize) / 4;
329
330 let mut reader = Reader {
331 buf,
332 pointer_base: search_tree_size + data_section_separator_size,
333 metadata,
334 ipv4_start: 0,
335 };
336 reader.ipv4_start = reader.find_ipv4_start()?;
337
338 Ok(reader)
339 }
340
341 /// Lookup the socket address in the opened MaxMind DB.
342 /// Returns `Ok(None)` if the address is not found in the database.
343 ///
344 /// # Examples
345 ///
346 /// Basic city lookup:
347 /// ```
348 /// # use maxminddb::geoip2;
349 /// # use std::net::IpAddr;
350 /// # use std::str::FromStr;
351 /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
352 /// let reader = maxminddb::Reader::open_readfile(
353 /// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
354 ///
355 /// let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
356 /// match reader.lookup::<geoip2::City>(ip)? {
357 /// Some(city) => {
358 /// if let Some(city_names) = city.city.and_then(|c| c.names) {
359 /// if let Some(name) = city_names.get("en") {
360 /// println!("City: {}", name);
361 /// }
362 /// }
363 /// if let Some(country) = city.country.and_then(|c| c.iso_code) {
364 /// println!("Country: {}", country);
365 /// }
366 /// }
367 /// None => println!("No data found for IP {}", ip),
368 /// }
369 /// # Ok(())
370 /// # }
371 /// ```
372 ///
373 /// Lookup with different record types:
374 /// ```
375 /// # use maxminddb::geoip2;
376 /// # use std::net::IpAddr;
377 /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
378 /// let reader = maxminddb::Reader::open_readfile(
379 /// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
380 /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
381 ///
382 /// // Different record types for the same IP
383 /// let city: Option<geoip2::City> = reader.lookup(ip)?;
384 /// let country: Option<geoip2::Country> = reader.lookup(ip)?;
385 ///
386 /// println!("City data available: {}", city.is_some());
387 /// println!("Country data available: {}", country.is_some());
388 /// # Ok(())
389 /// # }
390 /// ```
391 pub fn lookup<T>(&'de self, address: IpAddr) -> Result<Option<T>, MaxMindDbError>
392 where
393 T: Deserialize<'de>,
394 {
395 self.lookup_prefix(address)
396 .map(|(option_value, _prefix_len)| option_value)
397 }
398
399 /// Lookup the socket address in the opened MaxMind DB, returning the found value (if any)
400 /// and the prefix length of the network associated with the lookup.
401 ///
402 /// Returns `Ok((None, prefix_len))` if the address is found in the tree but has no data record.
403 /// Returns `Err(...)` for database errors (IO, corruption, decoding).
404 ///
405 /// Example:
406 ///
407 /// ```
408 /// # use maxminddb::geoip2;
409 /// # use std::net::IpAddr;
410 /// # use std::str::FromStr;
411 /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
412 /// let reader = maxminddb::Reader::open_readfile(
413 /// "test-data/test-data/GeoIP2-City-Test.mmdb")?;
414 ///
415 /// let ip: IpAddr = "89.160.20.128".parse().unwrap(); // Known IP
416 /// let ip_unknown: IpAddr = "10.0.0.1".parse().unwrap(); // Unknown IP
417 ///
418 /// let (city_option, prefix_len) = reader.lookup_prefix::<geoip2::City>(ip)?;
419 /// if let Some(city) = city_option {
420 /// println!("Found {:?} at prefix length {}", city.city.unwrap().names.unwrap().get("en").unwrap(), prefix_len);
421 /// } else {
422 /// // This case is less likely with lookup_prefix if the IP resolves in the tree
423 /// println!("IP found in tree but no data (prefix_len: {})", prefix_len);
424 /// }
425 ///
426 /// let (city_option_unknown, prefix_len_unknown) = reader.lookup_prefix::<geoip2::City>(ip_unknown)?;
427 /// assert!(city_option_unknown.is_none());
428 /// println!("Unknown IP resolved to prefix_len: {}", prefix_len_unknown);
429 /// # Ok(())
430 /// # }
431 /// ```
432 pub fn lookup_prefix<T>(
433 &'de self,
434 address: IpAddr,
435 ) -> Result<(Option<T>, usize), MaxMindDbError>
436 where
437 T: Deserialize<'de>,
438 {
439 let ip_int = IpInt::new(address);
440 // find_address_in_tree returns Result<(usize, usize), MaxMindDbError> -> (pointer, prefix_len)
441 let (pointer, prefix_len) = self.find_address_in_tree(&ip_int)?;
442
443 if pointer == 0 {
444 // If pointer is 0, it signifies no data record was associated during tree traversal.
445 // Return None for the data, but include the calculated prefix_len.
446 return Ok((None, prefix_len));
447 }
448
449 // If pointer > 0, attempt to resolve and decode data using the helper method
450 match self.decode_data_at_pointer(pointer) {
451 Ok(value) => Ok((Some(value), prefix_len)),
452 Err(e) => Err(e),
453 }
454 }
455
456 /// Iterate over blocks of IP networks in the opened MaxMind DB
457 ///
458 /// This method returns an iterator that yields all IP network blocks that
459 /// fall within the specified CIDR range and have associated data in the
460 /// database.
461 ///
462 /// # Examples
463 ///
464 /// Iterate over all IPv4 networks:
465 /// ```
466 /// use ipnetwork::IpNetwork;
467 /// use maxminddb::{geoip2, Within};
468 ///
469 /// let reader = maxminddb::Reader::open_readfile(
470 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
471 ///
472 /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
473 /// let mut count = 0;
474 /// for result in reader.within::<geoip2::City>(ipv4_all).unwrap() {
475 /// let item = result.unwrap();
476 /// let city_name = item.info.city.as_ref().and_then(|c| c.names.as_ref()).and_then(|n| n.get("en"));
477 /// println!("Network: {}, City: {:?}", item.ip_net, city_name);
478 /// count += 1;
479 /// if count >= 10 { break; } // Limit output for example
480 /// }
481 /// ```
482 ///
483 /// Search within a specific subnet:
484 /// ```
485 /// use ipnetwork::IpNetwork;
486 /// use maxminddb::geoip2;
487 ///
488 /// let reader = maxminddb::Reader::open_readfile(
489 /// "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
490 ///
491 /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
492 /// match reader.within::<geoip2::City>(subnet) {
493 /// Ok(iter) => {
494 /// for result in iter {
495 /// match result {
496 /// Ok(item) => println!("Found: {}", item.ip_net),
497 /// Err(e) => eprintln!("Error processing item: {}", e),
498 /// }
499 /// }
500 /// }
501 /// Err(e) => eprintln!("Failed to create iterator: {}", e),
502 /// }
503 /// ```
504 pub fn within<T>(&'de self, cidr: IpNetwork) -> Result<Within<'de, T, S>, MaxMindDbError>
505 where
506 T: Deserialize<'de>,
507 {
508 let ip_address = cidr.network();
509 let prefix_len = cidr.prefix() as usize;
510 let ip_int = IpInt::new(ip_address);
511 let bit_count = ip_int.bit_count();
512
513 let mut node = self.start_node(bit_count);
514 let node_count = self.metadata.node_count as usize;
515
516 let mut stack: Vec<WithinNode> = Vec::with_capacity(bit_count - prefix_len);
517
518 // Traverse down the tree to the level that matches the cidr mark
519 let mut i = 0_usize;
520 while i < prefix_len {
521 let bit = ip_int.get_bit(i);
522 node = self.read_node(node, bit as usize)?;
523 if node >= node_count {
524 // We've hit a dead end before we exhausted our prefix
525 break;
526 }
527
528 i += 1;
529 }
530
531 if node < node_count {
532 // Ok, now anything that's below node in the tree is "within", start with the node we
533 // traversed to as our to be processed stack.
534 stack.push(WithinNode {
535 node,
536 ip_int,
537 prefix_len,
538 });
539 }
540 // else the stack will be empty and we'll be returning an iterator that visits nothing,
541 // which makes sense.
542
543 let within: Within<T, S> = Within {
544 reader: self,
545 node_count,
546 stack,
547 phantom: PhantomData,
548 };
549
550 Ok(within)
551 }
552
553 fn find_address_in_tree(&self, ip_int: &IpInt) -> Result<(usize, usize), MaxMindDbError> {
554 let bit_count = ip_int.bit_count();
555 let mut node = self.start_node(bit_count);
556
557 let node_count = self.metadata.node_count as usize;
558 let mut prefix_len = bit_count;
559
560 for i in 0..bit_count {
561 if node >= node_count {
562 prefix_len = i;
563 break;
564 }
565 let bit = ip_int.get_bit(i);
566 node = self.read_node(node, bit as usize)?;
567 }
568 match node_count {
569 // If node == node_count, it means we hit the placeholder "empty" node
570 // return 0 as the pointer value to signify "not found".
571 n if n == node => Ok((0, prefix_len)),
572 n if node > n => Ok((node, prefix_len)),
573 _ => Err(MaxMindDbError::InvalidDatabase(
574 "invalid node in search tree".to_owned(),
575 )),
576 }
577 }
578
579 fn start_node(&self, length: usize) -> usize {
580 if length == 128 {
581 0
582 } else {
583 self.ipv4_start
584 }
585 }
586
587 fn find_ipv4_start(&self) -> Result<usize, MaxMindDbError> {
588 if self.metadata.ip_version != 6 {
589 return Ok(0);
590 }
591
592 // We are looking up an IPv4 address in an IPv6 tree. Skip over the
593 // first 96 nodes.
594 let mut node: usize = 0_usize;
595 for _ in 0_u8..96 {
596 if node >= self.metadata.node_count as usize {
597 break;
598 }
599 node = self.read_node(node, 0)?;
600 }
601 Ok(node)
602 }
603
604 #[inline(always)]
605 fn read_node(&self, node_number: usize, index: usize) -> Result<usize, MaxMindDbError> {
606 let buf = self.buf.as_ref();
607 let base_offset = node_number * (self.metadata.record_size as usize) / 4;
608
609 let val = match self.metadata.record_size {
610 24 => {
611 let offset = base_offset + index * 3;
612 to_usize(0, &buf[offset..offset + 3])
613 }
614 28 => {
615 let mut middle = buf[base_offset + 3];
616 if index != 0 {
617 middle &= 0x0F
618 } else {
619 middle = (0xF0 & middle) >> 4
620 }
621 let offset = base_offset + index * 4;
622 to_usize(middle, &buf[offset..offset + 3])
623 }
624 32 => {
625 let offset = base_offset + index * 4;
626 to_usize(0, &buf[offset..offset + 4])
627 }
628 s => {
629 return Err(MaxMindDbError::InvalidDatabase(format!(
630 "unknown record size: \
631 {s:?}"
632 )))
633 }
634 };
635 Ok(val)
636 }
637
638 /// Resolves a pointer from the search tree to an offset in the data section.
639 fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
640 let resolved = pointer - (self.metadata.node_count as usize) - 16;
641
642 // Check bounds using pointer_base which marks the start of the data section
643 if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
644 return Err(MaxMindDbError::InvalidDatabase(
645 "the MaxMind DB file's data pointer resolves to an invalid location".to_owned(),
646 ));
647 }
648
649 Ok(resolved)
650 }
651
652 /// Decodes data at the given pointer offset.
653 /// Assumes the pointer is valid and points to the data section.
654 fn decode_data_at_pointer<T>(&'de self, pointer: usize) -> Result<T, MaxMindDbError>
655 where
656 T: Deserialize<'de>,
657 {
658 let resolved_offset = self.resolve_data_pointer(pointer)?;
659 let mut decoder =
660 decoder::Decoder::new(&self.buf.as_ref()[self.pointer_base..], resolved_offset);
661 T::deserialize(&mut decoder)
662 }
663}
664
665// I haven't moved all patterns of this form to a generic function as
666// the FromPrimitive trait is unstable
667#[inline(always)]
668fn to_usize(base: u8, bytes: &[u8]) -> usize {
669 bytes
670 .iter()
671 .fold(base as usize, |acc, &b| (acc << 8) | b as usize)
672}
673
674#[inline]
675fn bytes_and_prefix_to_net(bytes: &IpInt, prefix: u8) -> Result<IpNetwork, MaxMindDbError> {
676 let (ip, prefix) = match bytes {
677 IpInt::V4(ip) => (IpAddr::V4(Ipv4Addr::from(*ip)), prefix),
678 IpInt::V6(ip) if bytes.is_ipv4_in_ipv6() => {
679 (IpAddr::V4(Ipv4Addr::from(*ip as u32)), prefix - 96)
680 }
681 IpInt::V6(ip) => (IpAddr::V6(Ipv6Addr::from(*ip)), prefix),
682 };
683 IpNetwork::new(ip, prefix).map_err(MaxMindDbError::InvalidNetwork)
684}
685
686fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
687 const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com";
688
689 memchr::memmem::rfind(buf, METADATA_START_MARKER)
690 .map(|x| x + METADATA_START_MARKER.len())
691 .ok_or_else(|| {
692 MaxMindDbError::InvalidDatabase(
693 "Could not find MaxMind DB metadata in file.".to_owned(),
694 )
695 })
696}
697
698mod decoder;
699pub mod geoip2;
700
701#[cfg(test)]
702mod reader_test;
703
704#[cfg(test)]
705mod tests {
706 use super::MaxMindDbError;
707 use ipnetwork::IpNetworkError;
708 use std::io::{Error, ErrorKind};
709
710 #[test]
711 fn test_error_display() {
712 assert_eq!(
713 format!(
714 "{}",
715 MaxMindDbError::InvalidDatabase("something went wrong".to_owned())
716 ),
717 "Invalid database: something went wrong".to_owned(),
718 );
719 let io_err = Error::new(ErrorKind::NotFound, "file not found");
720 assert_eq!(
721 format!("{}", MaxMindDbError::from(io_err)),
722 "I/O error: file not found".to_owned(),
723 );
724
725 #[cfg(feature = "mmap")]
726 {
727 let mmap_io_err = Error::new(ErrorKind::PermissionDenied, "mmap failed");
728 assert_eq!(
729 format!("{}", MaxMindDbError::Mmap(mmap_io_err)),
730 "Memory map error: mmap failed".to_owned(),
731 );
732 }
733
734 assert_eq!(
735 format!("{}", MaxMindDbError::Decoding("unexpected type".to_owned())),
736 "Decoding error: unexpected type".to_owned(),
737 );
738
739 let net_err = IpNetworkError::InvalidPrefix;
740 assert_eq!(
741 format!("{}", MaxMindDbError::from(net_err)),
742 "Invalid network: invalid prefix".to_owned(),
743 );
744 }
745
746 #[test]
747 fn test_lookup_returns_none_for_unknown_address() {
748 use super::Reader;
749 use crate::geoip2;
750 use std::net::IpAddr;
751 use std::str::FromStr;
752
753 let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
754 let ip: IpAddr = FromStr::from_str("10.0.0.1").unwrap();
755
756 let result_lookup = reader.lookup::<geoip2::City>(ip);
757 assert!(
758 matches!(result_lookup, Ok(None)),
759 "lookup should return Ok(None) for unknown IP"
760 );
761
762 let result_lookup_prefix = reader.lookup_prefix::<geoip2::City>(ip);
763 assert!(
764 matches!(result_lookup_prefix, Ok((None, 8))),
765 "lookup_prefix should return Ok((None, 8)) for unknown IP, got {:?}",
766 result_lookup_prefix
767 );
768 }
769
770 #[test]
771 fn test_lookup_returns_some_for_known_address() {
772 use super::Reader;
773 use crate::geoip2;
774 use std::net::IpAddr;
775 use std::str::FromStr;
776
777 let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
778 let ip: IpAddr = FromStr::from_str("89.160.20.128").unwrap();
779
780 let result_lookup = reader.lookup::<geoip2::City>(ip);
781 assert!(
782 matches!(result_lookup, Ok(Some(_))),
783 "lookup should return Ok(Some(_)) for known IP"
784 );
785 assert!(
786 result_lookup.unwrap().unwrap().city.is_some(),
787 "Expected city data"
788 );
789
790 let result_lookup_prefix = reader.lookup_prefix::<geoip2::City>(ip);
791 assert!(
792 matches!(result_lookup_prefix, Ok((Some(_), _))),
793 "lookup_prefix should return Ok(Some(_)) for known IP"
794 );
795 let (city_data, prefix_len) = result_lookup_prefix.unwrap();
796 assert!(
797 city_data.unwrap().city.is_some(),
798 "Expected city data from prefix lookup"
799 );
800 assert_eq!(prefix_len, 25, "Expected valid prefix length");
801 }
802}