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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let city: geoip2::City = result.decode().unwrap().unwrap();
248 assert!(!city.city.is_empty(), "Expected city data");
249
250 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 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 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 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 let u16_val: Option<u16> = result.decode_path(&[PathElement::Key("uint16")]).unwrap();
319 assert_eq!(u16_val, Some(100));
320
321 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 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 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 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 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 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 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 let utf8: Option<String> = result
370 .decode_path(&[PathElement::Key("utf8_string")])
371 .unwrap();
372 assert_eq!(utf8, Some("unicode! ☯ - ♫".to_owned()));
373 }
374}