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;
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;
26
27/// A reader for the MaxMind DB format. The lifetime `'data` is tied to the
28/// lifetime of the underlying buffer holding the contents of the database file.
29///
30/// The `Reader` supports both file-based and memory-mapped access to MaxMind
31/// DB files, including GeoIP2 and GeoLite2 databases.
32///
33/// # Features
34///
35/// - **`mmap`**: Enable memory-mapped file access for better performance
36/// - **`simdutf8`**: Use SIMD-accelerated UTF-8 validation (faster string
37///   decoding)
38/// - **`unsafe-str-decode`**: Skip UTF-8 validation entirely (unsafe, but
39///   ~20% faster)
40pub struct Reader<S: AsRef<[u8]>> {
41    pub(crate) buf: S,
42    /// Database metadata.
43    pub metadata: Metadata,
44    pub(crate) ipv4_start: usize,
45    /// Bit depth at which ipv4_start was found (0-96). Used to calculate
46    /// correct prefix lengths for IPv4 lookups in IPv6 databases.
47    pub(crate) ipv4_start_bit_depth: usize,
48    pub(crate) pointer_base: usize,
49}
50
51impl<S: AsRef<[u8]>> std::fmt::Debug for Reader<S> {
52    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53        f.debug_struct("Reader")
54            .field("buf_len", &self.buf.as_ref().len())
55            .field("metadata", &self.metadata)
56            .field("ipv4_start", &self.ipv4_start)
57            .field("ipv4_start_bit_depth", &self.ipv4_start_bit_depth)
58            .field("pointer_base", &self.pointer_base)
59            .finish_non_exhaustive()
60    }
61}
62
63#[cfg(feature = "mmap")]
64impl Reader<Mmap> {
65    /// Open a MaxMind DB database file by memory mapping it.
66    ///
67    /// # Safety
68    ///
69    /// The caller must ensure that the database file is not modified or
70    /// truncated while the `Reader` exists. Modifying or truncating the
71    /// file while it is memory-mapped will result in undefined behavior.
72    ///
73    /// # Example
74    ///
75    /// ```
76    /// # #[cfg(feature = "mmap")]
77    /// # {
78    /// // SAFETY: The database file will not be modified while the reader exists.
79    /// let reader = unsafe {
80    ///     maxminddb::Reader::open_mmap("test-data/test-data/GeoIP2-City-Test.mmdb")
81    /// }.unwrap();
82    /// # }
83    /// ```
84    pub unsafe fn open_mmap<P: AsRef<Path>>(database: P) -> Result<Reader<Mmap>, MaxMindDbError> {
85        let file_read = File::open(database)?;
86        let mmap = MmapOptions::new()
87            .map(&file_read)
88            .map_err(MaxMindDbError::Mmap)?;
89        Reader::from_source(mmap)
90    }
91}
92
93impl Reader<Vec<u8>> {
94    /// Open a MaxMind DB database file by loading it into memory.
95    ///
96    /// # Example
97    ///
98    /// ```
99    /// let reader = maxminddb::Reader::open_readfile(
100    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
101    /// ```
102    pub fn open_readfile<P: AsRef<Path>>(database: P) -> Result<Reader<Vec<u8>>, MaxMindDbError> {
103        let buf: Vec<u8> = fs::read(&database)?; // IO error converted via #[from]
104        Reader::from_source(buf)
105    }
106}
107
108impl<'de, S: AsRef<[u8]>> Reader<S> {
109    /// Open a MaxMind DB database from anything that implements AsRef<[u8]>
110    ///
111    /// # Example
112    ///
113    /// ```
114    /// use std::fs;
115    /// let buf = fs::read("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
116    /// let reader = maxminddb::Reader::from_source(buf).unwrap();
117    /// ```
118    pub fn from_source(buf: S) -> Result<Reader<S>, MaxMindDbError> {
119        let data_section_separator_size = 16;
120
121        let metadata_start = find_metadata_start(buf.as_ref())?;
122        let mut type_decoder = decoder::Decoder::new(&buf.as_ref()[metadata_start..], 0);
123        let metadata = Metadata::deserialize(&mut type_decoder)?;
124
125        let search_tree_size = (metadata.node_count as usize) * (metadata.record_size as usize) / 4;
126
127        let mut reader = Reader {
128            buf,
129            pointer_base: search_tree_size + data_section_separator_size,
130            metadata,
131            ipv4_start: 0,
132            ipv4_start_bit_depth: 0,
133        };
134        let (ipv4_start, ipv4_start_bit_depth) = reader.find_ipv4_start()?;
135        reader.ipv4_start = ipv4_start;
136        reader.ipv4_start_bit_depth = ipv4_start_bit_depth;
137
138        Ok(reader)
139    }
140
141    /// Lookup an IP address in the database.
142    ///
143    /// Returns a [`LookupResult`] that can be used to:
144    /// - Check if data exists with [`has_data()`](LookupResult::has_data)
145    /// - Get the network containing the IP with [`network()`](LookupResult::network)
146    /// - Decode the full record with [`decode()`](LookupResult::decode)
147    /// - Decode a specific path with [`decode_path()`](LookupResult::decode_path)
148    /// - Get a low-level decoder with [`decoder()`](LookupResult::decoder)
149    ///
150    /// # Examples
151    ///
152    /// Basic city lookup:
153    /// ```
154    /// # use maxminddb::geoip2;
155    /// # use std::net::IpAddr;
156    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
157    /// let reader = maxminddb::Reader::open_readfile(
158    ///     "test-data/test-data/GeoIP2-City-Test.mmdb")?;
159    ///
160    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
161    /// let result = reader.lookup(ip)?;
162    ///
163    /// if let Some(city) = result.decode::<geoip2::City>()? {
164    ///     // Access nested structs directly - no Option unwrapping needed
165    ///     if let Some(name) = city.city.names.english {
166    ///         println!("City: {}", name);
167    ///     }
168    /// } else {
169    ///     println!("No data found for IP {}", ip);
170    /// }
171    /// # Ok(())
172    /// # }
173    /// ```
174    ///
175    /// Selective field access:
176    /// ```
177    /// # use maxminddb::{Reader, PathElement};
178    /// # use std::net::IpAddr;
179    /// # fn main() -> Result<(), maxminddb::MaxMindDbError> {
180    /// let reader = Reader::open_readfile(
181    ///     "test-data/test-data/GeoIP2-City-Test.mmdb")?;
182    /// let ip: IpAddr = "89.160.20.128".parse().unwrap();
183    ///
184    /// let result = reader.lookup(ip)?;
185    /// let country_code: Option<String> = result.decode_path(&[
186    ///     PathElement::Key("country"),
187    ///     PathElement::Key("iso_code"),
188    /// ])?;
189    ///
190    /// println!("Country: {:?}", country_code);
191    /// # Ok(())
192    /// # }
193    /// ```
194    pub fn lookup(&'de self, address: IpAddr) -> Result<LookupResult<'de, S>, MaxMindDbError> {
195        // Check for IPv6 address in IPv4-only database
196        if matches!(address, IpAddr::V6(_)) && self.metadata.ip_version == 4 {
197            return Err(MaxMindDbError::invalid_input(
198                "cannot look up IPv6 address in IPv4-only database",
199            ));
200        }
201
202        let ip_int = IpInt::new(address);
203        let (pointer, prefix_len) = self.find_address_in_tree(&ip_int)?;
204
205        // For IPv4 addresses in IPv6 databases, adjust prefix_len to reflect
206        // the actual bit depth in the tree. The ipv4_start_bit_depth tells us
207        // how deep in the IPv6 tree we were when we found the IPv4 subtree.
208        let prefix_len = if matches!(address, IpAddr::V4(_)) && self.metadata.ip_version == 6 {
209            self.ipv4_start_bit_depth + prefix_len
210        } else {
211            prefix_len
212        };
213
214        if pointer == 0 {
215            // IP not found in database
216            Ok(LookupResult::new_not_found(self, prefix_len as u8, address))
217        } else {
218            // Resolve the pointer to a data offset
219            let data_offset = self.resolve_data_pointer(pointer)?;
220            Ok(LookupResult::new_found(
221                self,
222                data_offset,
223                prefix_len as u8,
224                address,
225            ))
226        }
227    }
228
229    /// Iterate over all networks in the database.
230    ///
231    /// This is a convenience method equivalent to calling [`within()`](Self::within)
232    /// with `0.0.0.0/0` for IPv4-only databases or `::/0` for IPv6 databases.
233    ///
234    /// # Arguments
235    ///
236    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
237    ///   for standard behavior.
238    ///
239    /// # Examples
240    ///
241    /// Iterate over all networks with default options:
242    /// ```
243    /// use maxminddb::{geoip2, Reader};
244    ///
245    /// let reader = Reader::open_readfile(
246    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
247    ///
248    /// let mut count = 0;
249    /// for result in reader.networks(Default::default()).unwrap() {
250    ///     let lookup = result.unwrap();
251    ///     count += 1;
252    ///     if count >= 10 { break; }
253    /// }
254    /// ```
255    pub fn networks(&'de self, options: WithinOptions) -> Result<Within<'de, S>, MaxMindDbError> {
256        let cidr = if self.metadata.ip_version == 6 {
257            IpNetwork::V6("::/0".parse().unwrap())
258        } else {
259            IpNetwork::V4("0.0.0.0/0".parse().unwrap())
260        };
261        self.within(cidr, options)
262    }
263
264    /// Iterate over IP networks within a CIDR range.
265    ///
266    /// Returns an iterator that yields [`LookupResult`] for each network in the
267    /// database that falls within the specified CIDR range.
268    ///
269    /// # Arguments
270    ///
271    /// * `cidr` - The CIDR range to iterate over.
272    /// * `options` - Controls which networks are yielded. Use [`Default::default()`]
273    ///   for standard behavior (skip aliases, skip networks without data, include
274    ///   empty values).
275    ///
276    /// # Examples
277    ///
278    /// Iterate over all IPv4 networks:
279    /// ```
280    /// use ipnetwork::IpNetwork;
281    /// use maxminddb::{geoip2, Reader};
282    ///
283    /// let reader = Reader::open_readfile(
284    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
285    ///
286    /// let ipv4_all = IpNetwork::V4("0.0.0.0/0".parse().unwrap());
287    /// let mut count = 0;
288    /// for result in reader.within(ipv4_all, Default::default()).unwrap() {
289    ///     let lookup = result.unwrap();
290    ///     let network = lookup.network().unwrap();
291    ///     let city: geoip2::City = lookup.decode().unwrap().unwrap();
292    ///     let city_name = city.city.names.english;
293    ///     println!("Network: {}, City: {:?}", network, city_name);
294    ///     count += 1;
295    ///     if count >= 10 { break; } // Limit output for example
296    /// }
297    /// ```
298    ///
299    /// Search within a specific subnet:
300    /// ```
301    /// use ipnetwork::IpNetwork;
302    /// use maxminddb::{geoip2, Reader};
303    ///
304    /// let reader = Reader::open_readfile(
305    ///     "test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
306    ///
307    /// let subnet = IpNetwork::V4("192.168.0.0/16".parse().unwrap());
308    /// for result in reader.within(subnet, Default::default()).unwrap() {
309    ///     match result {
310    ///         Ok(lookup) => {
311    ///             let network = lookup.network().unwrap();
312    ///             println!("Found: {}", network);
313    ///         }
314    ///         Err(e) => eprintln!("Error: {}", e),
315    ///     }
316    /// }
317    /// ```
318    ///
319    /// Include networks without data:
320    /// ```
321    /// use ipnetwork::IpNetwork;
322    /// use maxminddb::{Reader, WithinOptions};
323    ///
324    /// let reader = Reader::open_readfile(
325    ///     "test-data/test-data/MaxMind-DB-test-mixed-24.mmdb").unwrap();
326    ///
327    /// let opts = WithinOptions::default().include_networks_without_data();
328    /// for result in reader.within("1.0.0.0/8".parse().unwrap(), opts).unwrap() {
329    ///     let lookup = result.unwrap();
330    ///     if !lookup.has_data() {
331    ///         println!("Network {} has no data", lookup.network().unwrap());
332    ///     }
333    /// }
334    /// ```
335    pub fn within(
336        &'de self,
337        cidr: IpNetwork,
338        options: WithinOptions,
339    ) -> Result<Within<'de, S>, MaxMindDbError> {
340        let ip_address = cidr.network();
341        let prefix_len = cidr.prefix() as usize;
342        let ip_int = IpInt::new(ip_address);
343        let bit_count = ip_int.bit_count();
344
345        let mut node = self.start_node(bit_count);
346        let node_count = self.metadata.node_count as usize;
347
348        let mut stack: Vec<WithinNode> = Vec::with_capacity(bit_count - prefix_len);
349
350        // Traverse down the tree to the level that matches the cidr mark
351        let mut depth = 0_usize;
352        for i in 0..prefix_len {
353            let bit = ip_int.get_bit(i);
354            node = self.read_node(node, bit as usize)?;
355            depth = i + 1; // We've now traversed i+1 bits (bits 0 through i)
356
357            if node >= node_count {
358                // We've hit a data node or dead end before we exhausted our prefix.
359                // This means the requested CIDR is contained in a single record.
360                break;
361            }
362        }
363
364        // Always push the node - it could be:
365        // - A data node (> node_count): will be yielded as a single record
366        // - The empty node (== node_count): will be skipped unless include_networks_without_data
367        // - An internal node (< node_count): will be traversed to find all contained records
368        stack.push(WithinNode {
369            node,
370            ip_int,
371            prefix_len: depth,
372        });
373
374        let within = Within {
375            reader: self,
376            node_count,
377            stack,
378            options,
379        };
380
381        Ok(within)
382    }
383
384    fn find_address_in_tree(&self, ip_int: &IpInt) -> Result<(usize, usize), MaxMindDbError> {
385        let bit_count = ip_int.bit_count();
386        let mut node = self.start_node(bit_count);
387
388        let node_count = self.metadata.node_count as usize;
389        let mut prefix_len = bit_count;
390
391        for i in 0..bit_count {
392            if node >= node_count {
393                prefix_len = i;
394                break;
395            }
396            let bit = ip_int.get_bit(i);
397            node = self.read_node(node, bit as usize)?;
398        }
399        match node_count {
400            // If node == node_count, it means we hit the placeholder "empty" node
401            // return 0 as the pointer value to signify "not found".
402            _ if node == node_count => Ok((0, prefix_len)),
403            _ if node > node_count => Ok((node, prefix_len)),
404            _ => Err(MaxMindDbError::invalid_database(
405                "invalid node in search tree",
406            )),
407        }
408    }
409
410    #[inline]
411    fn start_node(&self, length: usize) -> usize {
412        if length == 128 {
413            0
414        } else {
415            self.ipv4_start
416        }
417    }
418
419    /// Find the IPv4 start node and the bit depth at which it was found.
420    /// Returns (node, depth) where depth is how far into the tree we traversed.
421    fn find_ipv4_start(&self) -> Result<(usize, usize), MaxMindDbError> {
422        if self.metadata.ip_version != 6 {
423            return Ok((0, 0));
424        }
425
426        // We are looking up an IPv4 address in an IPv6 tree. Skip over the
427        // first 96 nodes.
428        let mut node: usize = 0;
429        let mut depth: usize = 0;
430        for i in 0_u8..96 {
431            if node >= self.metadata.node_count as usize {
432                depth = i as usize;
433                break;
434            }
435            node = self.read_node(node, 0)?;
436            depth = (i + 1) as usize;
437        }
438        Ok((node, depth))
439    }
440
441    #[inline(always)]
442    pub(crate) fn read_node(
443        &self,
444        node_number: usize,
445        index: usize,
446    ) -> Result<usize, MaxMindDbError> {
447        let buf = self.buf.as_ref();
448        let base_offset = node_number * (self.metadata.record_size as usize) / 4;
449
450        let val = match self.metadata.record_size {
451            24 => {
452                let offset = base_offset + index * 3;
453                (buf[offset] as usize) << 16
454                    | (buf[offset + 1] as usize) << 8
455                    | buf[offset + 2] as usize
456            }
457            28 => {
458                let middle = if index != 0 {
459                    buf[base_offset + 3] & 0x0F
460                } else {
461                    (buf[base_offset + 3] & 0xF0) >> 4
462                };
463                let offset = base_offset + index * 4;
464                (middle as usize) << 24
465                    | (buf[offset] as usize) << 16
466                    | (buf[offset + 1] as usize) << 8
467                    | buf[offset + 2] as usize
468            }
469            32 => {
470                let offset = base_offset + index * 4;
471                (buf[offset] as usize) << 24
472                    | (buf[offset + 1] as usize) << 16
473                    | (buf[offset + 2] as usize) << 8
474                    | buf[offset + 3] as usize
475            }
476            s => {
477                return Err(MaxMindDbError::invalid_database(format!(
478                    "unknown record size: {s}"
479                )))
480            }
481        };
482        Ok(val)
483    }
484
485    /// Resolves a pointer from the search tree to an offset in the data section.
486    #[inline]
487    pub(crate) fn resolve_data_pointer(&self, pointer: usize) -> Result<usize, MaxMindDbError> {
488        let resolved = pointer - (self.metadata.node_count as usize) - 16;
489
490        // Check bounds using pointer_base which marks the start of the data section
491        if resolved >= (self.buf.as_ref().len() - self.pointer_base) {
492            return Err(MaxMindDbError::invalid_database(
493                "the MaxMind DB file's data pointer resolves to an invalid location",
494            ));
495        }
496
497        Ok(resolved)
498    }
499
500    /// Performs comprehensive validation of the MaxMind DB file.
501    ///
502    /// This method validates:
503    /// - Metadata section: format versions, required fields, and value constraints
504    /// - Search tree: traverses all networks to verify tree structure integrity
505    /// - Data section separator: validates the 16-byte separator between tree and data
506    /// - Data section: verifies all data records referenced by the search tree
507    ///
508    /// The verifier is stricter than the MaxMind DB specification and may return
509    /// errors on some databases that are still readable by normal operations.
510    /// This method is useful for:
511    /// - Validating database files after download or generation
512    /// - Debugging database corruption issues
513    /// - Ensuring database integrity in critical applications
514    ///
515    /// Note: Verification traverses the entire database and may be slow on large files.
516    /// The method is thread-safe and can be called on an active Reader.
517    ///
518    /// # Example
519    ///
520    /// ```
521    /// use maxminddb::Reader;
522    ///
523    /// let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
524    /// reader.verify().expect("Database should be valid");
525    /// ```
526    pub fn verify(&self) -> Result<(), MaxMindDbError> {
527        self.verify_metadata()?;
528        self.verify_database()
529    }
530
531    fn verify_metadata(&self) -> Result<(), MaxMindDbError> {
532        let m = &self.metadata;
533
534        if m.binary_format_major_version != 2 {
535            return Err(MaxMindDbError::invalid_database(format!(
536                "binary_format_major_version - Expected: 2 Actual: {}",
537                m.binary_format_major_version
538            )));
539        }
540        if m.binary_format_minor_version != 0 {
541            return Err(MaxMindDbError::invalid_database(format!(
542                "binary_format_minor_version - Expected: 0 Actual: {}",
543                m.binary_format_minor_version
544            )));
545        }
546        if m.database_type.is_empty() {
547            return Err(MaxMindDbError::invalid_database(
548                "database_type - Expected: non-empty string Actual: \"\"",
549            ));
550        }
551        if m.description.is_empty() {
552            return Err(MaxMindDbError::invalid_database(
553                "description - Expected: non-empty map Actual: {}",
554            ));
555        }
556        if m.ip_version != 4 && m.ip_version != 6 {
557            return Err(MaxMindDbError::invalid_database(format!(
558                "ip_version - Expected: 4 or 6 Actual: {}",
559                m.ip_version
560            )));
561        }
562        if m.record_size != 24 && m.record_size != 28 && m.record_size != 32 {
563            return Err(MaxMindDbError::invalid_database(format!(
564                "record_size - Expected: 24, 28, or 32 Actual: {}",
565                m.record_size
566            )));
567        }
568        if m.node_count == 0 {
569            return Err(MaxMindDbError::invalid_database(
570                "node_count - Expected: positive integer Actual: 0",
571            ));
572        }
573        Ok(())
574    }
575
576    fn verify_database(&self) -> Result<(), MaxMindDbError> {
577        let offsets = self.verify_search_tree()?;
578        self.verify_data_section_separator()?;
579        self.verify_data_section(offsets)
580    }
581
582    fn verify_search_tree(&self) -> Result<HashSet<usize>, MaxMindDbError> {
583        let mut offsets = HashSet::new();
584        let opts = WithinOptions::default().include_networks_without_data();
585
586        // Maximum number of networks we can expect in a valid database.
587        // A database with N nodes can have at most 2N data entries (each leaf node
588        // can have data). We add some margin for safety.
589        let max_iterations = (self.metadata.node_count as usize).saturating_mul(3);
590        let mut iteration_count = 0usize;
591
592        for result in self.networks(opts)? {
593            let lookup = result?;
594            if let Some(offset) = lookup.offset() {
595                offsets.insert(offset);
596            }
597
598            iteration_count += 1;
599            if iteration_count > max_iterations {
600                return Err(MaxMindDbError::invalid_database(format!(
601                    "search tree appears to have a cycle or invalid structure (exceeded {max_iterations} iterations)"
602                )));
603            }
604        }
605        Ok(offsets)
606    }
607
608    fn verify_data_section_separator(&self) -> Result<(), MaxMindDbError> {
609        let separator_start =
610            self.metadata.node_count as usize * self.metadata.record_size as usize / 4;
611        let separator_end = separator_start + DATA_SECTION_SEPARATOR_SIZE;
612
613        if separator_end > self.buf.as_ref().len() {
614            return Err(MaxMindDbError::invalid_database_at(
615                "data section separator extends past end of file",
616                separator_start,
617            ));
618        }
619
620        let separator = &self.buf.as_ref()[separator_start..separator_end];
621
622        for &b in separator {
623            if b != 0 {
624                return Err(MaxMindDbError::invalid_database_at(
625                    format!("unexpected byte in data separator: {separator:?}"),
626                    separator_start,
627                ));
628            }
629        }
630        Ok(())
631    }
632
633    fn verify_data_section(&self, offsets: HashSet<usize>) -> Result<(), MaxMindDbError> {
634        let data_section = &self.buf.as_ref()[self.pointer_base..];
635
636        // Verify each offset from the search tree points to valid, decodable data
637        for &offset in &offsets {
638            if offset >= data_section.len() {
639                return Err(MaxMindDbError::invalid_database_at(
640                    format!(
641                        "search tree pointer is beyond data section (len: {})",
642                        data_section.len()
643                    ),
644                    offset,
645                ));
646            }
647
648            let mut dec = decoder::Decoder::new(data_section, offset);
649
650            // Try to skip/decode the value to verify it's valid
651            if let Err(e) = dec.skip_value_for_verification() {
652                return Err(MaxMindDbError::invalid_database_at(
653                    format!("decoding error: {e}"),
654                    offset,
655                ));
656            }
657        }
658
659        Ok(())
660    }
661}
662
663fn find_metadata_start(buf: &[u8]) -> Result<usize, MaxMindDbError> {
664    const METADATA_START_MARKER: &[u8] = b"\xab\xcd\xefMaxMind.com";
665
666    memchr::memmem::rfind(buf, METADATA_START_MARKER)
667        .map(|x| x + METADATA_START_MARKER.len())
668        .ok_or_else(|| {
669            MaxMindDbError::invalid_database("could not find MaxMind DB metadata in file")
670        })
671}