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/// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string
38///   decoding)
39/// - **`unsafe-str-decode`**: Skip UTF-8 validation when deserializing trusted
40///   database strings into Rust `str` or `String` values. Cross-runtime format
41///   adapters should prefer [`crate::deserialize_any_with_raw_strings()`].
42pub struct Reader<S: AsRef<[u8]>> {
43    pub(crate) buf: S,
44    /// Database metadata.
45    metadata: Metadata,
46    record_size: u16,
47    /// Cached `Metadata::node_count` for `Reader` search-tree traversal.
48    /// Use this instead of `metadata.node_count` for traversal invariants.
49    node_count: usize,
50    /// Cached bytes per node derived from `Metadata::record_size` for `Reader`.
51    /// Use this instead of `metadata.record_size` in lookup hot paths.
52    node_byte_size: usize,
53    pub(crate) ipv4_start: usize,
54    /// Bit depth at which ipv4_start was found (0-96). Used to calculate
55    /// correct prefix lengths for IPv4 lookups in IPv6 databases.
56    pub(crate) ipv4_start_bit_depth: usize,
57    pub(crate) pointer_base: usize,
58    pub(crate) data_section_len: usize,
59    pub(crate) metadata_start: usize,
60}
61
62impl<S: AsRef<[u8]>> std::fmt::Debug for Reader<S> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Reader")
65            .field("buf_len", &self.buf.as_ref().len())
66            .field("metadata", &self.metadata)
67            .field("ipv4_start", &self.ipv4_start)
68            .field("ipv4_start_bit_depth", &self.ipv4_start_bit_depth)
69            .field("pointer_base", &self.pointer_base)
70            .field("data_section_len", &self.data_section_len)
71            .field("metadata_start", &self.metadata_start)
72            .finish_non_exhaustive()
73    }
74}
75
76#[cfg(feature = "mmap")]
77impl Reader<Mmap> {
78    /// Open a MaxMind DB database file by memory mapping it.
79    ///
80    /// # Safety
81    ///
82    /// The caller must ensure that the database file is not modified or
83    /// truncated while the `Reader` exists. Modifying or truncating the
84    /// file while it is memory-mapped will result in undefined behavior.
85    ///
86    /// # Example
87    ///
88    /// ```
89    /// # #[cfg(feature = "mmap")]
90    /// # {
91    /// // SAFETY: The database file will not be modified while the reader exists.
92    /// let reader = unsafe {
93    ///     maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb")
94    /// }.unwrap();
95    /// # }
96    /// ```
97    pub unsafe fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
98        let file_read = File::open(database)?;
99        let mmap = MmapOptions::new()
100            .map(&file_read)
101            .map_err(MaxMindDbError::Mmap)?;
102        Reader::from_source(mmap)
103    }
104}
105
106impl Reader<Vec<u8>> {
107    /// Open a MaxMind DB database file by loading it into memory.
108    ///
109    /// # Example
110    ///
111    /// ```
112    /// let reader = maxminddb::Reader::open_readfile(
113    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
114    /// ```
115    pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
116        let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
117        Reader::from_source(buf)
118    }
119}
120
121impl<'de, S: AsRef<[u8]>> Reader<S> {
122    /// Open a MaxMind DB database from anything that implements AsRef<[u8]>
123    ///
124    /// # Example
125    ///
126    /// ```
127    /// use std::fs;
128    /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
129    /// let reader = maxminddb::Reader::from_source(buf).unwrap();
130    /// ```
131    pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
132        let metadata_start = find_metadata_start(buf.as_ref())?;
133        // find_metadata_start returns the offset after the marker; the marker
134        // bytes are not part of the data section and must stay out of limits.
135        let data_section_end = metadata_marker_start(metadata_start)?;
136        let mut type_decoder = decoder::Decoder::new(&buf.as_ref()[metadata_start..], 0);
137        let metadata = Metadata::deserialize(&mut type_decoder)?;
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    pub fn lookup(&'de self, address: IpAddr) -> Result<LookupResult<'de, S>, MaxMindDbError> {
233        match address {
234            IpAddr::V4(v4) => {
235                let (pointer, prefix_len) = self.find_address_in_tree_v4(v4.into());
236
237                // For IPv4 addresses in IPv6 databases, adjust prefix_len to reflect
238                // the actual bit depth in the tree. The ipv4_start_bit_depth tells us
239                // how deep in the IPv6 tree we were when we found the IPv4 subtree.
240                let prefix_len = if self.metadata.ip_version == 6 {
241                    self.ipv4_start_bit_depth + prefix_len
242                } else {
243                    prefix_len
244                };
245
246                self.lookup_result(pointer, prefix_len as u8, address)
247            }
248            IpAddr::V6(v6) => {
249                if self.metadata.ip_version == 4 {
250                    return Err(MaxMindDbError::invalid_input(
251                        "cannot look up IPv6 address in IPv4-only database",
252                    ));
253                }
254
255                let (pointer, prefix_len) = self.find_address_in_tree_v6(v6.into());
256                self.lookup_result(pointer, prefix_len as u8, address)
257            }
258        }
259    }
260
261    /// Iterate over all networks in the database.
262    ///
263    /// This is a convenience method equivalent to calling [`within()`](Self::within)
264    /// with `0.0.0.0/0` for IPv4-only databases or `::/0` for IPv6 databases.
265    ///
266    /// # Arguments
267    ///
268    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
269    ///   for standard behavior.
270    ///
271    /// # Examples
272    ///
273    /// Iterate over all networks with default options:
274    /// ```
275    /// use maxminddb::{geoip2, Reader};
276    ///
277    /// let reader = Reader::open_readfile(
278    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
279    ///
280    /// let mut count = 0;
281    /// for result in reader.networks(Default::default()).unwrap() {
282    ///     let lookup = result.unwrap();
283    ///     count += 1;
284    ///     if count >= 10 { break; }
285    /// }
286    /// ```
287    pub fn networks(&'de self, options: WithinOptions) -> Result<Within<'de, S>, MaxMindDbError> {
288        let cidr = if self.metadata.ip_version == 6 {
289            IpNetwork::V6("::/0".parse().unwrap())
290        } else {
291            IpNetwork::V4("0.0.0.0/0".parse().unwrap())
292        };
293        self.within(cidr, options)
294    }
295
296    /// Iterate over IP networks within a CIDR range.
297    ///
298    /// Returns an iterator that yields [`LookupResult`] for each network in the
299    /// database that falls within the specified CIDR range.
300    ///
301    /// # Arguments
302    ///
303    /// * `cidr` - The CIDR range to iterate over.
304    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
305    ///   for standard behavior (skip aliases, skip networks without data, include
306    ///   empty values).
307    ///
308    /// # Examples
309    ///
310    /// Iterate over all IPv4 networks:
311    /// ```
312    /// use ipnetwork::IpNetwork;
313    /// use maxminddb::{geoip2, Reader};
314    ///
315    /// let reader = Reader::open_readfile(
316    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
317    ///
318    /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
319    /// let mut count = 0;
320    /// for result in reader.within(ipv4_all, Default::default()).unwrap() {
321    ///     let lookup = result.unwrap();
322    ///     let network = lookup.network().unwrap();
323    ///     let city: geoip2::City = lookup.decode().unwrap().unwrap();
324    ///     let city_name = city.city.names.english;
325    ///     println!("Network: {}, City: {:?}", network, city_name);
326    ///     count += 1;
327    ///     if count >= 10 { break; } // Limit output for example
328    /// }
329    /// ```
330    ///
331    /// Search within a specific subnet:
332    /// ```
333    /// use ipnetwork::IpNetwork;
334    /// use maxminddb::{geoip2, Reader};
335    ///
336    /// let reader = Reader::open_readfile(
337    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
338    ///
339    /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
340    /// for result in reader.within(subnet, Default::default()).unwrap() {
341    ///     match result {
342    ///         Ok(lookup) => {
343    ///             let network = lookup.network().unwrap();
344    ///             println!("Found: {}", network);
345    ///         }
346    ///         Err(e) => eprintln!("Error: {}", e),
347    ///     }
348    /// }
349    /// ```
350    ///
351    /// Include networks without data:
352    /// ```
353    /// use ipnetwork::IpNetwork;
354    /// use maxminddb::{Reader, WithinOptions};
355    ///
356    /// let reader = Reader::open_readfile(
357    ///     "test-data/test-data/MaxMind-DB-test-mixed-24.mmdb").unwrap();
358    ///
359    /// let opts = WithinOptions::default().include_networks_without_data();
360    /// for result in reader.within("1.0.0.0/8".parse().unwrap(), opts).unwrap() {
361    ///     let lookup = result.unwrap();
362    ///     if !lookup.has_data() {
363    ///         println!("Network {} has no data", lookup.network().unwrap());
364    ///     }
365    /// }
366    /// ```
367    pub fn within(
368        &'de self,
369        cidr: IpNetwork,
370        options: WithinOptions,
371    ) -> Result<Within<'de, S>, MaxMindDbError> {
372        if self.metadata.ip_version == 4 && matches!(cidr, IpNetwork::V6(_)) {
373            return Err(MaxMindDbError::invalid_input(
374                "cannot iterate IPv6 network in IPv4-only database",
375            ));
376        }
377        let ip_address = cidr.network();
378        let prefix_len = cidr.prefix() as usize;
379        let ip_int = IpInt::new(ip_address);
380        let bit_count = ip_int.bit_count();
381
382        let mut node = self.start_node(bit_count);
383        let node_count = self.node_count;
384        let has_ipv4_subtree = self.has_ipv4_subtree();
385
386        let mut stack: Vec<WithinNode> = Vec::with_capacity(bit_count - prefix_len);
387
388        // `bit_count == 32` means the caller requested an IPv4 CIDR. In an
389        // IPv6 database with no IPv4 subtree, `start_node(32)` can already be a
390        // terminal IPv6 record reached by walking the all-zero prefix. Do not
391        // read that terminal value as a tree node; yield the containing IPv6
392        // network instead, matching lookup behavior.
393        if bit_count == 32
394            && self.metadata.ip_version == 6
395            && !has_ipv4_subtree
396            && node >= node_count
397        {
398            stack.push(WithinNode {
399                node,
400                ip_int: IpInt::V6(0),
401                prefix_len: self.ipv4_start_bit_depth,
402            });
403
404            return Ok(Within {
405                reader: self,
406                node_count,
407                has_ipv4_subtree,
408                stack,
409                options,
410            });
411        }
412
413        // Traverse down the tree to the level that matches the cidr mark
414        let mut depth = 0_usize;
415        for i in 0..prefix_len {
416            // `read_node` is only valid for internal search-tree nodes.
417            if node >= node_count {
418                // We've hit a data node or dead end before we exhausted our prefix.
419                // This means the requested CIDR is contained in a single record.
420                break;
421            }
422
423            let bit = ip_int.get_bit(i);
424            node = self.read_node(node, bit as usize);
425            depth = i + 1; // We've now traversed i+1 bits (bits 0 through i)
426
427            if node >= node_count {
428                // We've hit a data node or dead end before we exhausted our prefix.
429                // This means the requested CIDR is contained in a single record.
430                break;
431            }
432        }
433
434        // Always push the node - it could be:
435        // - A data node (> node_count): will be yielded as a single record
436        // - The empty node (== node_count): will be skipped unless include_networks_without_data
437        // - An internal node (< node_count): will be traversed to find all contained records
438        stack.push(WithinNode {
439            node,
440            ip_int,
441            prefix_len: depth,
442        });
443
444        let within = Within {
445            reader: self,
446            node_count,
447            has_ipv4_subtree,
448            stack,
449            options,
450        };
451
452        Ok(within)
453    }
454
455    // Pointer 0 means "not found" because normalize_lookup_result collapses both
456    // the placeholder empty node (`node == node_count`) and an unfinished internal
457    // terminal (`node < node_count`, i.e. bits exhausted while still on a tree
458    // node) into 0, so neither path reaches resolve_data_pointer with a non-data
459    // value.
460    #[inline(always)]
461    fn lookup_result(
462        &'de self,
463        pointer: usize,
464        prefix_len: u8,
465        address: IpAddr,
466    ) -> Result<LookupResult<'de, S>, MaxMindDbError> {
467        let network_kind = match address {
468            IpAddr::V4(_) if self.metadata.ip_version == 6 && self.has_ipv4_subtree() => {
469                NetworkKind::V4InV6Subtree
470            }
471            IpAddr::V4(_) if self.metadata.ip_version == 6 => NetworkKind::V6,
472            IpAddr::V4(_) => NetworkKind::V4,
473            IpAddr::V6(_) => NetworkKind::V6,
474        };
475        if pointer == 0 {
476            Ok(LookupResult::new_not_found(
477                self,
478                prefix_len,
479                address,
480                LookupSource::Lookup,
481                network_kind,
482            ))
483        } else {
484            let data_offset = self.resolve_data_pointer(pointer)?;
485            Ok(LookupResult::new_found(
486                self,
487                data_offset,
488                prefix_len,
489                address,
490                LookupSource::Lookup,
491                network_kind,
492            ))
493        }
494    }
495
496    #[inline(always)]
497    fn find_address_in_tree_v4(&self, ip: u32) -> (usize, usize) {
498        let buf = self.buf.as_ref();
499        let node_count = self.node_count;
500
501        match self.record_size {
502            24 => find_address_in_tree_v4::<RecordSize24>(buf, self.ipv4_start, node_count, ip),
503            28 => find_address_in_tree_v4::<RecordSize28>(buf, self.ipv4_start, node_count, ip),
504            32 => find_address_in_tree_v4::<RecordSize32>(buf, self.ipv4_start, node_count, ip),
505            _ => unreachable!("record_size is validated in Reader::from_source"),
506        }
507    }
508
509    #[inline(always)]
510    fn find_address_in_tree_v6(&self, ip: u128) -> (usize, usize) {
511        let buf = self.buf.as_ref();
512        let node_count = self.node_count;
513
514        match self.record_size {
515            24 => find_address_in_tree_v6::<RecordSize24>(buf, node_count, ip),
516            28 => find_address_in_tree_v6::<RecordSize28>(buf, node_count, ip),
517            32 => find_address_in_tree_v6::<RecordSize32>(buf, node_count, ip),
518            _ => unreachable!("record_size is validated in Reader::from_source"),
519        }
520    }
521
522    #[inline]
523    fn start_node(&self, length: usize) -> usize {
524        if length == 128 {
525            0
526        } else {
527            self.ipv4_start
528        }
529    }
530
531    #[inline]
532    pub(crate) fn has_ipv4_subtree(&self) -> bool {
533        self.metadata.ip_version == 6 && self.ipv4_start < self.node_count
534    }
535
536    /// Find the IPv4 start node and the bit depth at which it was found.
537    /// Returns (node, depth) where depth is how far into the tree we traversed.
538    fn find_ipv4_start(&self) -> (usize, usize) {
539        if self.metadata.ip_version != 6 {
540            return (0, 0);
541        }
542
543        // We are looking up an IPv4 address in an IPv6 tree. Skip over the
544        // first 96 nodes.
545        let mut node: usize = 0;
546        for i in 0_u8..96 {
547            if node >= self.node_count {
548                return (node, i as usize);
549            }
550            node = self.read_node(node, 0);
551        }
552        (node, 96)
553    }
554
555    #[inline(always)]
556    pub(crate) fn read_node(&self, node_number: usize, index: usize) -> usize {
557        let buf = self.buf.as_ref();
558
559        match self.record_size {
560            24 => RecordSize24::read_node(buf, node_number, index),
561            28 => RecordSize28::read_node(buf, node_number, index),
562            32 => RecordSize32::read_node(buf, node_number, index),
563            _ => unreachable!("record_size is validated in Reader::from_source"),
564        }
565    }
566
567    /// Resolves a pointer from the search tree to an offset in the data section.
568    #[inline]
569    pub(crate) fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
570        let resolved = pointer
571            .checked_sub(self.node_count)
572            .and_then(|p| p.checked_sub(DATA_SECTION_SEPARATOR_SIZE))
573            .ok_or_else(|| {
574                MaxMindDbError::invalid_database(
575                    "the MaxMind DB file's data pointer resolves to an invalid location",
576                )
577            })?;
578        // Reject offsets at or beyond the marker-excluding data section length.
579        if resolved >= self.data_section_len {
580            return Err(MaxMindDbError::invalid_database(
581                "the MaxMind DB file's data pointer resolves to an invalid location",
582            ));
583        }
584
585        Ok(resolved)
586    }
587
588    /// Performs comprehensive validation of the MaxMind DB file.
589    ///
590    /// This method validates:
591    /// - Metadata section: format versions, required fields, and value constraints
592    /// - Search tree: traverses all networks to verify tree structure integrity
593    /// - Data section separator: validates the 16-byte separator between tree and data
594    /// - Data section: verifies all data records referenced by the search tree
595    ///
596    /// The verifier is stricter than the MaxMind DB specification and may return
597    /// errors on some databases that are still readable by normal operations.
598    /// This method is useful for:
599    /// - Validating database files after download or generation
600    /// - Debugging database corruption issues
601    /// - Ensuring database integrity in critical applications
602    ///
603    /// Note: Verification traverses the entire database and retains visited data
604    /// offsets for the duration of the call. It may be slow and use memory
605    /// proportional to the number of distinct referenced values on large files.
606    /// The method is thread-safe and can be called on an active Reader.
607    ///
608    /// # Example
609    ///
610    /// ```
611    /// use maxminddb::Reader;
612    ///
613    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
614    /// reader.verify().expect("Database should be valid");
615    /// ```
616    pub fn verify(&self) -> Result<(), MaxMindDbError> {
617        let metadata_start = find_metadata_start(self.buf.as_ref())?;
618        let data_section_end = metadata_marker_start(metadata_start)?;
619        self.verify_metadata(data_section_end)?;
620        self.verify_database(data_section_end)
621    }
622
623    fn verify_metadata(&self, data_section_end: usize) -> Result<(), MaxMindDbError> {
624        let m = &self.metadata;
625
626        validate_metadata_for_reader(m)?;
627        if m.database_type.is_empty() {
628            return Err(MaxMindDbError::invalid_database(
629                "database_type - Expected: non-empty string Actual: \"\"",
630            ));
631        }
632        if m.description.is_empty() {
633            return Err(MaxMindDbError::invalid_database(
634                "description - Expected: non-empty map Actual: {}",
635            ));
636        }
637        validate_search_tree_layout(self.pointer_base, data_section_end)?;
638        Ok(())
639    }
640
641    fn verify_database(&self, data_section_end: usize) -> Result<(), MaxMindDbError> {
642        let offsets = self.verify_search_tree()?;
643        self.verify_data_section_separator()?;
644        self.verify_data_section(offsets, data_section_end)
645    }
646
647    fn verify_search_tree(&self) -> Result<HashSet<usize>, MaxMindDbError> {
648        let mut offsets = HashSet::new();
649        let opts = WithinOptions::default().include_networks_without_data();
650
651        // Maximum number of networks we can expect in a valid database.
652        // A database with N nodes can have at most 2N data entries (each leaf node
653        // can have data). We add some margin for safety.
654        let max_iterations = self.node_count.saturating_mul(3);
655        let mut iteration_count = 0usize;
656
657        for result in self.networks(opts)? {
658            let lookup = result?;
659            if let Some(offset) = lookup.offset() {
660                offsets.insert(offset);
661            }
662
663            iteration_count += 1;
664            if iteration_count > max_iterations {
665                return Err(MaxMindDbError::invalid_database(format!(
666                    "search tree appears to have a cycle or invalid structure (exceeded {max_iterations} iterations)"
667                )));
668            }
669        }
670        Ok(offsets)
671    }
672
673    fn verify_data_section_separator(&self) -> Result<(), MaxMindDbError> {
674        let separator_start = self.node_count * self.node_byte_size;
675        let separator_end = separator_start + DATA_SECTION_SEPARATOR_SIZE;
676
677        if separator_end > self.buf.as_ref().len() {
678            return Err(MaxMindDbError::invalid_database_at(
679                "data section separator extends past end of file",
680                separator_start,
681            ));
682        }
683
684        let separator = &self.buf.as_ref()[separator_start..separator_end];
685
686        for &b in separator {
687            if b != 0 {
688                return Err(MaxMindDbError::invalid_database_at(
689                    format!("unexpected byte in data separator: {separator:?}"),
690                    separator_start,
691                ));
692            }
693        }
694        Ok(())
695    }
696
697    fn verify_data_section(
698        &self,
699        offsets: HashSet<usize>,
700        data_section_end: usize,
701    ) -> Result<(), MaxMindDbError> {
702        let data_section = &self.buf.as_ref()[self.pointer_base..data_section_end];
703        let mut verification_state = decoder::VerificationState::default();
704
705        // Verify each offset from the search tree points to valid, decodable data
706        for &offset in &offsets {
707            if offset >= data_section.len() {
708                return Err(MaxMindDbError::invalid_database_at(
709                    format!(
710                        "search tree pointer is beyond data section (len: {})",
711                        data_section.len()
712                    ),
713                    offset,
714                ));
715            }
716
717            let mut dec = decoder::Decoder::new(data_section, offset);
718
719            // Try to skip/decode the value to verify it's valid
720            if let Err(e) = dec.skip_value_for_verification(&mut verification_state) {
721                return Err(MaxMindDbError::invalid_database_at(
722                    format!("decoding error: {e}"),
723                    offset,
724                ));
725            }
726        }
727
728        Ok(())
729    }
730}
731
732fn validate_record_size(record_size: u16) -> Result<(), MaxMindDbError> {
733    if matches!(record_size, 24 | 28 | 32) {
734        Ok(())
735    } else {
736        Err(MaxMindDbError::invalid_database(format!(
737            "record_size - Expected: 24, 28, or 32 Actual: {}",
738            record_size
739        )))
740    }
741}
742
743pub(crate) fn validate_metadata_for_reader(metadata: &Metadata) -> Result<(), MaxMindDbError> {
744    if metadata.binary_format_major_version != 2 {
745        return Err(MaxMindDbError::invalid_database(format!(
746            "binary_format_major_version - Expected: 2 Actual: {}",
747            metadata.binary_format_major_version
748        )));
749    }
750    // Minor format versions are intended to be forward-compatible.
751    if metadata.ip_version != 4 && metadata.ip_version != 6 {
752        return Err(MaxMindDbError::invalid_database(format!(
753            "ip_version - Expected: 4 or 6 Actual: {}",
754            metadata.ip_version
755        )));
756    }
757    if metadata.node_count == 0 {
758        return Err(MaxMindDbError::invalid_database(
759            "node_count - Expected: positive integer Actual: 0",
760        ));
761    }
762    metadata.build_time()?;
763    validate_record_size(metadata.record_size)
764}
765
766fn search_tree_size_bytes(node_count: usize, record_size: usize) -> Result<usize, MaxMindDbError> {
767    node_count
768        .checked_mul(record_size)
769        .map(|size| size / 4)
770        .ok_or_else(|| {
771            MaxMindDbError::invalid_database(
772                "search tree size calculation overflowed or is impossibly large",
773            )
774        })
775}
776
777fn validate_search_tree_layout(
778    pointer_base: usize,
779    data_section_end: usize,
780) -> Result<(), MaxMindDbError> {
781    if pointer_base > data_section_end {
782        return Err(MaxMindDbError::invalid_database(
783            "the MaxMind DB file's search tree extends beyond the metadata section",
784        ));
785    }
786    Ok(())
787}
788
789trait SearchTreeRecord {
790    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize;
791}
792
793struct RecordSize24;
794
795impl SearchTreeRecord for RecordSize24 {
796    #[inline(always)]
797    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
798        let offset = node_number * 6 + index * 3;
799        (buf[offset] as usize) << 16 | (buf[offset + 1] as usize) << 8 | buf[offset + 2] as usize
800    }
801}
802
803struct RecordSize28;
804
805impl SearchTreeRecord for RecordSize28 {
806    #[inline(always)]
807    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
808        let base_offset = node_number * 7;
809        let middle = if index == 0 {
810            (buf[base_offset + 3] & 0xF0) >> 4
811        } else {
812            buf[base_offset + 3] & 0x0F
813        };
814        let offset = base_offset + index * 4;
815        (middle as usize) << 24
816            | (buf[offset] as usize) << 16
817            | (buf[offset + 1] as usize) << 8
818            | buf[offset + 2] as usize
819    }
820}
821
822struct RecordSize32;
823
824impl SearchTreeRecord for RecordSize32 {
825    #[inline(always)]
826    fn read_node(buf: &[u8], node_number: usize, index: usize) -> usize {
827        let offset = node_number * 8 + index * 4;
828        (buf[offset] as usize) << 24
829            | (buf[offset + 1] as usize) << 16
830            | (buf[offset + 2] as usize) << 8
831            | buf[offset + 3] as usize
832    }
833}
834
835#[inline(always)]
836fn find_address_in_tree_v4<R: SearchTreeRecord>(
837    buf: &[u8],
838    start_node: usize,
839    node_count: usize,
840    ip: u32,
841) -> (usize, usize) {
842    let mut node = start_node;
843    let mut prefix_len = 32;
844
845    for i in 0..32 {
846        if node >= node_count {
847            prefix_len = i;
848            break;
849        }
850        let bit = ((ip >> (31 - i)) & 1) as usize;
851        node = R::read_node(buf, node, bit);
852    }
853
854    normalize_lookup_result(node, node_count, prefix_len)
855}
856
857#[inline(always)]
858fn find_address_in_tree_v6<R: SearchTreeRecord>(
859    buf: &[u8],
860    node_count: usize,
861    ip: u128,
862) -> (usize, usize) {
863    let mut node = 0;
864    let mut prefix_len = 128;
865
866    for i in 0..128 {
867        if node >= node_count {
868            prefix_len = i;
869            break;
870        }
871        let bit = ((ip >> (127 - i)) & 1) as usize;
872        node = R::read_node(buf, node, bit);
873    }
874
875    normalize_lookup_result(node, node_count, prefix_len)
876}
877
878// Map both "not found" outcomes onto pointer 0:
879//   - `node == node_count`: the placeholder empty terminal in the search tree.
880//   - `node < node_count`: bits exhausted while still on an internal node
881//     (a partially-specified address that did not reach a record).
882// Anything strictly greater than `node_count` is a data-section pointer that
883// the caller must resolve via `resolve_data_pointer`.
884#[inline(always)]
885fn normalize_lookup_result(node: usize, node_count: usize, prefix_len: usize) -> (usize, usize) {
886    if node <= node_count {
887        (0, prefix_len)
888    } else {
889        (node, prefix_len)
890    }
891}
892
893fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
894    memchr::memmem::rfind(buf, METADATA_START_MARKER)
895        .map(|x| x + METADATA_START_MARKER.len())
896        .ok_or_else(|| {
897            MaxMindDbError::invalid_database("could not find MaxMind DB metadata in file")
898        })
899}
900
901fn metadata_marker_start(metadata_start: usize) -> Result<usize, MaxMindDbError> {
902    metadata_start
903        .checked_sub(METADATA_START_MARKER.len())
904        .ok_or_else(|| MaxMindDbError::invalid_database("invalid metadata marker location"))
905}