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