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(&current.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}