1#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
2#[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
85pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let city: geoip2::City = result.decode().unwrap().unwrap();
245 assert!(!city.city.is_empty(), "Expected city data");
246
247 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 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 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 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 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 let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
366 assert_eq!(u16_val, Some(100));
367
368 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 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 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 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 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 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 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 let utf8: Option<String> = result
417 .decode_path(&[PathElement::Key("utf8_string")])
418 .unwrap();
419 assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
420 }
421}