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