maxmind-db-rust 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,25 +5,26 @@ use ::maxminddb as maxminddb_crate;
5
5
  use arc_swap::{ArcSwapOption, Guard};
6
6
  use ipnetwork::IpNetwork;
7
7
  use magnus::{
8
- error::Error, prelude::*, scan_args::get_kwargs, scan_args::scan_args, ExceptionClass,
9
- IntoValue, RArray, RClass, RHash, RModule, RString, Symbol, Value,
8
+ error::Error, prelude::*, scan_args::get_kwargs, scan_args::scan_args, typed_data::Obj,
9
+ ExceptionClass, IntoValue, RArray, RClass, RHash, RModule, RString, Symbol, Value,
10
10
  };
11
- use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within};
11
+ use maxminddb_crate::{MaxMindDbError, PathElement, Reader as MaxMindReader, Within};
12
12
  use memmap2::Mmap;
13
- use rustc_hash::FxHashMap;
13
+ use rustc_hash::FxHasher;
14
14
  use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
15
15
  use std::{
16
- cell::RefCell,
17
- collections::BTreeMap,
16
+ cell::{OnceCell, RefCell},
17
+ collections::{BTreeMap, VecDeque},
18
18
  fmt,
19
19
  fs::File,
20
+ hash::{Hash, Hasher},
20
21
  io::Read as IoRead,
21
- net::IpAddr,
22
+ net::{IpAddr, Ipv4Addr},
22
23
  path::Path,
23
24
  str::FromStr,
24
25
  sync::{
25
26
  atomic::{AtomicBool, Ordering},
26
- Arc,
27
+ Arc, Mutex,
27
28
  },
28
29
  };
29
30
 
@@ -31,57 +32,95 @@ use std::{
31
32
  const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
32
33
  const ERR_BAD_DATA: &str =
33
34
  "The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)";
35
+ const STRING_CACHE_ROOTS_CONST: &str = "__STRING_CACHE_ROOTS__";
36
+ const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
37
+ const STRING_CACHE_MAX: usize = 4096;
38
+ const STRING_CACHE_MIN_LEN: usize = 2;
39
+ const STRING_CACHE_MAX_LEN: usize = 64;
40
+ const PATH_CACHE_MAX_ENTRIES: usize = 64;
41
+
42
+ #[derive(Default)]
43
+ struct StringCacheEntry {
44
+ hash: u64,
45
+ value: String,
46
+ }
34
47
 
35
- thread_local! {
36
- static MAP_KEY_CACHE: RefCell<MapKeyCache> = RefCell::new(MapKeyCache::new());
48
+ struct StringCache {
49
+ entries: Box<[StringCacheEntry]>,
37
50
  }
38
51
 
39
- const MAP_KEY_CACHE_MAX: usize = 256;
40
- const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
52
+ impl StringCache {
53
+ fn new() -> Self {
54
+ let entries = (0..STRING_CACHE_MAX)
55
+ .map(|_| StringCacheEntry::default())
56
+ .collect::<Vec<_>>()
57
+ .into_boxed_slice();
58
+ Self { entries }
59
+ }
60
+ }
41
61
 
42
- struct MapKeyCache {
43
- key_to_index: FxHashMap<String, usize>,
62
+ thread_local! {
63
+ static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
64
+ static STRING_CACHE_ROOTS: OnceCell<RArray> = const { OnceCell::new() };
44
65
  }
45
66
 
46
- impl MapKeyCache {
47
- #[inline]
48
- fn new() -> Self {
49
- Self {
50
- key_to_index: FxHashMap::default(),
51
- }
67
+ #[inline]
68
+ fn string_cache_roots_owner(ruby: &magnus::Ruby) -> RArray {
69
+ let value = rust_module(ruby)
70
+ .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
71
+ .expect("string cache roots constant should exist");
72
+ RArray::from_value(value).expect("string cache roots constant should be an array")
73
+ }
74
+
75
+ #[inline]
76
+ fn init_thread_string_cache_roots(ruby: &magnus::Ruby) -> RArray {
77
+ let roots = ruby.ary_new_capa(STRING_CACHE_MAX);
78
+ for _ in 0..STRING_CACHE_MAX {
79
+ roots
80
+ .push(ruby.qnil().as_value())
81
+ .expect("string cache roots initialization should succeed");
52
82
  }
83
+ string_cache_roots_owner(ruby)
84
+ .push(roots.as_value())
85
+ .expect("string cache roots owner should retain per-thread roots");
86
+ roots
53
87
  }
54
88
 
55
89
  #[inline]
56
- fn map_key_roots_array(ruby: &magnus::Ruby) -> RArray {
57
- let rust = rust_module(ruby);
58
- let roots = rust
59
- .const_get::<_, Value>(MAP_KEY_ROOTS_CONST)
60
- .expect("map key roots constant should exist");
61
- RArray::from_value(roots).expect("map key roots constant should be an array")
90
+ fn string_cache_roots(ruby: &magnus::Ruby) -> RArray {
91
+ STRING_CACHE_ROOTS.with(|roots| *roots.get_or_init(|| init_thread_string_cache_roots(ruby)))
62
92
  }
63
93
 
64
94
  #[inline]
65
- fn cached_map_key(ruby: &magnus::Ruby, key: &str) -> Value {
66
- MAP_KEY_CACHE.with(|cache| {
67
- let mut cache = cache.borrow_mut();
68
- let roots = map_key_roots_array(ruby);
69
- if let Some(index) = cache.key_to_index.get(key) {
70
- return roots
71
- .entry::<Value>(*index as isize)
72
- .expect("cached map key index should be valid");
73
- }
95
+ fn cached_string(ruby: &magnus::Ruby, value: &str) -> Value {
96
+ if !(STRING_CACHE_MIN_LEN..=STRING_CACHE_MAX_LEN).contains(&value.len()) {
97
+ return ruby.str_new(value).into_value_with(ruby);
98
+ }
74
99
 
75
- let interned = ruby.str_new(key).to_interned_str();
76
- let value = interned.as_value();
77
- if cache.key_to_index.len() < MAP_KEY_CACHE_MAX {
78
- let index = roots.len();
79
- roots
80
- .push(value)
81
- .expect("map key roots array push should succeed");
82
- cache.key_to_index.insert(key.to_owned(), index);
100
+ let mut hasher = FxHasher::default();
101
+ value.hash(&mut hasher);
102
+ let hash = hasher.finish();
103
+ let slot = (hash as usize) & (STRING_CACHE_MAX - 1);
104
+
105
+ STRING_CACHE.with(|cache_cell| {
106
+ let mut cache = cache_cell.borrow_mut();
107
+ let entry = &mut cache.entries[slot];
108
+ if entry.hash == hash && entry.value == value {
109
+ return string_cache_roots(ruby)
110
+ .entry::<Value>(slot as isize)
111
+ .expect("string cache roots lookup should succeed");
83
112
  }
84
- value
113
+
114
+ let string = ruby.str_new(value);
115
+ string.freeze();
116
+ let cached = string.as_value();
117
+ string_cache_roots(ruby)
118
+ .store(slot as isize, cached)
119
+ .expect("string cache roots update should succeed");
120
+ entry.hash = hash;
121
+ entry.value.clear();
122
+ entry.value.push_str(value);
123
+ cached
85
124
  })
86
125
  }
87
126
 
@@ -214,18 +253,14 @@ impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
214
253
  where
215
254
  E: de::Error,
216
255
  {
217
- Ok(RubyDecodedValue::new(
218
- self.ruby.str_new(value).into_value_with(self.ruby),
219
- ))
256
+ Ok(RubyDecodedValue::new(cached_string(self.ruby, value)))
220
257
  }
221
258
 
222
259
  fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
223
260
  where
224
261
  E: de::Error,
225
262
  {
226
- Ok(RubyDecodedValue::new(
227
- self.ruby.str_new(&value).into_value_with(self.ruby),
228
- ))
263
+ Ok(RubyDecodedValue::new(cached_string(self.ruby, &value)))
229
264
  }
230
265
 
231
266
  fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
@@ -270,8 +305,8 @@ impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
270
305
  None => self.ruby.hash_new(),
271
306
  };
272
307
  while let Some(key) = map.next_key::<&'de str>()? {
273
- let key_val = cached_map_key(self.ruby, key);
274
308
  let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
309
+ let key_val = cached_string(self.ruby, key);
275
310
  hash.aset(key_val, value.into_value())
276
311
  .map_err(|e| de::Error::custom(e.to_string()))?;
277
312
  }
@@ -285,6 +320,42 @@ enum ReaderSource {
285
320
  Memory(MaxMindReader<Vec<u8>>),
286
321
  }
287
322
 
323
+ #[derive(Copy, Clone)]
324
+ enum OpenMode {
325
+ Mmap,
326
+ Memory,
327
+ Buffer,
328
+ }
329
+
330
+ impl OpenMode {
331
+ fn from_symbol(mode: Symbol, ruby: &magnus::Ruby) -> Result<Self, Error> {
332
+ let mode_name = mode.name()?;
333
+ match mode_name.as_ref() {
334
+ // MODE_FILE is the official gem's file-backed mode; use the
335
+ // existing mmap reader for the same path-backed behavior.
336
+ "MODE_AUTO" | "MODE_FILE" | "MODE_MMAP" => Ok(Self::Mmap),
337
+ "MODE_MEMORY" => Ok(Self::Memory),
338
+ "MODE_PARAM_IS_BUFFER" => Ok(Self::Buffer),
339
+ _ => Err(Error::new(
340
+ ruby.exception_arg_error(),
341
+ format!("Unsupported mode: {}", mode_name),
342
+ )),
343
+ }
344
+ }
345
+ }
346
+
347
+ #[derive(PartialEq, Eq)]
348
+ enum OwnedPathElement {
349
+ Key(String),
350
+ Index(usize),
351
+ IndexFromEnd(usize),
352
+ }
353
+
354
+ struct CachedPath {
355
+ hash: u64,
356
+ elements: Arc<[OwnedPathElement]>,
357
+ }
358
+
288
359
  impl ReaderSource {
289
360
  #[inline]
290
361
  fn lookup(
@@ -302,9 +373,30 @@ impl ReaderSource {
302
373
  &self,
303
374
  ip: IpAddr,
304
375
  ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
376
+ let (result, prefix_len) = match self {
377
+ ReaderSource::Mmap(reader) => {
378
+ let result = reader.lookup(ip)?;
379
+ let network = result.network()?;
380
+ (result.decode()?, prefix_len_for_ip_network(ip, network))
381
+ }
382
+ ReaderSource::Memory(reader) => {
383
+ let result = reader.lookup(ip)?;
384
+ let network = result.network()?;
385
+ (result.decode()?, prefix_len_for_ip_network(ip, network))
386
+ }
387
+ };
388
+ Ok((result, prefix_len))
389
+ }
390
+
391
+ #[inline]
392
+ fn lookup_path(
393
+ &self,
394
+ ip: IpAddr,
395
+ path_elements: &[PathElement<'_>],
396
+ ) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
305
397
  match self {
306
- ReaderSource::Mmap(reader) => lookup_prefix_for_reader(reader, ip),
307
- ReaderSource::Memory(reader) => lookup_prefix_for_reader(reader, ip),
398
+ ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode_path(path_elements),
399
+ ReaderSource::Memory(reader) => reader.lookup(ip)?.decode_path(path_elements),
308
400
  }
309
401
  }
310
402
 
@@ -317,34 +409,25 @@ impl ReaderSource {
317
409
  }
318
410
 
319
411
  #[inline]
320
- fn within(&self, network: IpNetwork) -> Result<ReaderWithin, MaxMindDbError> {
412
+ fn within(&self, network: IpNetwork) -> Result<ReaderWithin<'_>, MaxMindDbError> {
321
413
  match self {
322
- ReaderSource::Mmap(reader) => {
323
- let iter = reader.within(network, Default::default())?;
324
- // SAFETY: the iterator holds a reference into `reader`. We'll store an Arc guard
325
- // alongside it so the reader outlives the transmuted iterator.
326
- Ok(ReaderWithin::Mmap(unsafe {
327
- std::mem::transmute::<Within<'_, Mmap>, Within<'static, Mmap>>(iter)
328
- }))
329
- }
330
- ReaderSource::Memory(reader) => {
331
- let iter = reader.within(network, Default::default())?;
332
- // SAFETY: same as above, the Arc guard keeps the reader alive.
333
- Ok(ReaderWithin::Memory(unsafe {
334
- std::mem::transmute::<Within<'_, Vec<u8>>, Within<'static, Vec<u8>>>(iter)
335
- }))
336
- }
414
+ ReaderSource::Mmap(reader) => Ok(ReaderWithin::Mmap(
415
+ reader.within(network, Default::default())?,
416
+ )),
417
+ ReaderSource::Memory(reader) => Ok(ReaderWithin::Memory(
418
+ reader.within(network, Default::default())?,
419
+ )),
337
420
  }
338
421
  }
339
422
  }
340
423
 
341
424
  /// Wrapper enum for Within iterators
342
- enum ReaderWithin {
343
- Mmap(Within<'static, Mmap>),
344
- Memory(Within<'static, Vec<u8>>),
425
+ enum ReaderWithin<'reader> {
426
+ Mmap(Within<'reader, Mmap>),
427
+ Memory(Within<'reader, Vec<u8>>),
345
428
  }
346
429
 
347
- impl ReaderWithin {
430
+ impl ReaderWithin<'_> {
348
431
  fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
349
432
  match self {
350
433
  ReaderWithin::Mmap(iter) => next_within_result(iter),
@@ -353,17 +436,6 @@ impl ReaderWithin {
353
436
  }
354
437
  }
355
438
 
356
- #[inline]
357
- fn lookup_prefix_for_reader<S: AsRef<[u8]>>(
358
- reader: &MaxMindReader<S>,
359
- ip: IpAddr,
360
- ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
361
- let result = reader.lookup(ip)?;
362
- let network = result.network()?;
363
- let prefix_len = prefix_len_for_ip_network(ip, network);
364
- Ok((result.decode()?, prefix_len))
365
- }
366
-
367
439
  #[inline]
368
440
  // prefix_len_for_ip_network uses 0 as a sentinel for ip.is_ipv4() && network.is_ipv6().
369
441
  // In this case, 0 is not a real prefix length; it signals an IPv4-in-IPv6 mapping path,
@@ -378,7 +450,7 @@ fn prefix_len_for_ip_network(ip: IpAddr, network: IpNetwork) -> usize {
378
450
 
379
451
  #[inline]
380
452
  fn next_within_result<S: AsRef<[u8]>>(
381
- iter: &mut Within<'static, S>,
453
+ iter: &mut Within<'_, S>,
382
454
  ) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
383
455
  loop {
384
456
  match iter.next() {
@@ -472,6 +544,10 @@ impl Metadata {
472
544
  }
473
545
  }
474
546
 
547
+ // SAFETY: Metadata stores only owned Rust values copied out of the database
548
+ // metadata. It contains no Ruby VALUE handles or borrowed database/source data,
549
+ // so moving it between Ruby-managed threads cannot invalidate GC or lifetime
550
+ // assumptions.
475
551
  unsafe impl Send for Metadata {}
476
552
 
477
553
  /// A Ruby wrapper around the MaxMind DB reader
@@ -480,6 +556,7 @@ unsafe impl Send for Metadata {}
480
556
  struct Reader {
481
557
  reader: Arc<ArcSwapOption<ReaderSource>>,
482
558
  closed: Arc<AtomicBool>,
559
+ path_cache: Arc<Mutex<VecDeque<CachedPath>>>,
483
560
  ip_version: u16,
484
561
  }
485
562
 
@@ -487,7 +564,7 @@ impl Reader {
487
564
  fn new(args: &[Value]) -> Result<Self, Error> {
488
565
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
489
566
 
490
- let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
567
+ let args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
491
568
  let (database,) = args.required;
492
569
  let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
493
570
  let (mode,) = kw.optional;
@@ -495,29 +572,13 @@ impl Reader {
495
572
  // Parse mode from options hash
496
573
  let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
497
574
 
498
- let mode_str = mode.name()?;
499
- let mode_str: &str = &mode_str;
500
-
501
- // Determine actual mode to use
502
- let actual_mode = match mode_str {
503
- "MODE_AUTO" | "MODE_MMAP" => "MMAP",
504
- "MODE_MEMORY" => "MEMORY",
505
- _ => {
506
- return Err(Error::new(
507
- ruby.exception_arg_error(),
508
- format!("Unsupported mode: {}", mode_str),
509
- ))
510
- }
511
- };
575
+ let open_mode = OpenMode::from_symbol(mode, &ruby)?;
512
576
 
513
577
  // Open database with appropriate mode
514
- match actual_mode {
515
- "MMAP" => open_database_mmap(&database),
516
- "MEMORY" => open_database_memory(&database),
517
- _ => Err(Error::new(
518
- ruby.exception_arg_error(),
519
- format!("Invalid mode: {}", actual_mode),
520
- )),
578
+ match open_mode {
579
+ OpenMode::Mmap => open_database_mmap(&database_path(database)?),
580
+ OpenMode::Memory => open_database_memory(&database_path(database)?),
581
+ OpenMode::Buffer => open_database_buffer(database_buffer(database)?),
521
582
  }
522
583
  }
523
584
 
@@ -529,32 +590,28 @@ impl Reader {
529
590
  let reader_option = guard.as_ref();
530
591
  let reader = reader_option.as_ref().unwrap();
531
592
 
532
- // Parse IP address
533
- let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
593
+ let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
534
594
 
535
- if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
536
- return Err(Error::new(
537
- ruby.exception_arg_error(),
538
- ipv6_in_ipv4_error(&parsed_ip),
539
- ));
540
- }
595
+ lookup_result_to_value(&ruby, reader.lookup(parsed_ip), "Database lookup failed")
596
+ }
541
597
 
542
- // Perform lookup
543
- match reader.lookup(parsed_ip) {
544
- Ok(Some(data)) => Ok(data.into_value()),
545
- Ok(None) => Ok(ruby.qnil().as_value()),
546
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
547
- Err(Error::new(
548
- ExceptionClass::from_value(invalid_database_error().as_value())
549
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
550
- ERR_BAD_DATA,
551
- ))
552
- }
553
- Err(e) => Err(Error::new(
554
- ruby.exception_runtime_error(),
555
- format!("Database lookup failed: {}", e),
556
- )),
557
- }
598
+ #[inline]
599
+ fn get_path(&self, ip_address: Value, path: Value) -> Result<Value, Error> {
600
+ let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
601
+
602
+ let guard = self.get_reader(&ruby)?;
603
+ let reader_option = guard.as_ref();
604
+ let reader = reader_option.as_ref().unwrap();
605
+
606
+ let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
607
+ let owned_path = self.parse_path(path, &ruby)?;
608
+ let path_elements = path_elements_from_owned_path(owned_path.as_ref());
609
+
610
+ lookup_result_to_value(
611
+ &ruby,
612
+ reader.lookup_path(parsed_ip, &path_elements),
613
+ "Database lookup failed",
614
+ )
558
615
  }
559
616
 
560
617
  #[inline]
@@ -565,42 +622,65 @@ impl Reader {
565
622
  let reader_option = guard.as_ref();
566
623
  let reader = reader_option.as_ref().unwrap();
567
624
 
568
- // Parse IP address
569
- let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
570
-
571
- if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
572
- return Err(Error::new(
573
- ruby.exception_arg_error(),
574
- ipv6_in_ipv4_error(&parsed_ip),
575
- ));
576
- }
625
+ let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
577
626
 
578
627
  // Perform lookup with prefix
579
- match reader.lookup_prefix(parsed_ip) {
580
- Ok((Some(data), prefix)) => {
581
- let arr = ruby.ary_new();
582
- arr.push(data.into_value())?;
583
- arr.push(prefix.into_value_with(&ruby))?;
584
- Ok(arr)
585
- }
586
- Ok((None, prefix)) => {
587
- let arr = ruby.ary_new();
588
- arr.push(ruby.qnil().as_value())?;
589
- arr.push(prefix.into_value_with(&ruby))?;
590
- Ok(arr)
628
+ lookup_prefix_result_to_array(
629
+ &ruby,
630
+ reader.lookup_prefix(parsed_ip),
631
+ "Database lookup failed",
632
+ )
633
+ }
634
+
635
+ fn get_many(&self, ips: Value) -> Result<RArray, Error> {
636
+ let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
637
+
638
+ let guard = self.get_reader(&ruby)?;
639
+ let reader_option = guard.as_ref();
640
+ let reader = reader_option.as_ref().unwrap();
641
+
642
+ if let Ok(ip_array) = RArray::try_convert(ips) {
643
+ let results = ruby.ary_new_capa(ip_array.len());
644
+ for index in 0..ip_array.len() {
645
+ let ip = ip_array.entry::<Value>(index as isize)?;
646
+ results.push(self.lookup_ip_value(&ruby, reader, ip)?)?;
591
647
  }
592
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
593
- Err(Error::new(
594
- ExceptionClass::from_value(invalid_database_error().as_value())
595
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
596
- ERR_BAD_DATA,
597
- ))
648
+ return Ok(results);
649
+ }
650
+
651
+ ensure_enumerable(ips, &ruby, "ips must be an Array or Enumerable")?;
652
+ let results = ruby.ary_new();
653
+ for ip in ips.enumeratorize("each", ()) {
654
+ results.push(self.lookup_ip_value(&ruby, reader, ip?)?)?;
655
+ }
656
+ Ok(results)
657
+ }
658
+
659
+ fn get_many_path(&self, ips: Value, path: Value) -> Result<RArray, Error> {
660
+ let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
661
+
662
+ let guard = self.get_reader(&ruby)?;
663
+ let reader_option = guard.as_ref();
664
+ let reader = reader_option.as_ref().unwrap();
665
+
666
+ let owned_path = self.parse_path(path, &ruby)?;
667
+ let path_elements = path_elements_from_owned_path(owned_path.as_ref());
668
+
669
+ if let Ok(ip_array) = RArray::try_convert(ips) {
670
+ let results = ruby.ary_new_capa(ip_array.len());
671
+ for index in 0..ip_array.len() {
672
+ let ip = ip_array.entry::<Value>(index as isize)?;
673
+ results.push(self.lookup_ip_path_value(&ruby, reader, ip, &path_elements)?)?;
598
674
  }
599
- Err(e) => Err(Error::new(
600
- ruby.exception_runtime_error(),
601
- format!("Database lookup failed: {}", e),
602
- )),
675
+ return Ok(results);
603
676
  }
677
+
678
+ ensure_enumerable(ips, &ruby, "ips must be an Array or Enumerable")?;
679
+ let results = ruby.ary_new();
680
+ for ip in ips.enumeratorize("each", ()) {
681
+ results.push(self.lookup_ip_path_value(&ruby, reader, ip?, &path_elements)?)?;
682
+ }
683
+ Ok(results)
604
684
  }
605
685
 
606
686
  fn metadata(&self) -> Result<Metadata, Error> {
@@ -635,19 +715,25 @@ impl Reader {
635
715
  self.closed.load(Ordering::Acquire)
636
716
  }
637
717
 
638
- fn each(&self, args: &[Value]) -> Result<Value, Error> {
639
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
718
+ fn inspect(&self) -> String {
719
+ format!(
720
+ "#<MaxMind::DB::Rust::Reader:0x{:x} @closed={} @ip_version={}>",
721
+ self as *const Self as usize,
722
+ self.closed(),
723
+ self.ip_version,
724
+ )
725
+ }
640
726
 
641
- let guard = self.get_reader(&ruby)?;
727
+ fn each(ruby: &magnus::Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Value, Error> {
728
+ let reader_self = &*rb_self;
729
+
730
+ let guard = reader_self.get_reader(ruby)?;
642
731
  let reader_option = guard.as_ref();
643
732
  let reader = reader_option.as_ref().unwrap();
644
733
 
645
734
  // If no block given, return enumerator
646
735
  if !ruby.block_given() {
647
- return Err(Error::new(
648
- ruby.exception_runtime_error(),
649
- "Enumerator support not yet implemented, please provide a block",
650
- ));
736
+ return Ok(rb_self.enumeratorize("each", args).as_value());
651
737
  }
652
738
 
653
739
  let ip_version = reader.metadata().ip_version;
@@ -718,14 +804,9 @@ impl Reader {
718
804
  ));
719
805
  }
720
806
 
721
- let mut iter = reader.within(network).map_err(|e| {
722
- Error::new(
723
- ExceptionClass::from_value(invalid_database_error().as_value())
724
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
725
- format!("Failed to iterate: {}", e),
726
- )
727
- })?;
728
-
807
+ let mut iter = reader
808
+ .within(network)
809
+ .map_err(|e| invalid_database_exception(&format!("Failed to iterate: {}", e)))?;
729
810
  // Get IPAddr class
730
811
  let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
731
812
 
@@ -741,12 +822,10 @@ impl Reader {
741
822
  let values = (ipaddr, data.into_value());
742
823
  ruby.yield_values::<(Value, Value), Value>(values)?;
743
824
  }
744
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
745
- return Err(Error::new(
746
- ExceptionClass::from_value(invalid_database_error().as_value())
747
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
748
- ERR_BAD_DATA,
749
- ));
825
+ Err(MaxMindDbError::InvalidDatabase { .. })
826
+ | Err(MaxMindDbError::Decoding { .. })
827
+ | Err(MaxMindDbError::Io(_)) => {
828
+ return Err(invalid_database_exception(ERR_BAD_DATA));
750
829
  }
751
830
  Err(e) => {
752
831
  return Err(Error::new(
@@ -768,8 +847,114 @@ impl Reader {
768
847
  }
769
848
  Ok(guard)
770
849
  }
850
+
851
+ #[inline]
852
+ fn parse_lookup_ip(&self, ip_address: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
853
+ let parsed_ip = parse_ip_address_fast(ip_address, ruby)?;
854
+ self.validate_lookup_ip(parsed_ip, ruby)
855
+ }
856
+
857
+ #[inline]
858
+ fn validate_lookup_ip(&self, parsed_ip: IpAddr, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
859
+ if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
860
+ Err(Error::new(
861
+ ruby.exception_arg_error(),
862
+ ipv6_in_ipv4_error(&parsed_ip),
863
+ ))
864
+ } else {
865
+ Ok(parsed_ip)
866
+ }
867
+ }
868
+
869
+ #[inline]
870
+ fn lookup_ip_value(
871
+ &self,
872
+ ruby: &magnus::Ruby,
873
+ reader: &ReaderSource,
874
+ ip: Value,
875
+ ) -> Result<Value, Error> {
876
+ let parsed_ip = self.parse_lookup_ip(ip, ruby)?;
877
+ lookup_result_to_value(ruby, reader.lookup(parsed_ip), "Database lookup failed")
878
+ }
879
+
880
+ #[inline]
881
+ fn lookup_ip_path_value(
882
+ &self,
883
+ ruby: &magnus::Ruby,
884
+ reader: &ReaderSource,
885
+ ip: Value,
886
+ path_elements: &[PathElement<'_>],
887
+ ) -> Result<Value, Error> {
888
+ let parsed_ip = self.parse_lookup_ip(ip, ruby)?;
889
+ lookup_result_to_value(
890
+ ruby,
891
+ reader.lookup_path(parsed_ip, path_elements),
892
+ "Database lookup failed",
893
+ )
894
+ }
895
+
896
+ fn parse_path(
897
+ &self,
898
+ path: Value,
899
+ ruby: &magnus::Ruby,
900
+ ) -> Result<Arc<[OwnedPathElement]>, Error> {
901
+ let path = path_array(path, ruby)?;
902
+ let hash = path_cache_hash(path, ruby)?;
903
+
904
+ if let Some(cached) = self.cached_path(path, hash)? {
905
+ return Ok(cached);
906
+ }
907
+
908
+ let parsed_path: Arc<[OwnedPathElement]> = parse_path_array(path, ruby)?.into();
909
+ self.store_cached_path(hash, parsed_path.clone());
910
+ Ok(parsed_path)
911
+ }
912
+
913
+ fn cached_path(
914
+ &self,
915
+ path: RArray,
916
+ hash: u64,
917
+ ) -> Result<Option<Arc<[OwnedPathElement]>>, Error> {
918
+ let candidates = match self.path_cache.lock() {
919
+ Ok(cache) => cache
920
+ .iter()
921
+ .filter(|entry| entry.hash == hash && entry.elements.len() == path.len())
922
+ .map(|entry| entry.elements.clone())
923
+ .collect::<Vec<_>>(),
924
+ Err(_) => return Ok(None),
925
+ };
926
+
927
+ for candidate in candidates {
928
+ if path_matches_cached(path, candidate.as_ref())? {
929
+ return Ok(Some(candidate));
930
+ }
931
+ }
932
+
933
+ Ok(None)
934
+ }
935
+
936
+ fn store_cached_path(&self, hash: u64, elements: Arc<[OwnedPathElement]>) {
937
+ if let Ok(mut cache) = self.path_cache.lock() {
938
+ if cache
939
+ .iter()
940
+ .any(|entry| entry.hash == hash && entry.elements.as_ref() == elements.as_ref())
941
+ {
942
+ return;
943
+ }
944
+
945
+ cache.push_back(CachedPath { hash, elements });
946
+ while cache.len() > PATH_CACHE_MAX_ENTRIES {
947
+ cache.pop_front();
948
+ }
949
+ }
950
+ }
771
951
  }
772
952
 
953
+ // SAFETY: Reader does not store Ruby VALUE handles. The database source is
954
+ // owned by ReaderSource and is read-only after construction; close atomically
955
+ // swaps the shared source to None. The path cache contains only Rust-owned path
956
+ // elements behind a Mutex. All Ruby object access happens inside method calls
957
+ // while the Ruby VM is active.
773
958
  unsafe impl Send for Reader {}
774
959
 
775
960
  /// Helper function to create a Reader from a ReaderSource
@@ -779,10 +964,152 @@ fn create_reader(source: ReaderSource) -> Reader {
779
964
  Reader {
780
965
  reader: Arc::new(ArcSwapOption::from(Some(source))),
781
966
  closed: Arc::new(AtomicBool::new(false)),
967
+ path_cache: Arc::new(Mutex::new(VecDeque::with_capacity(PATH_CACHE_MAX_ENTRIES))),
782
968
  ip_version,
783
969
  }
784
970
  }
785
971
 
972
+ fn path_array(path: Value, ruby: &magnus::Ruby) -> Result<RArray, Error> {
973
+ RArray::try_convert(path).map_err(|_| {
974
+ Error::new(
975
+ ruby.exception_arg_error(),
976
+ "Path must be an Array of String and Integer elements",
977
+ )
978
+ })
979
+ }
980
+
981
+ fn parse_path_array(path: RArray, ruby: &magnus::Ruby) -> Result<Vec<OwnedPathElement>, Error> {
982
+ let mut elements = Vec::with_capacity(path.len());
983
+ for index in 0..path.len() {
984
+ let item = path.entry::<Value>(index as isize)?;
985
+ if let Ok(key) = RString::try_convert(item) {
986
+ elements.push(OwnedPathElement::Key(key.to_string()?));
987
+ continue;
988
+ }
989
+ if let Ok(index) = isize::try_convert(item) {
990
+ elements.push(signed_index_to_owned_path_element(index));
991
+ continue;
992
+ }
993
+ return Err(Error::new(
994
+ ruby.exception_arg_error(),
995
+ "Path elements must be Strings or Integers",
996
+ ));
997
+ }
998
+
999
+ Ok(elements)
1000
+ }
1001
+
1002
+ #[inline]
1003
+ fn signed_index_to_owned_path_element(index: isize) -> OwnedPathElement {
1004
+ if index >= 0 {
1005
+ OwnedPathElement::Index(index as usize)
1006
+ } else {
1007
+ let index_from_end = index
1008
+ .checked_neg()
1009
+ .and_then(|index| index.checked_sub(1))
1010
+ .map(|index| index as usize)
1011
+ .unwrap_or(usize::MAX);
1012
+ OwnedPathElement::IndexFromEnd(index_from_end)
1013
+ }
1014
+ }
1015
+
1016
+ fn path_cache_hash(path: RArray, ruby: &magnus::Ruby) -> Result<u64, Error> {
1017
+ let mut hasher = FxHasher::default();
1018
+ path.len().hash(&mut hasher);
1019
+
1020
+ for index in 0..path.len() {
1021
+ let item = path.entry::<Value>(index as isize)?;
1022
+ if let Ok(key) = RString::try_convert(item) {
1023
+ 0_u8.hash(&mut hasher);
1024
+ hash_path_key(key, &mut hasher)?;
1025
+ continue;
1026
+ }
1027
+ if let Ok(index) = isize::try_convert(item) {
1028
+ 1_u8.hash(&mut hasher);
1029
+ index.hash(&mut hasher);
1030
+ continue;
1031
+ }
1032
+ return Err(Error::new(
1033
+ ruby.exception_arg_error(),
1034
+ "Path elements must be Strings or Integers",
1035
+ ));
1036
+ }
1037
+
1038
+ Ok(hasher.finish())
1039
+ }
1040
+
1041
+ fn hash_path_key(key: RString, hasher: &mut FxHasher) -> Result<(), Error> {
1042
+ // SAFETY: the borrowed str is used only for immediate hashing and is not
1043
+ // stored across any call that could mutate or free the Ruby string.
1044
+ if let Some(key_str) = unsafe { key.test_as_str() } {
1045
+ key_str.hash(hasher);
1046
+ } else {
1047
+ key.to_string()?.hash(hasher);
1048
+ }
1049
+ Ok(())
1050
+ }
1051
+
1052
+ fn path_matches_cached(path: RArray, cached: &[OwnedPathElement]) -> Result<bool, Error> {
1053
+ if path.len() != cached.len() {
1054
+ return Ok(false);
1055
+ }
1056
+
1057
+ for (index, cached_element) in cached.iter().enumerate() {
1058
+ let item = path.entry::<Value>(index as isize)?;
1059
+ match cached_element {
1060
+ OwnedPathElement::Key(expected) => {
1061
+ let Ok(key) = RString::try_convert(item) else {
1062
+ return Ok(false);
1063
+ };
1064
+ if !path_key_matches(key, expected)? {
1065
+ return Ok(false);
1066
+ }
1067
+ }
1068
+ OwnedPathElement::Index(_) | OwnedPathElement::IndexFromEnd(_) => {
1069
+ let Ok(index) = isize::try_convert(item) else {
1070
+ return Ok(false);
1071
+ };
1072
+ if signed_index_to_owned_path_element(index) != *cached_element {
1073
+ return Ok(false);
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ Ok(true)
1080
+ }
1081
+
1082
+ fn path_key_matches(key: RString, expected: &str) -> Result<bool, Error> {
1083
+ // SAFETY: the borrowed str is used only for immediate comparison and is not
1084
+ // stored across any call that could mutate or free the Ruby string.
1085
+ if let Some(key_str) = unsafe { key.test_as_str() } {
1086
+ Ok(key_str == expected)
1087
+ } else {
1088
+ Ok(key.to_string()? == expected)
1089
+ }
1090
+ }
1091
+
1092
+ fn path_elements_from_owned_path(path: &[OwnedPathElement]) -> Vec<PathElement<'_>> {
1093
+ path.iter()
1094
+ .map(|element| match element {
1095
+ OwnedPathElement::Key(key) => PathElement::Key(key.as_str()),
1096
+ OwnedPathElement::Index(index) => PathElement::Index(*index),
1097
+ OwnedPathElement::IndexFromEnd(index) => PathElement::IndexFromEnd(*index),
1098
+ })
1099
+ .collect()
1100
+ }
1101
+
1102
+ fn ensure_enumerable(value: Value, ruby: &magnus::Ruby, error_message: &str) -> Result<(), Error> {
1103
+ if value.respond_to("each", false)? {
1104
+ Ok(())
1105
+ } else {
1106
+ Err(Error::new(
1107
+ ruby.exception_arg_error(),
1108
+ error_message.to_owned(),
1109
+ ))
1110
+ }
1111
+ }
1112
+
786
1113
  /// Parse IP address from Ruby value (String or IPAddr) - optimized version
787
1114
  #[inline(always)]
788
1115
  fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
@@ -797,12 +1124,7 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
797
1124
  )
798
1125
  })?;
799
1126
 
800
- return IpAddr::from_str(ip_str).map_err(|_| {
801
- Error::new(
802
- ruby.exception_arg_error(),
803
- format!("'{}' does not appear to be an IPv4 or IPv6 address", ip_str),
804
- )
805
- });
1127
+ return parse_ip_string(ip_str, ruby);
806
1128
  }
807
1129
 
808
1130
  // Slow path: Try as IPAddr object
@@ -833,15 +1155,7 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
833
1155
  }
834
1156
 
835
1157
  if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
836
- return IpAddr::from_str(&ipaddr_obj).map_err(|_| {
837
- Error::new(
838
- ruby.exception_arg_error(),
839
- format!(
840
- "'{}' does not appear to be an IPv4 or IPv6 address",
841
- ipaddr_obj
842
- ),
843
- )
844
- });
1158
+ return parse_ip_string(&ipaddr_obj, ruby);
845
1159
  }
846
1160
 
847
1161
  Err(Error::new(
@@ -850,14 +1164,126 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
850
1164
  ))
851
1165
  }
852
1166
 
1167
+ #[inline(always)]
1168
+ fn parse_ip_string(s: &str, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
1169
+ if let Some(ip) = parse_ipv4_string(s.as_bytes()) {
1170
+ return Ok(IpAddr::V4(ip));
1171
+ }
1172
+
1173
+ IpAddr::from_str(s).map_err(|_| {
1174
+ Error::new(
1175
+ ruby.exception_arg_error(),
1176
+ format!("'{}' does not appear to be an IPv4 or IPv6 address", s),
1177
+ )
1178
+ })
1179
+ }
1180
+
1181
+ #[inline(always)]
1182
+ fn parse_ipv4_string(bytes: &[u8]) -> Option<Ipv4Addr> {
1183
+ let mut octets = [0u8; 4];
1184
+ let mut octet_index = 0;
1185
+ let mut value: u16 = 0;
1186
+ let mut digits = 0;
1187
+
1188
+ for &byte in bytes {
1189
+ if byte == b'.' {
1190
+ if digits == 0 || octet_index == 3 {
1191
+ return None;
1192
+ }
1193
+ octets[octet_index] = value as u8;
1194
+ octet_index += 1;
1195
+ value = 0;
1196
+ digits = 0;
1197
+ continue;
1198
+ }
1199
+
1200
+ if !byte.is_ascii_digit() {
1201
+ return None;
1202
+ }
1203
+ if digits == 1 && value == 0 {
1204
+ return None;
1205
+ }
1206
+
1207
+ digits += 1;
1208
+ if digits > 3 {
1209
+ return None;
1210
+ }
1211
+ value = value * 10 + u16::from(byte - b'0');
1212
+ if value > u16::from(u8::MAX) {
1213
+ return None;
1214
+ }
1215
+ }
1216
+
1217
+ if octet_index != 3 || digits == 0 {
1218
+ return None;
1219
+ }
1220
+ octets[octet_index] = value as u8;
1221
+
1222
+ Some(Ipv4Addr::from(octets))
1223
+ }
1224
+
1225
+ #[inline]
1226
+ fn lookup_result_to_value(
1227
+ ruby: &magnus::Ruby,
1228
+ result: Result<Option<RubyDecodedValue>, MaxMindDbError>,
1229
+ error_context: &str,
1230
+ ) -> Result<Value, Error> {
1231
+ match result {
1232
+ Ok(Some(data)) => Ok(data.into_value()),
1233
+ Ok(None) => Ok(ruby.qnil().as_value()),
1234
+ Err(err) => Err(lookup_error(ruby, err, error_context)),
1235
+ }
1236
+ }
1237
+
1238
+ #[inline]
1239
+ fn lookup_prefix_result_to_array(
1240
+ ruby: &magnus::Ruby,
1241
+ result: Result<(Option<RubyDecodedValue>, usize), MaxMindDbError>,
1242
+ error_context: &str,
1243
+ ) -> Result<RArray, Error> {
1244
+ match result {
1245
+ Ok((data, prefix)) => {
1246
+ let arr = ruby.ary_new();
1247
+ arr.push(data.map_or_else(|| ruby.qnil().as_value(), RubyDecodedValue::into_value))?;
1248
+ arr.push(prefix.into_value_with(ruby))?;
1249
+ Ok(arr)
1250
+ }
1251
+ Err(err) => Err(lookup_error(ruby, err, error_context)),
1252
+ }
1253
+ }
1254
+
1255
+ #[inline]
1256
+ fn lookup_error(ruby: &magnus::Ruby, err: MaxMindDbError, context: &str) -> Error {
1257
+ match err {
1258
+ MaxMindDbError::InvalidDatabase { .. }
1259
+ | MaxMindDbError::Decoding { .. }
1260
+ | MaxMindDbError::Io(_) => invalid_database_exception(ERR_BAD_DATA),
1261
+ other => Error::new(
1262
+ ruby.exception_runtime_error(),
1263
+ format!("{}: {}", context, other),
1264
+ ),
1265
+ }
1266
+ }
1267
+
853
1268
  /// Generate error message for IPv6 in IPv4-only database
854
1269
  fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
855
1270
  format!(
856
- "Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database",
1271
+ "Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database.",
857
1272
  ip
858
1273
  )
859
1274
  }
860
1275
 
1276
+ fn database_path(database: Value) -> Result<String, Error> {
1277
+ RString::try_convert(database)?.to_string()
1278
+ }
1279
+
1280
+ fn database_buffer(database: Value) -> Result<Vec<u8>, Error> {
1281
+ let string = RString::try_convert(database)?;
1282
+ // SAFETY: the slice is copied into an owned Vec before Ruby can mutate or
1283
+ // free the string, and the reader only ever sees the owned bytes.
1284
+ Ok(unsafe { string.as_slice() }.to_vec())
1285
+ }
1286
+
861
1287
  /// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
862
1288
  fn open_database_mmap(path: &str) -> Result<Reader, Error> {
863
1289
  let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
@@ -869,16 +1295,11 @@ fn open_database_mmap(path: &str) -> Result<Reader, Error> {
869
1295
  format!("Failed to memory-map database file: {}", e),
870
1296
  )
871
1297
  })?;
872
-
873
1298
  let reader = MaxMindReader::from_source(mmap).map_err(|_| {
874
- Error::new(
875
- ExceptionClass::from_value(invalid_database_error().as_value())
876
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
877
- format!(
878
- "Error opening database file ({}). Is this a valid MaxMind DB file?",
879
- path
880
- ),
881
- )
1299
+ invalid_database_exception(&format!(
1300
+ "Error opening database file ({}). Is this a valid MaxMind DB file?",
1301
+ path
1302
+ ))
882
1303
  })?;
883
1304
 
884
1305
  Ok(create_reader(ReaderSource::Mmap(reader)))
@@ -897,16 +1318,25 @@ fn open_database_memory(path: &str) -> Result<Reader, Error> {
897
1318
  )
898
1319
  })?;
899
1320
 
900
- let reader = MaxMindReader::from_source(buffer).map_err(|_| {
901
- Error::new(
902
- ExceptionClass::from_value(invalid_database_error().as_value())
903
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
904
- format!(
905
- "Error opening database file ({}). Is this a valid MaxMind DB file?",
906
- path
907
- ),
908
- )
909
- })?;
1321
+ reader_from_buffer(
1322
+ buffer,
1323
+ format!(
1324
+ "Error opening database file ({}). Is this a valid MaxMind DB file?",
1325
+ path
1326
+ ),
1327
+ )
1328
+ }
1329
+
1330
+ fn open_database_buffer(buffer: Vec<u8>) -> Result<Reader, Error> {
1331
+ reader_from_buffer(
1332
+ buffer,
1333
+ "Error opening database from buffer. Is this a valid MaxMind DB file?".to_owned(),
1334
+ )
1335
+ }
1336
+
1337
+ fn reader_from_buffer(buffer: Vec<u8>, invalid_message: String) -> Result<Reader, Error> {
1338
+ let reader = MaxMindReader::from_source(buffer)
1339
+ .map_err(|_| invalid_database_exception(invalid_message.as_str()))?;
910
1340
 
911
1341
  Ok(create_reader(ReaderSource::Memory(reader)))
912
1342
  }
@@ -944,6 +1374,14 @@ fn invalid_database_error() -> RClass {
944
1374
  .expect("InvalidDatabaseError class should exist")
945
1375
  }
946
1376
 
1377
+ fn invalid_database_exception(message: &str) -> Error {
1378
+ Error::new(
1379
+ ExceptionClass::from_value(invalid_database_error().as_value())
1380
+ .expect("InvalidDatabaseError should convert to ExceptionClass"),
1381
+ message.to_owned(),
1382
+ )
1383
+ }
1384
+
947
1385
  fn rust_module(ruby: &magnus::Ruby) -> RModule {
948
1386
  let maxmind = ruby
949
1387
  .class_object()
@@ -1004,8 +1442,15 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
1004
1442
  }
1005
1443
  };
1006
1444
 
1007
- if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_err() {
1008
- rust.const_set(MAP_KEY_ROOTS_CONST, ruby.ary_new_capa(MAP_KEY_CACHE_MAX))?;
1445
+ if rust
1446
+ .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
1447
+ .is_err()
1448
+ {
1449
+ rust.const_set(STRING_CACHE_ROOTS_CONST, ruby.ary_new())?;
1450
+ }
1451
+
1452
+ if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_ok() {
1453
+ let _ = rust.funcall::<_, _, Value>("send", ("remove_const", MAP_KEY_ROOTS_CONST))?;
1009
1454
  }
1010
1455
 
1011
1456
  // The extension can be loaded more than once from different paths.
@@ -1021,13 +1466,17 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
1021
1466
  let reader_class = rust.define_class("Reader", ruby.class_object())?;
1022
1467
  reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
1023
1468
  reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
1469
+ reader_class.define_method("get_path", magnus::method!(Reader::get_path, 2))?;
1024
1470
  reader_class.define_method(
1025
1471
  "get_with_prefix_length",
1026
1472
  magnus::method!(Reader::get_with_prefix_length, 1),
1027
1473
  )?;
1474
+ reader_class.define_method("get_many", magnus::method!(Reader::get_many, 1))?;
1475
+ reader_class.define_method("get_many_path", magnus::method!(Reader::get_many_path, 2))?;
1028
1476
  reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
1029
1477
  reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
1030
1478
  reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
1479
+ reader_class.define_method("inspect", magnus::method!(Reader::inspect, 0))?;
1031
1480
  reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
1032
1481
 
1033
1482
  // Include Enumerable module
@@ -1062,8 +1511,49 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
1062
1511
 
1063
1512
  // Define MODE constants
1064
1513
  rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
1514
+ rust.const_set("MODE_FILE", ruby.to_symbol("MODE_FILE"))?;
1065
1515
  rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
1066
1516
  rust.const_set("MODE_MMAP", ruby.to_symbol("MODE_MMAP"))?;
1517
+ rust.const_set(
1518
+ "MODE_PARAM_IS_BUFFER",
1519
+ ruby.to_symbol("MODE_PARAM_IS_BUFFER"),
1520
+ )?;
1067
1521
 
1068
1522
  Ok(())
1069
1523
  }
1524
+
1525
+ #[cfg(test)]
1526
+ mod tests {
1527
+ use super::parse_ipv4_string;
1528
+ use std::net::Ipv4Addr;
1529
+
1530
+ #[test]
1531
+ fn parses_strict_ipv4_strings() {
1532
+ assert_eq!(
1533
+ parse_ipv4_string(b"0.1.2.255"),
1534
+ Some(Ipv4Addr::new(0, 1, 2, 255))
1535
+ );
1536
+ assert_eq!(
1537
+ parse_ipv4_string(b"192.0.2.1"),
1538
+ Some(Ipv4Addr::new(192, 0, 2, 1))
1539
+ );
1540
+ }
1541
+
1542
+ #[test]
1543
+ fn rejects_ipv4_strings_that_std_parser_rejects() {
1544
+ for value in [
1545
+ b"01.2.3.4".as_slice(),
1546
+ b"1.02.3.4".as_slice(),
1547
+ b"1.2.3.04".as_slice(),
1548
+ b"1.2.3".as_slice(),
1549
+ b"1.2.3.4.5".as_slice(),
1550
+ b"1..2.3".as_slice(),
1551
+ b"256.1.1.1".as_slice(),
1552
+ b"1.2.3.4 ".as_slice(),
1553
+ b" 1.2.3.4".as_slice(),
1554
+ b"2001:db8::1".as_slice(),
1555
+ ] {
1556
+ assert_eq!(parse_ipv4_string(value), None);
1557
+ }
1558
+ }
1559
+ }