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}