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