Skip to main content

maxminddb/
lib.rs

1#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
2//! # MaxMind DB Reader
3//!
4//! This library reads the MaxMind DB format, including the GeoIP2 and GeoLite2 databases.
5//!
6//! ## Features
7//!
8//! This crate provides several optional features for performance and functionality:
9//!
10//! - **`mmap`** (default: disabled): Enable memory-mapped file access for
11//!   better performance in long-running applications
12//! - **`simdutf8`** (default: disabled): Use SIMD instructions for faster
13//!   UTF-8 validation during string decoding
14//! - **`unsafe-str-decode`** (default: disabled): Skip UTF-8 validation
15//!   when deserializing trusted database strings into Rust `str` or `String`
16//!   values. Cross-runtime format adapters should prefer
17//!   [`deserialize_any_with_raw_strings()`] and validate while constructing
18//!   the target runtime's string type.
19//!
20//! **Note**: `simdutf8` and `unsafe-str-decode` are mutually exclusive.
21//!
22//! ## Database Compatibility
23//!
24//! This library supports all MaxMind DB format databases:
25//! - **GeoIP2** databases (City, Country, Enterprise, ISP, etc.)
26//! - **GeoLite2** databases (free versions)
27//! - Custom MaxMind DB format databases
28//!
29//! ## Thread Safety
30//!
31//! The `Reader` is `Send` and `Sync`, making it safe to share across threads.
32//! This makes it ideal for web servers and other concurrent applications.
33//!
34//! ## Quick Start
35//!
36//! ```rust
37//! use maxminddb::{Reader, geoip2};
38//! use std::net::IpAddr;
39//!
40//! fn main() -> Result<(), Box<dyn std::error::Error>> {
41//!     // Open database file
42//! #   let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb")?;
43//! #   /*
44//!     let reader = Reader::open_readfile("/path/to/GeoIP2-City.mmdb")?;
45//! #   */
46//!
47//!     // Look up an IP address
48//!     let ip: IpAddr = "89.160.20.128".parse()?;
49//!     let result = reader.lookup(ip)?;
50//!
51//!     if let Some(city) = result.decode::<geoip2::City>()? {
52//!         // Access nested structs directly - no Option unwrapping needed
53//!         println!("Country: {}", city.country.iso_code.unwrap_or("Unknown"));
54//!     }
55//!
56//!     Ok(())
57//! }
58//! ```
59//!
60//! ## Selective Field Access
61//!
62//! Use `decode_path` to extract specific fields without deserializing the entire record:
63//!
64//! ```rust
65//! use maxminddb::{path, Reader};
66//! use std::net::IpAddr;
67//!
68//! let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
69//! let ip: IpAddr = "89.160.20.128".parse().unwrap();
70//!
71//! let result = reader.lookup(ip).unwrap();
72//! let country_code: Option<String> = result.decode_path(&path!["country", "iso_code"]).unwrap();
73//!
74//! println!("Country: {:?}", country_code);
75//! ```
76
77#[cfg(all(feature = "simdutf8", feature = "unsafe-str-decode"))]
78compile_error!("features `simdutf8` and `unsafe-str-decode` are mutually exclusive");
79
80mod decoder;
81mod error;
82pub mod geoip2;
83mod metadata;
84mod reader;
85mod result;
86mod within;
87
88// Re-export public types
89pub use decoder::deserialize_any_with_raw_strings;
90pub use error::MaxMindDbError;
91pub use metadata::Metadata;
92pub use reader::Reader;
93pub use result::{LookupResult, PathElement};
94pub use within::{Within, WithinOptions};
95
96#[cfg(feature = "mmap")]
97pub use memmap2::Mmap;
98
99/// Internal entry points for the cargo-fuzz targets.
100#[cfg(feature = "fuzzing")]
101#[doc(hidden)]
102pub mod fuzzing {
103    use serde::Deserialize;
104
105    use crate::decoder::{Decoder, VerificationState};
106    use crate::MaxMindDbError;
107
108    /// Deserialize one data-section value through the Serde decoder.
109    pub fn decode<'de, T>(data: &'de [u8]) -> Result<T, MaxMindDbError>
110    where
111        T: Deserialize<'de>,
112    {
113        T::deserialize(&mut Decoder::new(data, 0))
114    }
115
116    /// Validate one data-section value through the verification decoder.
117    pub fn verify(data: &[u8]) -> Result<(), MaxMindDbError> {
118        Decoder::new(data, 0).skip_value_for_verification(&mut VerificationState::default())
119    }
120}
121
122#[cfg(test)]
123mod reader_test;
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use std::net::IpAddr;
129
130    #[test]
131    fn test_lookup_network() {
132        use std::collections::HashMap;
133
134        struct TestCase {
135            ip: &'static str,
136            db_file: &'static str,
137            expected_network: &'static str,
138            expected_found: bool,
139        }
140
141        let test_cases = [
142            // IPv4 address in IPv6 database - not found, returns containing network
143            TestCase {
144                ip: "1.1.1.1",
145                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb",
146                expected_network: "1.0.0.0/8",
147                expected_found: false,
148            },
149            // IPv6 exact match
150            TestCase {
151                ip: "::1:ffff:ffff",
152                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
153                expected_network: "::1:ffff:ffff/128",
154                expected_found: true,
155            },
156            // IPv6 network match (not exact)
157            TestCase {
158                ip: "::2:0:1",
159                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
160                expected_network: "::2:0:0/122",
161                expected_found: true,
162            },
163            // IPv4 exact match
164            TestCase {
165                ip: "1.1.1.1",
166                db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
167                expected_network: "1.1.1.1/32",
168                expected_found: true,
169            },
170            // IPv4 network match (not exact)
171            TestCase {
172                ip: "1.1.1.3",
173                db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
174                expected_network: "1.1.1.2/31",
175                expected_found: true,
176            },
177            // IPv4 in decoder test database
178            TestCase {
179                ip: "1.1.1.3",
180                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
181                expected_network: "1.1.1.0/24",
182                expected_found: true,
183            },
184            // IPv4-mapped IPv6 address - preserves IPv6 form
185            TestCase {
186                ip: "::ffff:1.1.1.128",
187                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
188                expected_network: "::ffff:1.1.1.0/120",
189                expected_found: true,
190            },
191            // IPv4-compatible IPv6 address - uses compressed IPv6 notation
192            TestCase {
193                ip: "::1.1.1.128",
194                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
195                expected_network: "::101:100/120",
196                expected_found: true,
197            },
198            // No IPv4 search tree - IPv4 address returns ::/64
199            TestCase {
200                ip: "200.0.2.1",
201                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
202                expected_network: "::/64",
203                expected_found: true,
204            },
205            // No IPv4 search tree - IPv6 address in IPv4 range
206            TestCase {
207                ip: "::200.0.2.1",
208                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
209                expected_network: "::/64",
210                expected_found: true,
211            },
212            // No IPv4 search tree - IPv6 address at boundary of IPv4 space
213            TestCase {
214                ip: "0:0:0:0:ffff:ffff:ffff:ffff",
215                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
216                expected_network: "::/64",
217                expected_found: true,
218            },
219            // No IPv4 search tree - high IPv6 address not found
220            TestCase {
221                ip: "ef00::",
222                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
223                expected_network: "8000::/1",
224                expected_found: false,
225            },
226        ];
227
228        // Cache readers to avoid reopening the same file multiple times
229        let mut readers: HashMap<&str, Reader<Vec<u8>>> = HashMap::new();
230
231        for test in &test_cases {
232            let reader = readers
233                .entry(test.db_file)
234                .or_insert_with(|| Reader::open_readfile(test.db_file).unwrap());
235
236            let ip: IpAddr = test.ip.parse().unwrap();
237            let result = reader.lookup(ip).unwrap();
238
239            assert_eq!(
240                result.has_data(),
241                test.expected_found,
242                "IP {} in {}: expected has_data={}, got has_data={}",
243                test.ip,
244                test.db_file,
245                test.expected_found,
246                result.has_data()
247            );
248
249            let network = result.network().unwrap();
250            assert_eq!(
251                network.to_string(),
252                test.expected_network,
253                "IP {} in {}: expected network {}, got {}",
254                test.ip,
255                test.db_file,
256                test.expected_network,
257                network
258            );
259        }
260    }
261
262    #[test]
263    fn test_lookup_with_geoip_data() {
264        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
265        let ip: IpAddr = "89.160.20.128".parse().unwrap();
266
267        let result = reader.lookup(ip).unwrap();
268        assert!(result.has_data(), "lookup should find known IP");
269
270        // Decode the data
271        let city: geoip2::City = result.decode().unwrap().unwrap();
272        assert!(!city.city.is_empty(), "Expected city data");
273
274        // Check full network (not just prefix)
275        let network = result.network().unwrap();
276        assert_eq!(
277            network.to_string(),
278            "89.160.20.128/25",
279            "Expected network 89.160.20.128/25"
280        );
281
282        // Check offset is available for caching
283        assert!(
284            result.offset().is_some(),
285            "Expected offset to be Some for found IP"
286        );
287    }
288
289    #[test]
290    fn test_lookup_network_uses_measured_ipv4_subtree_depth() {
291        let mut reader =
292            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb").unwrap();
293        assert_eq!(reader.metadata().ip_version, 6);
294
295        // Simulate a valid IPv6 database whose IPv4 subtree starts somewhere
296        // other than bit 96. Using a shallow subtree depth keeps the combined
297        // prefix length <= 32, which would be ambiguous without an explicit
298        // Lookup vs Iter source flag.
299        reader.ipv4_start_bit_depth = 16;
300
301        let result = reader.lookup("1.1.1.1".parse().unwrap()).unwrap();
302        assert_eq!(result.network().unwrap().to_string(), "1.0.0.0/8");
303    }
304
305    #[test]
306    fn test_lookup_offset_is_stable_for_shared_record() {
307        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
308
309        let first = reader.lookup("89.160.20.128".parse().unwrap()).unwrap();
310        let second = reader.lookup("89.160.20.129".parse().unwrap()).unwrap();
311
312        assert!(first.has_data());
313        assert!(second.has_data());
314        assert_eq!(first.network().unwrap(), second.network().unwrap());
315        assert_eq!(
316            first.offset(),
317            second.offset(),
318            "IPs in the same record should share a cacheable offset"
319        );
320    }
321
322    #[test]
323    fn test_decode_path() {
324        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
325        let ip: IpAddr = "89.160.20.128".parse().unwrap();
326
327        let result = reader.lookup(ip).unwrap();
328
329        // Navigate to country.iso_code
330        let iso_code: Option<String> = result
331            .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
332            .unwrap();
333        assert_eq!(iso_code, Some("SE".to_owned()));
334
335        // Navigate to non-existent path
336        let missing: Option<String> = result
337            .decode_path(&[PathElement::Key("nonexistent")])
338            .unwrap();
339        assert!(missing.is_none());
340    }
341
342    #[test]
343    fn test_decode_path_on_not_found_lookup() {
344        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
345        let ip: IpAddr = "2c0f:ff00::1".parse().unwrap();
346
347        let result = reader.lookup(ip).unwrap();
348
349        assert!(!result.has_data());
350        assert!(result.offset().is_none());
351        assert!(result.decode::<geoip2::City>().unwrap().is_none());
352
353        let country_code: Option<String> = result
354            .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
355            .unwrap();
356        assert!(country_code.is_none());
357    }
358
359    #[test]
360    fn test_ipv6_in_ipv4_database() {
361        let reader =
362            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb").unwrap();
363        let ip: IpAddr = "2001::".parse().unwrap();
364
365        let result = reader.lookup(ip);
366        match result {
367            Err(MaxMindDbError::InvalidInput { message }) => {
368                assert!(
369                    message.contains("IPv6") && message.contains("IPv4"),
370                    "Expected error message about IPv6 in IPv4 database, got: {}",
371                    message
372                );
373            }
374            Err(e) => panic!(
375                "Expected InvalidInput error for IPv6 in IPv4 database, got: {:?}",
376                e
377            ),
378            Ok(_) => panic!("Expected error for IPv6 lookup in IPv4-only database"),
379        }
380    }
381
382    #[test]
383    fn test_decode_path_comprehensive() {
384        let reader =
385            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap();
386        let ip: IpAddr = "::1.1.1.0".parse().unwrap();
387
388        let result = reader.lookup(ip).unwrap();
389        assert!(result.has_data());
390
391        // Test simple path: uint16
392        let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
393        assert_eq!(u16_val, Some(100));
394
395        // Test array access: first element
396        let arr_first: Option<u32> = result
397            .decode_path(&[PathElement::Key("array"), PathElement::Index(0)])
398            .unwrap();
399        assert_eq!(arr_first, Some(1));
400
401        // Test array access: last element (index 2)
402        let arr_last: Option<u32> = result
403            .decode_path(&[PathElement::Key("array"), PathElement::Index(2)])
404            .unwrap();
405        assert_eq!(arr_last, Some(3));
406
407        // Test array access: out of bounds (index 3) returns None
408        let arr_oob: Option<u32> = result
409            .decode_path(&[PathElement::Key("array"), PathElement::Index(3)])
410            .unwrap();
411        assert!(arr_oob.is_none());
412
413        // Test IndexFromEnd: 0 means last element
414        let arr_last: Option<u32> = result
415            .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(0)])
416            .unwrap();
417        assert_eq!(arr_last, Some(3));
418
419        // Test IndexFromEnd: 2 means first element (array has 3 elements)
420        let arr_first: Option<u32> = result
421            .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(2)])
422            .unwrap();
423        assert_eq!(arr_first, Some(1));
424
425        // Test nested path: map.mapX.arrayX[1]
426        let nested: Option<u32> = result
427            .decode_path(&[
428                PathElement::Key("map"),
429                PathElement::Key("mapX"),
430                PathElement::Key("arrayX"),
431                PathElement::Index(1),
432            ])
433            .unwrap();
434        assert_eq!(nested, Some(8));
435
436        // Test non-existent key returns None
437        let missing: Option<u32> = result
438            .decode_path(&[PathElement::Key("does-not-exist"), PathElement::Index(1)])
439            .unwrap();
440        assert!(missing.is_none());
441
442        // Test utf8_string path
443        let utf8: Option<String> = result
444            .decode_path(&[PathElement::Key("utf8_string")])
445            .unwrap();
446        assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
447    }
448}