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#[cfg(test)]
100mod reader_test;
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::net::IpAddr;
106
107    #[test]
108    fn test_lookup_network() {
109        use std::collections::HashMap;
110
111        struct TestCase {
112            ip: &'static str,
113            db_file: &'static str,
114            expected_network: &'static str,
115            expected_found: bool,
116        }
117
118        let test_cases = [
119            // IPv4 address in IPv6 database - not found, returns containing network
120            TestCase {
121                ip: "1.1.1.1",
122                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb",
123                expected_network: "1.0.0.0/8",
124                expected_found: false,
125            },
126            // IPv6 exact match
127            TestCase {
128                ip: "::1:ffff:ffff",
129                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
130                expected_network: "::1:ffff:ffff/128",
131                expected_found: true,
132            },
133            // IPv6 network match (not exact)
134            TestCase {
135                ip: "::2:0:1",
136                db_file: "test-data/test-data/MaxMind-DB-test-ipv6-24.mmdb",
137                expected_network: "::2:0:0/122",
138                expected_found: true,
139            },
140            // IPv4 exact match
141            TestCase {
142                ip: "1.1.1.1",
143                db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
144                expected_network: "1.1.1.1/32",
145                expected_found: true,
146            },
147            // IPv4 network match (not exact)
148            TestCase {
149                ip: "1.1.1.3",
150                db_file: "test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb",
151                expected_network: "1.1.1.2/31",
152                expected_found: true,
153            },
154            // IPv4 in decoder test database
155            TestCase {
156                ip: "1.1.1.3",
157                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
158                expected_network: "1.1.1.0/24",
159                expected_found: true,
160            },
161            // IPv4-mapped IPv6 address - preserves IPv6 form
162            TestCase {
163                ip: "::ffff:1.1.1.128",
164                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
165                expected_network: "::ffff:1.1.1.0/120",
166                expected_found: true,
167            },
168            // IPv4-compatible IPv6 address - uses compressed IPv6 notation
169            TestCase {
170                ip: "::1.1.1.128",
171                db_file: "test-data/test-data/MaxMind-DB-test-decoder.mmdb",
172                expected_network: "::101:100/120",
173                expected_found: true,
174            },
175            // No IPv4 search tree - IPv4 address returns ::/64
176            TestCase {
177                ip: "200.0.2.1",
178                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
179                expected_network: "::/64",
180                expected_found: true,
181            },
182            // No IPv4 search tree - IPv6 address in IPv4 range
183            TestCase {
184                ip: "::200.0.2.1",
185                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
186                expected_network: "::/64",
187                expected_found: true,
188            },
189            // No IPv4 search tree - IPv6 address at boundary of IPv4 space
190            TestCase {
191                ip: "0:0:0:0:ffff:ffff:ffff:ffff",
192                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
193                expected_network: "::/64",
194                expected_found: true,
195            },
196            // No IPv4 search tree - high IPv6 address not found
197            TestCase {
198                ip: "ef00::",
199                db_file: "test-data/test-data/MaxMind-DB-no-ipv4-search-tree.mmdb",
200                expected_network: "8000::/1",
201                expected_found: false,
202            },
203        ];
204
205        // Cache readers to avoid reopening the same file multiple times
206        let mut readers: HashMap<&str, Reader<Vec<u8>>> = HashMap::new();
207
208        for test in &test_cases {
209            let reader = readers
210                .entry(test.db_file)
211                .or_insert_with(|| Reader::open_readfile(test.db_file).unwrap());
212
213            let ip: IpAddr = test.ip.parse().unwrap();
214            let result = reader.lookup(ip).unwrap();
215
216            assert_eq!(
217                result.has_data(),
218                test.expected_found,
219                "IP {} in {}: expected has_data={}, got has_data={}",
220                test.ip,
221                test.db_file,
222                test.expected_found,
223                result.has_data()
224            );
225
226            let network = result.network().unwrap();
227            assert_eq!(
228                network.to_string(),
229                test.expected_network,
230                "IP {} in {}: expected network {}, got {}",
231                test.ip,
232                test.db_file,
233                test.expected_network,
234                network
235            );
236        }
237    }
238
239    #[test]
240    fn test_lookup_with_geoip_data() {
241        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
242        let ip: IpAddr = "89.160.20.128".parse().unwrap();
243
244        let result = reader.lookup(ip).unwrap();
245        assert!(result.has_data(), "lookup should find known IP");
246
247        // Decode the data
248        let city: geoip2::City = result.decode().unwrap().unwrap();
249        assert!(!city.city.is_empty(), "Expected city data");
250
251        // Check full network (not just prefix)
252        let network = result.network().unwrap();
253        assert_eq!(
254            network.to_string(),
255            "89.160.20.128/25",
256            "Expected network 89.160.20.128/25"
257        );
258
259        // Check offset is available for caching
260        assert!(
261            result.offset().is_some(),
262            "Expected offset to be Some for found IP"
263        );
264    }
265
266    #[test]
267    fn test_lookup_network_uses_measured_ipv4_subtree_depth() {
268        let mut reader =
269            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv6-32.mmdb").unwrap();
270        assert_eq!(reader.metadata().ip_version, 6);
271
272        // Simulate a valid IPv6 database whose IPv4 subtree starts somewhere
273        // other than bit 96. Using a shallow subtree depth keeps the combined
274        // prefix length <= 32, which would be ambiguous without an explicit
275        // Lookup vs Iter source flag.
276        reader.ipv4_start_bit_depth = 16;
277
278        let result = reader.lookup("1.1.1.1".parse().unwrap()).unwrap();
279        assert_eq!(result.network().unwrap().to_string(), "1.0.0.0/8");
280    }
281
282    #[test]
283    fn test_lookup_offset_is_stable_for_shared_record() {
284        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
285
286        let first = reader.lookup("89.160.20.128".parse().unwrap()).unwrap();
287        let second = reader.lookup("89.160.20.129".parse().unwrap()).unwrap();
288
289        assert!(first.has_data());
290        assert!(second.has_data());
291        assert_eq!(first.network().unwrap(), second.network().unwrap());
292        assert_eq!(
293            first.offset(),
294            second.offset(),
295            "IPs in the same record should share a cacheable offset"
296        );
297    }
298
299    #[test]
300    fn test_decode_path() {
301        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
302        let ip: IpAddr = "89.160.20.128".parse().unwrap();
303
304        let result = reader.lookup(ip).unwrap();
305
306        // Navigate to country.iso_code
307        let iso_code: Option<String> = result
308            .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
309            .unwrap();
310        assert_eq!(iso_code, Some("SE".to_owned()));
311
312        // Navigate to non-existent path
313        let missing: Option<String> = result
314            .decode_path(&[PathElement::Key("nonexistent")])
315            .unwrap();
316        assert!(missing.is_none());
317    }
318
319    #[test]
320    fn test_decode_path_on_not_found_lookup() {
321        let reader = Reader::open_readfile("test-data/test-data/GeoIP2-City-Test.mmdb").unwrap();
322        let ip: IpAddr = "2c0f:ff00::1".parse().unwrap();
323
324        let result = reader.lookup(ip).unwrap();
325
326        assert!(!result.has_data());
327        assert!(result.offset().is_none());
328        assert!(result.decode::<geoip2::City>().unwrap().is_none());
329
330        let country_code: Option<String> = result
331            .decode_path(&[PathElement::Key("country"), PathElement::Key("iso_code")])
332            .unwrap();
333        assert!(country_code.is_none());
334    }
335
336    #[test]
337    fn test_ipv6_in_ipv4_database() {
338        let reader =
339            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-ipv4-24.mmdb").unwrap();
340        let ip: IpAddr = "2001::".parse().unwrap();
341
342        let result = reader.lookup(ip);
343        match result {
344            Err(MaxMindDbError::InvalidInput { message }) => {
345                assert!(
346                    message.contains("IPv6") && message.contains("IPv4"),
347                    "Expected error message about IPv6 in IPv4 database, got: {}",
348                    message
349                );
350            }
351            Err(e) => panic!(
352                "Expected InvalidInput error for IPv6 in IPv4 database, got: {:?}",
353                e
354            ),
355            Ok(_) => panic!("Expected error for IPv6 lookup in IPv4-only database"),
356        }
357    }
358
359    #[test]
360    fn test_decode_path_comprehensive() {
361        let reader =
362            Reader::open_readfile("test-data/test-data/MaxMind-DB-test-decoder.mmdb").unwrap();
363        let ip: IpAddr = "::1.1.1.0".parse().unwrap();
364
365        let result = reader.lookup(ip).unwrap();
366        assert!(result.has_data());
367
368        // Test simple path: uint16
369        let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
370        assert_eq!(u16_val, Some(100));
371
372        // Test array access: first element
373        let arr_first: Option<u32> = result
374            .decode_path(&[PathElement::Key("array"), PathElement::Index(0)])
375            .unwrap();
376        assert_eq!(arr_first, Some(1));
377
378        // Test array access: last element (index 2)
379        let arr_last: Option<u32> = result
380            .decode_path(&[PathElement::Key("array"), PathElement::Index(2)])
381            .unwrap();
382        assert_eq!(arr_last, Some(3));
383
384        // Test array access: out of bounds (index 3) returns None
385        let arr_oob: Option<u32> = result
386            .decode_path(&[PathElement::Key("array"), PathElement::Index(3)])
387            .unwrap();
388        assert!(arr_oob.is_none());
389
390        // Test IndexFromEnd: 0 means last element
391        let arr_last: Option<u32> = result
392            .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(0)])
393            .unwrap();
394        assert_eq!(arr_last, Some(3));
395
396        // Test IndexFromEnd: 2 means first element (array has 3 elements)
397        let arr_first: Option<u32> = result
398            .decode_path(&[PathElement::Key("array"), PathElement::IndexFromEnd(2)])
399            .unwrap();
400        assert_eq!(arr_first, Some(1));
401
402        // Test nested path: map.mapX.arrayX[1]
403        let nested: Option<u32> = result
404            .decode_path(&[
405                PathElement::Key("map"),
406                PathElement::Key("mapX"),
407                PathElement::Key("arrayX"),
408                PathElement::Index(1),
409            ])
410            .unwrap();
411        assert_eq!(nested, Some(8));
412
413        // Test non-existent key returns None
414        let missing: Option<u32> = result
415            .decode_path(&[PathElement::Key("does-not-exist"), PathElement::Index(1)])
416            .unwrap();
417        assert!(missing.is_none());
418
419        // Test utf8_string path
420        let utf8: Option<String> = result
421            .decode_path(&[PathElement::Key("utf8_string")])
422            .unwrap();
423        assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
424    }
425}