1#![deny(trivial_casts, trivial_numeric_casts, unused_import_braces)]
2#[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
88pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let city: geoip2::City = result.decode().unwrap().unwrap();
249 assert!(!city.city.is_empty(), "Expected city data");
250
251 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 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 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 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 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 let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
370 assert_eq!(u16_val, Some(100));
371
372 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 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 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 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 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 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 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 let utf8: Option<String> = result
421 .decode_path(&[PathElement::Key("utf8_string")])
422 .unwrap();
423 assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
424 }
425}