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