Skip to main content

maxminddb/
reader.rs

1//! MaxMind DB reader implementation.
2
3use std::collections::HashSet;
4use std::fs;
5use std::net::IpAddr;
6use std::path::Path;
7
8use ipnetwork::IpNetwork;
9use serde::Deserialize;
10
11#[cfg(feature = "mmap")]
12pub use memmap2::Mmap;
13#[cfg(feature = "mmap")]
14use memmap2::MmapOptions;
15#[cfg(feature = "mmap")]
16use std::fs::File;
17
18use crate::decoder;
19use crate::error::MaxMindDbError;
20use crate::metadata::Metadata;
21use crate::result::{LookupResult, LookupSource, NetworkKind};
22use crate::within::{IpInt, Within, WithinNode, WithinOptions};
23
24/// Size of the data section separator (16 zero bytes).
25const DATA_SECTION_SEPARATOR_SIZE: usize = 16;
26const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com";
27
28/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the
29/// lifetime of the underlying buffer holding the contents of the database file.
30///
31/// The `Reader` supports both file-based and memory-mapped access to MaxMind
32/// DB files, including GeoIP2 and GeoLite2 databases.
33///
34/// # Features
35///
36/// - **`mmap`**: Enable memory-mapped file access for better performance
37/// - **`unsafe-str-decode`**: Skip UTF-8 validation when deserializing trusted
38///   database strings into Rust `str` or `String` values. Cross-runtime format
39///   adapters should prefer [`crate::deserialize_any_with_raw_strings()`].
40pub struct Reader<S: AsRef<[u8]>> {
41    pub(crate) buf: S,
42    /// Database metadata.
43    metadata: Metadata,
44    record_size: u16,
45    /// Cached `Metadata::node_count` for `Reader` search-tree traversal.
46    /// Use this instead of `metadata.node_count` for traversal invariants.
47    node_count: usize,
48    /// Cached bytes per node derived from `Metadata::record_size` for `Reader`.
49    /// Use this instead of `metadata.record_size` in lookup hot paths.
50    node_byte_size: usize,
51    pub(crate) ipv4_start: usize,
52    /// Bit depth at which ipv4_start was found (0-96). Used to calculate
53    /// correct prefix lengths for IPv4 lookups in IPv6 databases.
54    pub(crate) ipv4_start_bit_depth: usize,
55    pub(crate) pointer_base: usize,
56    pub(crate) data_section_len: usize,
57    pub(crate) metadata_start: usize,
58}
59
60impl<S: AsRef<[u8]>> std::fmt::Debug for Reader<S> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("Reader")
63            .field("buf_len", &self.buf.as_ref().len())
64            .field("metadata", &self.metadata)
65            .field("ipv4_start", &self.ipv4_start)
66            .field("ipv4_start_bit_depth", &self.ipv4_start_bit_depth)
67            .field("pointer_base", &self.pointer_base)
68            .field("data_section_len", &self.data_section_len)
69            .field("metadata_start", &self.metadata_start)
70            .finish_non_exhaustive()
71    }
72}
73
74#[cfg(feature = "mmap")]
75impl Reader<Mmap> {
76    /// Open a MaxMind DB database file by memory mapping it.
77    ///
78    /// # Safety
79    ///
80    /// The caller must ensure that the database file is not modified or
81    /// truncated while the `Reader` exists. Modifying or truncating the
82    /// file while it is memory-mapped will result in undefined behavior.
83    ///
84    /// # Example
85    ///
86    /// ```
87    /// # #[cfg(feature = "mmap")]
88    /// # {
89    /// // SAFETY: The database file will not be modified while the reader exists.
90    /// let reader = unsafe {
91    ///     maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb")
92    /// }.unwrap();
93    /// # }
94    /// ```
95    pub unsafe fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
96        let file_read = File::open(database)?;
97        let mmap = MmapOptions::new()
98            .map(&file_read)
99            .map_err(MaxMindDbError::Mmap)?;
100        Reader::from_source(mmap)
101    }
102}
103
104impl Reader<Vec<u8>> {
105    /// Open a MaxMind DB database file by loading it into memory.
106    ///
107    /// # Example
108    ///
109    /// ```
110    /// let reader = maxminddb::Reader::open_readfile(
111    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
112    /// ```
113    pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
114        let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
115        Reader::from_source(buf)
116    }
117}
118
119impl<'de, S: AsRef<[u8]>> Reader<S> {
120    /// Open a MaxMind DB database from anything that implements AsRef<[u8]>
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// use std::fs;
126    /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
127    /// let reader = maxminddb::Reader::from_source(buf).unwrap();
128    /// ```
129    pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
130        let metadata_start = find_metadata_start(buf.as_ref())?;
131        // find_metadata_start returns the offset after the marker; the marker
132        // bytes are not part of the data section and must stay out of limits.
133        let data_section_end = metadata_marker_start(metadata_start)?;
134        let metadata_bytes = &buf.as_ref()[metadata_start..];
135        let mut type_decoder = decoder::Decoder::new(metadata_bytes, 0);
136        let metadata = Metadata::deserialize(&mut type_decoder)
137            .map_err(|error| error.with_invalid_database_offset_base(metadata_start))?;
138        validate_metadata_for_reader(&metadata)?;
139
140        let search_tree_size =
141            search_tree_size_bytes(metadata.node_count as usize, metadata.record_size as usize)?;
142        let record_size = metadata.record_size;
143        let node_count = metadata.node_count as usize;
144        let node_byte_size = record_size as usize / 4;
145        let pointer_base = search_tree_size
146            .checked_add(DATA_SECTION_SEPARATOR_SIZE)
147            .ok_or_else(|| {
148                MaxMindDbError::invalid_database(
149                    "the MaxMind DB file's search tree extends beyond the file",
150                )
151            })?;
152        validate_search_tree_layout(pointer_base, data_section_end)?;
153        let data_section_len = data_section_end - pointer_base;
154
155        let mut reader = Reader {
156            buf,
157            record_size,
158            node_count,
159            node_byte_size,
160            pointer_base,
161            data_section_len,
162            metadata_start,
163            metadata,
164            ipv4_start: 0,
165            ipv4_start_bit_depth: 0,
166        };
167        let (ipv4_start, ipv4_start_bit_depth) = reader.find_ipv4_start();
168        reader.ipv4_start = ipv4_start;
169        reader.ipv4_start_bit_depth = ipv4_start_bit_depth;
170
171        Ok(reader)
172    }
173
174    /// Returns database metadata.
175    ///
176    /// Metadata is validated when the reader is created and exposed by
177    /// reference so it cannot be mutated independently of cached reader state.
178    #[inline]
179    pub fn metadata(&self) -> &Metadata {
180        &self.metadata
181    }
182
183    /// Lookup an IP address in the database.
184    ///
185    /// Returns a [`LookupResult`] that can be used to:
186    /// - Check if data exists with [`has_data()`](LookupResult::has_data)
187    /// - Get the network containing the IP with [`network()`](LookupResult::network)
188    /// - Decode the full record with [`decode()`](LookupResult::decode)
189    /// - Decode a specific path with [`decode_path()`](LookupResult::decode_path)
190    ///
191    /// # Examples
192    ///
193    /// Basic city lookup:
194    /// ```
195    /// # use maxminddb::geoip2;
196    /// # use std::net::IpAddr;
197    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
198    /// let reader = maxminddb::Reader::open_readfile(
199    ///     "test-data/test-data/GeoIP2-City-Test.mmdb")?;
200    ///
201    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
202    /// let result = reader.lookup(ip)?;
203    ///
204    /// if let Some(city) = result.decode::<geoip2::City>()? {
205    ///     // Access nested structs directly - no Option unwrapping needed
206    ///     if let Some(name) = city.city.names.english {
207    ///         println!("City: {}", name);
208    ///     }
209    /// } else {
210    ///     println!("No data found for IP {}", ip);
211    /// }
212    /// # Ok(())
213    /// # }
214    /// ```
215    ///
216    /// Selective field access:
217    /// ```
218    /// # use maxminddb::{path, Reader};
219    /// # use std::net::IpAddr;
220    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
221    /// let reader = Reader::open_readfile(
222    ///     "test-data/test-data/GeoIP2-City-Test.mmdb")?;
223    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
224    ///
225    /// let result = reader.lookup(ip)?;
226    /// let country_code: Option<String> = result.decode_path(&path!["country", "iso_code"])?;
227    ///
228    /// println!("Country: {:?}", country_code);
229    /// # Ok(())
230    /// # }
231    /// ```
232    #[inline]
233    pub fn lookup(&'de self, address: IpAddr) -> Result<LookupResult<'de, S>, MaxMindDbError> {
234        match address {
235            IpAddr::V4(v4) => {
236                let (pointer, prefix_len) = self.find_address_in_tree_v4(v4.into());
237
238                // For IPv4 addresses in IPv6 databases, adjust prefix_len to reflect
239                // the actual bit depth in the tree. The ipv4_start_bit_depth tells us
240                // how deep in the IPv6 tree we were when we found the IPv4 subtree.
241                let prefix_len = if self.metadata.ip_version == 6 {
242                    self.ipv4_start_bit_depth + prefix_len
243                } else {
244                    prefix_len
245                };
246
247                self.lookup_result(pointer, prefix_len as u8, address)
248            }
249            IpAddr::V6(v6) => {
250                if self.metadata.ip_version == 4 {
251                    return Err(MaxMindDbError::invalid_input(
252                        "cannot look up IPv6 address in IPv4-only database",
253                    ));
254                }
255
256                let (pointer, prefix_len) = self.find_address_in_tree_v6(v6.into());
257                self.lookup_result(pointer, prefix_len as u8, address)
258            }
259        }
260    }
261
262    /// Iterate over all networks in the database.
263    ///
264    /// This is a convenience method equivalent to calling [`within()`](Self::within)
265    /// with `0.0.0.0/0` for IPv4-only databases or `::/0` for IPv6 databases.
266    ///
267    /// # Arguments
268    ///
269    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
270    ///   for standard behavior.
271    ///
272    /// # Examples
273    ///
274    /// Iterate over all networks with default options:
275    /// ```
276    /// use maxminddb::{geoip2, Reader};
277    ///
278    /// let reader = Reader::open_readfile(
279    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
280    ///
281    /// let mut count = 0;
282    /// for result in reader.networks(Default::default()).unwrap() {
283    ///     let lookup = result.unwrap();
284    ///     count += 1;
285    ///     if count >= 10 { break; }
286    /// }
287    /// ```
288    pub fn networks(&'de self, options: WithinOptions) -> Result<Within<'de, S>, MaxMindDbError> {
289        let cidr = if self.metadata.ip_version == 6 {
290            IpNetwork::V6("::/0".parse().unwrap())
291        } else {
292            IpNetwork::V4("0.0.0.0/0".parse().unwrap())
293        };
294        self.within(cidr, options)
295    }
296
297    /// Iterate over database networks matching a CIDR range.
298    ///
299    /// Returns an iterator that yields [`LookupResult`] for networks that overlap
300    /// the range in the database's search-tree address space, subject to `options`.
301    /// Results are not clipped to the requested range. If the range is inside a
302    /// larger database network in that address space, the iterator can yield that
303    /// enclosing network.
304    ///
305    /// In IPv6 databases, IPv4 CIDRs are searched under `::/96`. Iteration reports
306    /// networks in that IPv4 subtree as IPv4, even for an IPv6 CIDR. If the search
307    /// reaches a record before the IPv4 subtree, the result remains IPv6. Returned
308    /// networks therefore need not share the CIDR's address family. IPv4-only
309    /// databases reject IPv6 CIDRs.
310    ///
311    /// # Arguments
312    ///
313    /// * `cidr` - The CIDR range to iterate over.
314    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
315    ///   for standard behavior (skip aliases, skip networks without data, include
316    ///   empty values).
317    ///
318    /// # Examples
319    ///
320    /// Iterate over all IPv4 networks:
321    /// ```
322    /// use ipnetwork::IpNetwork;
323    /// use maxminddb::{geoip2, Reader};
324    ///
325    /// let reader = Reader::open_readfile(
326    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
327    ///
328    /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
329    /// let mut count = 0;
330    /// for result in reader.within(ipv4_all, Default::default()).unwrap() {
331    ///     let lookup = result.unwrap();
332    ///     let network = lookup.network().unwrap();
333    ///     let city: geoip2::City = lookup.decode().unwrap().unwrap();
334    ///     let city_name = city.city.names.english;
335    ///     println!("Network: {}, City: {:?}", network, city_name);
336    ///     count += 1;
337    ///     if count >= 10 { break; } // Limit output for example
338    /// }
339    /// ```
340    ///
341    /// Search within a specific subnet:
342    /// ```
343    /// use ipnetwork::IpNetwork;
344    /// use maxminddb::{geoip2, Reader};
345    ///
346    /// let reader = Reader::open_readfile(
347    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
348    ///
349    /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
350    /// for result in reader.within(subnet, Default::default()).unwrap() {
351    ///     match result {
352    ///         Ok(lookup) => {
353    ///             let network = lookup.network().unwrap();
354    ///             println!("Found: {}", network);
355    ///         }
356    ///         Err(e) => eprintln!("Error: {}", e),
357    ///     }
358    /// }
359    /// ```
360    ///
361    /// A requested range inside a larger database network returns that network:
362    /// ```
363    /// use maxminddb::Reader;
364    ///
365    /// let reader = Reader::open_readfile(
366    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
367    /// let mut results = reader
368    ///     .within("89.160.20.128/32".parse().unwrap(), Default::default())
369    ///     .unwrap();
370    /// let lookup = results.next().unwrap().unwrap();
371    /// assert_eq!(lookup.network().unwrap(), "89.160.20.128/25".parse().unwrap());
372    /// assert!(results.next().is_none());
373    /// ```
374    ///
375    /// IPv6 CIDRs can return IPv4 networks, and IPv4 CIDRs can return IPv6 networks:
376    /// ```
377    /// use maxminddb::Reader;
378    ///
379    /// for (database, cidr, expected) in [
380    ///     ("GeoIP2-City-Test.mmdb", "::89.160.20.128/128", "89.160.20.128/25"),
381    ///     ("MaxMind-DB-no-ipv4-search-tree.mmdb", "200.0.2.1/32", "::/64"),
382    /// ] {
383    ///     let reader = Reader::open_readfile(format!("test-data/test-data/{database}")).unwrap();
384    ///     let mut results = reader.within(cidr.parse().unwrap(), Default::default()).unwrap();
385    ///     let lookup = results.next().unwrap().unwrap();
386    ///     assert_eq!(lookup.network().unwrap(), expected.parse().unwrap());
387    ///     assert!(results.next().is_none());
388    /// }
389    /// ```
390    ///
391    /// Include networks without data:
392    /// ```
393    /// use ipnetwork::IpNetwork;
394    /// use maxminddb::{Reader, WithinOptions};
395    ///
396    /// let reader = Reader::open_readfile(
397    ///     "test-data/test-data/MaxMind-DB-test-mixed-24.mmdb").unwrap();
398    ///
399    /// let opts = WithinOptions::default().include_networks_without_data();
400    /// for result in reader.within("1.0.0.0/8".parse().unwrap(), opts).unwrap() {
401    ///     let lookup = result.unwrap();
402    ///     if !lookup.has_data() {
403    ///         println!("Network {} has no data", lookup.network().unwrap());
404    ///     }
405    /// }
406    /// ```
407    pub fn within(
408        &'de self,
409        cidr: IpNetwork,
410        options: WithinOptions,
411    ) -> Result<Within<'de, S>, MaxMindDbError> {
412        if self.metadata.ip_version == 4 && matches!(cidr, IpNetwork::V6(_)) {
413            return Err(MaxMindDbError::invalid_input(
414                "cannot iterate IPv6 network in IPv4-only database",
415            ));
416        }
417        let ip_address = cidr.network();
418        let prefix_len = cidr.prefix() as usize;
419        let ip_int = IpInt::new(ip_address);
420        let bit_count = ip_int.bit_count();
421
422        let mut node = self.start_node(bit_count);
423        let node_count = self.node_count;
424        let has_ipv4_subtree = self.has_ipv4_subtree();
425
426        let mut stack: Vec<WithinNode> = Vec::with_capacity(bit_count - prefix_len);
427
428        // `bit_count == 32` means the caller requested an IPv4 CIDR. In an
429        // IPv6 database with no IPv4 subtree, `start_node(32)` can already be a
430        // terminal IPv6 record reached by walking the all-zero prefix. Do not
431        // read that terminal value as a tree node; yield the containing IPv6
432        // network instead, matching lookup behavior.
433        if bit_count == 32
434            && self.metadata.ip_version == 6
435            && !has_ipv4_subtree
436            && node >= node_count
437        {
438            stack.push(WithinNode {
439                node,
440                ip_int: IpInt::V6(0),
441                prefix_len: self.ipv4_start_bit_depth,
442            });
443
444            return Ok(Within {
445                reader: self,
446                node_count,
447                has_ipv4_subtree,
448                stack,
449                options,
450            });
451        }
452
453        // Traverse down the tree to the level that matches the cidr mark
454        let mut depth = 0_usize;
455        for i in 0..prefix_len {
456            // `read_node` is only valid for internal search-tree nodes.
457            if node >= node_count {
458                // We've hit a data node or dead end before we exhausted our prefix.
459                // This means the requested CIDR is contained in a single record.
460                break;
461            }
462
463            let bit = ip_int.get_bit(i);
464            node = self.read_node(node, bit as usize);
465            depth = i + 1; // We've now traversed i+1 bits (bits 0 through i)
466
467            if node >= node_count {
468                // We've hit a data node or dead end before we exhausted our prefix.
469                // This means the requested CIDR is contained in a single record.
470                break;
471            }
472        }
473
474        // Always push the node - it could be:
475        // - A data node (> node_count): will be yielded as a single record
476        // - The empty node (== node_count): will be skipped unless include_networks_without_data
477        // - An internal node (< node_count): will be traversed to find all contained records
478        stack.push(WithinNode {
479            node,
480            ip_int,
481            prefix_len: depth,
482        });
483
484        let within = Within {
485            reader: self,
486            node_count,
487            has_ipv4_subtree,
488            stack,
489            options,
490        };
491
492        Ok(within)
493    }
494
495    // Pointer 0 means "not found" because normalize_lookup_result collapses both
496    // the placeholder empty node (`node == node_count`) and an unfinished internal
497    // terminal (`node < node_count`, i.e. bits exhausted while still on a tree
498    // node) into 0, so neither path reaches resolve_data_pointer with a non-data
499    // value.
500    #[inline(always)]
501    fn lookup_result(
502        &'de self,
503        pointer: usize,
504        prefix_len: u8,
505        address: IpAddr,
506    ) -> Result<LookupResult<'de, S>, MaxMindDbError> {
507        let network_kind = match address {
508            IpAddr::V4(_) if self.metadata.ip_version == 6 && self.has_ipv4_subtree() => {
509                NetworkKind::V4InV6Subtree
510            }
511            IpAddr::V4(_) if self.metadata.ip_version == 6 => NetworkKind::V6,
512            IpAddr::V4(_) => NetworkKind::V4,
513            IpAddr::V6(_) => NetworkKind::V6,
514        };
515        if pointer == 0 {
516            Ok(LookupResult::new_not_found(
517                self,
518                prefix_len,
519                address,
520                LookupSource::Lookup,
521                network_kind,
522            ))
523        } else {
524            let data_offset = self.resolve_data_pointer(pointer)?;
525            Ok(LookupResult::new_found(
526                self,
527                data_offset,
528                prefix_len,
529                address,
530                LookupSource::Lookup,
531                network_kind,
532            ))
533        }
534    }
535
536    #[inline(always)]
537    fn find_address_in_tree_v4(&self, ip: u32) -> (usize, usize) {
538        let buf = self.buf.as_ref();
539        let node_count = self.node_count;
540
541        match self.record_size {
542            24 => find_address_in_tree_v4::<RecordSize24>(buf, self.ipv4_start, node_count, ip),
543            28 => find_address_in_tree_v4::<RecordSize28>(buf, self.ipv4_start, node_count, ip),
544            32 => find_address_in_tree_v4::<RecordSize32>(buf, self.ipv4_start, node_count, ip),
545            _ => unreachable!("record_size is validated in Reader::from_source"),
546        }
547    }
548
549    #[inline(always)]
550    fn find_address_in_tree_v6(&self, ip: u128) -> (usize, usize) {
551        let buf = self.buf.as_ref();
552        let node_count = self.node_count;
553
554        match self.record_size {
555            24 => find_address_in_tree_v6::<RecordSize24>(buf, node_count, ip),
556            28 => find_address_in_tree_v6::<RecordSize28>(buf, node_count, ip),
557            32 => find_address_in_tree_v6::<RecordSize32>(buf, node_count, ip),
558            _ => unreachable!("record_size is validated in Reader::from_source"),
559        }
560    }
561
562    #[inline]
563    fn start_node(&self, length: usize) -> usize {
564        if length == 128 {
565            0
566        } else {
567            self.ipv4_start
568        }
569    }
570
571    #[inline]
572    pub(crate) fn has_ipv4_subtree(&self) -> bool {
573        self.metadata.ip_version == 6 && self.ipv4_start < self.node_count
574    }
575
576    /// Find the IPv4 start node and the bit depth at which it was found.
577    /// Returns (node, depth) where depth is how far into the tree we traversed.
578    fn find_ipv4_start(&self) -> (usize, usize) {
579        if self.metadata.ip_version != 6 {
580            return (0, 0);
581        }
582
583        // We are looking up an IPv4 address in an IPv6 tree. Skip over the
584        // first 96 nodes.
585        let mut node: usize = 0;
586        for i in 0_u8..96 {
587            if node >= self.node_count {
588                return (node, i as usize);
589            }
590            node = self.read_node(node, 0);
591        }
592        (node, 96)
593    }
594
595    #[inline(always)]
596    pub(crate) fn read_node(&self, node_number: usize, index: usize) -> usize {
597        let buf = self.buf.as_ref();
598
599        match self.record_size {
600            24 => RecordSize24::read_node(buf, node_number, index),
601            28 => RecordSize28::read_node(buf, node_number, index),
602            32 => RecordSize32::read_node(buf, node_number, index),
603            _ => unreachable!("record_size is validated in Reader::from_source"),
604        }
605    }
606
607    /// Resolves a pointer from the search tree to an offset in the data section.
608    #[inline]
609    pub(crate) fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
610        let resolved = pointer
611            .checked_sub(self.node_count)
612            .and_then(|p| p.checked_sub(DATA_SECTION_SEPARATOR_SIZE))
613            .ok_or_else(|| {
614                MaxMindDbError::invalid_database(
615                    "the MaxMind DB file's data pointer resolves to an invalid location",
616                )
617            })?;
618        // Reject offsets at or beyond the marker-excluding data section length.
619        if resolved >= self.data_section_len {
620            return Err(MaxMindDbError::invalid_database(
621                "the MaxMind DB file's data pointer resolves to an invalid location",
622            ));
623        }
624
625        Ok(resolved)
626    }
627
628    /// Performs comprehensive validation of the MaxMind DB file.
629    ///
630    /// This method validates:
631    /// - Metadata section: format versions, required fields, and value constraints
632    /// - Search tree: traverses all networks to verify tree structure integrity
633    /// - Data section separator: validates the 16-byte separator between tree and data
634    /// - Data section: verifies all data records referenced by the search tree
635    ///
636    /// The verifier is stricter than the MaxMind DB specification and may return
637    /// errors on some databases that are still readable by normal operations.
638    /// This method is useful for:
639    /// - Validating database files after download or generation
640    /// - Debugging database corruption issues
641    /// - Ensuring database integrity in critical applications
642    ///
643    /// Note: Verification traverses the entire database and retains visited data
644    /// offsets for the duration of the call. It may be slow and use memory
645    /// proportional to the number of distinct referenced values on large files.
646    /// Verification validates each shared target once. For each metadata or
647    /// data section, it permits up to eight times the section's byte length in
648    /// work units: one per visited value and one per string byte validated.
649    /// Exceeding this allowance returns [`MaxMindDbError::ResourceLimit`],
650    /// bounding repeated scans of overlapping payloads. Later deserialization
651    /// independently applies the per-operation container and payload limits
652    /// documented on [`crate::LookupResult::decode()`]. The method is
653    /// thread-safe and can be called on an active Reader.
654    ///
655    /// # Example
656    ///
657    /// ```
658    /// use maxminddb::Reader;
659    ///
660    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
661    /// reader.verify().expect("Database should be valid");
662    /// ```
663    pub fn verify(&self) -> Result<(), MaxMindDbError> {
664        let metadata_start = find_metadata_start(self.buf.as_ref())?;
665        let data_section_end = metadata_marker_start(metadata_start)?;
666        self.verify_metadata(metadata_start, data_section_end)?;
667        self.verify_database(data_section_end)
668    }
669
670    fn verify_metadata(
671        &self,
672        metadata_start: usize,
673        data_section_end: usize,
674    ) -> Result<(), MaxMindDbError> {
675        // Metadata deserialization intentionally ignores unknown fields. Verify
676        // the original value graph as well so an invalid or cyclic pointer in
677        // an unknown field cannot hide behind the cached typed representation.
678        let metadata_bytes = &self.buf.as_ref()[metadata_start..];
679        let mut decoder = decoder::Decoder::new(metadata_bytes, 0);
680        decoder
681            .skip_value_for_verification(&mut decoder::VerificationState::new(metadata_bytes.len()))
682            .map_err(|error| error.with_invalid_database_offset_base(metadata_start))?;
683
684        let m = &self.metadata;
685
686        validate_metadata_for_reader(m)?;
687        if m.database_type.is_empty() {
688            return Err(MaxMindDbError::invalid_database(
689                "database_type - Expected: non-empty string Actual: \"\"",
690            ));
691        }
692        if m.description.is_empty() {
693            return Err(MaxMindDbError::invalid_database(
694                "description - Expected: non-empty map Actual: {}",
695            ));
696        }
697        validate_search_tree_layout(self.pointer_base, data_section_end)?;
698        Ok(())
699    }
700
701    fn verify_database(&self, data_section_end: usize) -> Result<(), MaxMindDbError> {
702        let offsets = self.verify_search_tree()?;
703        self.verify_data_section_separator()?;
704        self.verify_data_section(offsets, data_section_end)
705    }
706
707    fn verify_search_tree(&self) -> Result<HashSet<usize>, MaxMindDbError> {
708        let mut offsets = HashSet::new();
709        let opts = WithinOptions::default().include_networks_without_data();
710
711        // Maximum number of networks we can expect in a valid database.
712        // A database with N nodes can have at most 2N data entries (each leaf node
713        // can have data). We add some margin for safety.
714        let max_iterations = self.node_count.saturating_mul(3);
715        let mut iteration_count = 0usize;
716
717        for result in self.networks(opts)? {
718            let lookup = result?;
719            if let Some(offset) = lookup.offset() {
720                offsets.insert(offset);
721            }
722
723            iteration_count += 1;
724            if iteration_count > max_iterations {
725                return Err(MaxMindDbError::invalid_database(format!(
726                    "search tree appears to have a cycle or invalid structure (exceeded {max_iterations} iterations)"
727                )));
728            }
729        }
730        Ok(offsets)
731    }
732
733    fn verify_data_section_separator(&self) -> Result<(), MaxMindDbError> {
734        let separator_start = self.node_count * self.node_byte_size;
735        let separator_end = separator_start + DATA_SECTION_SEPARATOR_SIZE;
736
737        if separator_end > self.buf.as_ref().len() {
738            return Err(MaxMindDbError::invalid_database_at(
739                "data section separator extends past end of file",
740                separator_start,
741            ));
742        }
743
744        let separator = &self.buf.as_ref()[separator_start..separator_end];
745
746        for &b in separator {
747            if b != 0 {
748                return Err(MaxMindDbError::invalid_database_at(
749                    format!("unexpected byte in data separator: {separator:?}"),
750                    separator_start,
751                ));
752            }
753        }
754        Ok(())
755    }
756
757    fn verify_data_section(
758        &self,
759        offsets: HashSet<usize>,
760        data_section_end: usize,
761    ) -> Result<(), MaxMindDbError> {
762        let data_section = &self.buf.as_ref()[self.pointer_base..data_section_end];
763        let mut verification_state = decoder::VerificationState::new(data_section.len());
764
765        // Verify each offset from the search tree points to valid, decodable data
766        for &offset in &offsets {
767            if offset >= data_section.len() {
768                return Err(MaxMindDbError::invalid_database_at(
769                    format!(
770                        "search tree pointer is beyond data section (len: {})",
771                        data_section.len()
772                    ),
773                    offset,
774                )
775                .with_invalid_database_offset_base(self.pointer_base));
776            }
777
778            let mut dec = decoder::Decoder::new(data_section, offset);
779
780            // Try to skip/decode the value to verify it's valid
781            dec.skip_value_for_verification(&mut verification_state)
782                .map_err(|error| error.with_invalid_database_offset_base(self.pointer_base))?;
783        }
784
785        Ok(())
786    }
787}
788
789fn validate_record_size(record_size: u16) -> Result<(), MaxMindDbError> {
790    if matches!(record_size, 24 | 28 | 32) {
791        Ok(())
792    } else {
793        Err(MaxMindDbError::invalid_database(format!(
794            "record_size - Expected: 24, 28, or 32 Actual: {}",
795            record_size
796        )))
797    }
798}
799
800pub(crate) fn validate_metadata_for_reader(metadata: &Metadata) -> Result<(), MaxMindDbError> {
801    if metadata.binary_format_major_version != 2 {
802        return Err(MaxMindDbError::invalid_database(format!(
803            "binary_format_major_version - Expected: 2 Actual: {}",
804            metadata.binary_format_major_version
805        )));
806    }
807    // Minor format versions are intended to be forward-compatible.
808    if metadata.ip_version != 4 && metadata.ip_version != 6 {
809        return Err(MaxMindDbError::invalid_database(format!(
810            "ip_version - Expected: 4 or 6 Actual: {}",
811            metadata.ip_version
812        )));
813    }
814    if metadata.node_count == 0 {
815        return Err(MaxMindDbError::invalid_database(
816            "node_count - Expected: positive integer Actual: 0",
817        ));
818    }
819    metadata.build_time()?;
820    validate_record_size(metadata.record_size)
821}
822
823fn search_tree_size_bytes(node_count: usize, record_size: usize) -> Result<usize, MaxMindDbError> {
824    node_count
825        .checked_mul(record_size)
826        .map(|size| size / 4)
827        .ok_or_else(|| {
828            MaxMindDbError::invalid_database(
829                "search tree size calculation overflowed or is impossibly large",
830            )
831        })
832}
833
834fn validate_search_tree_layout(
835    pointer_base: usize,
836    data_section_end: usize,
837) -> Result<(), MaxMindDbError> {
838    if pointer_base > data_section_end {
839        return Err(MaxMindDbError::invalid_database(
840            "the MaxMind DB file's search tree extends beyond the metadata section",
841        ));
842    }
843    Ok(())
844}
845
846trait SearchTreeRecord {
847    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize;
848}
849
850struct RecordSize24;
851
852impl SearchTreeRecord for RecordSize24 {
853    #[inline(always)]
854    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
855        // Both four-byte windows stay inside the six-byte node. The left
856        // child occupies the high three bytes; the right occupies the low three.
857        let offset = node_number * 6 + index * 2;
858        let bytes: [u8; 4] = buf[offset..offset + 4].try_into().unwrap();
859        let word = u32::from_be_bytes(bytes);
860        ((word >> ((1 - index) * 8)) & 0x00FF_FFFF) as usize
861    }
862}
863
864struct RecordSize28;
865
866impl SearchTreeRecord for RecordSize28 {
867    #[inline(always)]
868    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
869        // The left window ends at the shared nibble byte; the right starts
870        // there. Each child and its shared nibble fit in a single word.
871        let offset = node_number * 7 + index * 3;
872        let bytes: [u8; 4] = buf[offset..offset + 4].try_into().unwrap();
873        let word = u32::from_be_bytes(bytes);
874        if index == 0 {
875            // The first three bytes hold bits 23..0, and the shared byte's
876            // high nibble holds bits 27..24. Move that nibble above the bytes.
877            ((word >> 8) | ((word << 20) & 0x0F00_0000)) as usize
878        } else {
879            (word & 0x0FFF_FFFF) as usize
880        }
881    }
882}
883
884struct RecordSize32;
885
886impl SearchTreeRecord for RecordSize32 {
887    #[inline(always)]
888    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
889        let offset = node_number * 8 + index * 4;
890        let bytes: [u8; 4] = buf[offset..offset + 4].try_into().unwrap();
891        u32::from_be_bytes(bytes) as usize
892    }
893}
894
895#[inline(always)]
896fn find_address_in_tree_v4<R: SearchTreeRecord>(
897    buf: &[u8],
898    start_node: usize,
899    node_count: usize,
900    ip: u32,
901) -> (usize, usize) {
902    let mut node = start_node;
903    let mut prefix_len = 32;
904
905    for i in 0..32 {
906        if node >= node_count {
907            prefix_len = i;
908            break;
909        }
910        let bit = ((ip >> (31 - i)) & 1) as usize;
911        node = R::read_node(buf, node, bit);
912    }
913
914    normalize_lookup_result(node, node_count, prefix_len)
915}
916
917#[inline(always)]
918fn find_address_in_tree_v6<R: SearchTreeRecord>(
919    buf: &[u8],
920    node_count: usize,
921    ip: u128,
922) -> (usize, usize) {
923    let mut node = 0;
924    let mut prefix_len = 128;
925
926    for i in 0..128 {
927        if node >= node_count {
928            prefix_len = i;
929            break;
930        }
931        let bit = ((ip >> (127 - i)) & 1) as usize;
932        node = R::read_node(buf, node, bit);
933    }
934
935    normalize_lookup_result(node, node_count, prefix_len)
936}
937
938// Map both "not found" outcomes onto pointer 0:
939//   - `node == node_count`: the placeholder empty terminal in the search tree.
940//   - `node < node_count`: bits exhausted while still on an internal node
941//     (a partially-specified address that did not reach a record).
942// Anything strictly greater than `node_count` is a data-section pointer that
943// the caller must resolve via `resolve_data_pointer`.
944#[inline(always)]
945fn normalize_lookup_result(node: usize, node_count: usize, prefix_len: usize) -> (usize, usize) {
946    if node <= node_count {
947        (0, prefix_len)
948    } else {
949        (node, prefix_len)
950    }
951}
952
953fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
954    memchr::memmem::rfind(buf, METADATA_START_MARKER)
955        .map(|x| x + METADATA_START_MARKER.len())
956        .ok_or_else(|| {
957            MaxMindDbError::invalid_database("could not find MaxMind DB metadata in file")
958        })
959}
960
961fn metadata_marker_start(metadata_start: usize) -> Result<usize, MaxMindDbError> {
962    metadata_start
963        .checked_sub(METADATA_START_MARKER.len())
964        .ok_or_else(|| MaxMindDbError::invalid_database("invalid metadata marker location"))
965}
966
967#[cfg(test)]
968mod tests {
969    use super::{RecordSize24, RecordSize28, RecordSize32, SearchTreeRecord};
970
971    #[test]
972    fn packed_28_bit_node_matches_spec_layout() {
973        // The MMDB specification places each child's most-significant nibble
974        // in the shared byte: [left low 24][left high 4 | right high 4][right low 24].
975        // https://maxmind.github.io/MaxMind-DB/#28-bits-medium-database-one-node-is-7-bytes
976        let node = [0x23, 0x45, 0x67, 0x18, 0x9A, 0xBC, 0xDE];
977        assert_eq!(RecordSize28::read_node(&node, 0, 0), 0x0123_4567);
978        assert_eq!(RecordSize28::read_node(&node, 0, 1), 0x089A_BCDE);
979    }
980
981    #[test]
982    fn packed_nodes_round_trip_both_children() {
983        type ReadNode = fn(&[u8], usize, usize) -> usize;
984        let readers: [(usize, ReadNode); 3] = [
985            (24, RecordSize24::read_node),
986            (28, RecordSize28::read_node),
987            (32, RecordSize32::read_node),
988        ];
989        for (bits, read_node) in readers {
990            let node_size = bits / 4;
991            let mask = u32::MAX >> (32 - bits);
992            let mut state = 0x4D59_5DF4_D0F3_3173_u64;
993            let mut buf = [0u8; 24];
994            for sample in 0..65_536 {
995                state = state
996                    .wrapping_mul(6_364_136_223_846_793_005)
997                    .wrapping_add(1_442_695_040_888_963_407);
998                let (left, right) = match sample {
999                    0 => (0, mask),
1000                    1 => (mask, 0),
1001                    _ => (state as u32 & mask, (state >> 32) as u32 & mask),
1002                };
1003                // Exercise every shared-nibble combination for 28-bit records.
1004                let (left, right) = if bits == 28 && sample >= 2 {
1005                    (
1006                        (left & 0x00FF_FFFF) | ((sample as u32 & 15) << 24),
1007                        (right & 0x00FF_FFFF) | (((sample as u32 >> 4) & 15) << 24),
1008                    )
1009                } else {
1010                    (left, right)
1011                };
1012                let node = sample % 3;
1013                let start = node * node_size;
1014                let end = start + node_size;
1015                let encoded = &mut buf[start..end];
1016                match bits {
1017                    24 => {
1018                        encoded[..3].copy_from_slice(&left.to_be_bytes()[1..]);
1019                        encoded[3..].copy_from_slice(&right.to_be_bytes()[1..]);
1020                    }
1021                    28 => {
1022                        encoded[..3].copy_from_slice(&left.to_be_bytes()[1..]);
1023                        encoded[3] = ((left >> 20) as u8 & 0xF0) | (right >> 24) as u8;
1024                        encoded[4..].copy_from_slice(&right.to_be_bytes()[1..]);
1025                    }
1026                    32 => {
1027                        encoded[..4].copy_from_slice(&left.to_be_bytes());
1028                        encoded[4..].copy_from_slice(&right.to_be_bytes());
1029                    }
1030                    _ => unreachable!(),
1031                }
1032                // Include unaligned nodes and end the buffer at this node:
1033                // neither child may require a byte from the following node.
1034                assert_eq!(read_node(&buf[..end], node, 0), left as usize);
1035                assert_eq!(read_node(&buf[..end], node, 1), right as usize);
1036            }
1037        }
1038    }
1039}