maxmind-db-rust 0.5.0 → 0.6.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.
@@ -1,1388 +1,17 @@
1
- // SAFETY: the `maxminddb` crate is built with the `unsafe-str-decode` feature enabled.
2
- // Ruby validates UTF-8 when we construct `RString`s, so skipping the redundant check in
3
- // the decoder is safe and avoids re-validating every string record twice.
4
- use ::maxminddb as maxminddb_crate;
5
- use arc_swap::{ArcSwapOption, Guard};
6
- use ipnetwork::IpNetwork;
7
- use magnus::{
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
- };
11
- use maxminddb_crate::{MaxMindDbError, PathElement, Reader as MaxMindReader, Within};
12
- use memmap2::Mmap;
13
- use rustc_hash::FxHasher;
14
- use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
15
- use std::{
16
- cell::{OnceCell, RefCell},
17
- collections::{BTreeMap, VecDeque},
18
- fmt,
19
- fs::File,
20
- hash::{Hash, Hasher},
21
- io::Read as IoRead,
22
- net::{IpAddr, Ipv4Addr},
23
- path::Path,
24
- str::FromStr,
25
- sync::{
26
- atomic::{AtomicBool, Ordering},
27
- Arc, Mutex,
28
- },
29
- };
1
+ mod decoder;
2
+ mod initialization;
3
+ mod metadata;
4
+ mod path;
5
+ mod reader;
30
6
 
31
- // Error constants
32
- const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
33
- const ERR_BAD_DATA: &str =
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;
7
+ use magnus::{error::Error, prelude::*, RModule, Value};
41
8
 
42
- #[derive(Default)]
43
- struct StringCacheEntry {
44
- hash: u64,
45
- value: String,
46
- }
47
-
48
- struct StringCache {
49
- entries: Box<[StringCacheEntry]>,
50
- }
51
-
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
- }
61
-
62
- thread_local! {
63
- static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
64
- static STRING_CACHE_ROOTS: OnceCell<RArray> = const { OnceCell::new() };
65
- }
66
-
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");
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
87
- }
88
-
89
- #[inline]
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)))
92
- }
93
-
94
- #[inline]
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
- }
99
-
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");
112
- }
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
124
- })
125
- }
126
-
127
- /// Wrapper that owns the Ruby value produced by deserializing a MaxMind record
128
- #[derive(Clone)]
129
- struct RubyDecodedValue {
130
- value: Value,
131
- }
132
-
133
- impl RubyDecodedValue {
134
- #[inline]
135
- fn new(value: Value) -> Self {
136
- Self { value }
137
- }
138
-
139
- #[inline]
140
- fn into_value(self) -> Value {
141
- self.value
142
- }
143
- }
144
-
145
- impl<'de> Deserialize<'de> for RubyDecodedValue {
146
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
147
- where
148
- D: Deserializer<'de>,
149
- {
150
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in deserializer");
151
- RubyValueSeed { ruby: &ruby }.deserialize(deserializer)
152
- }
153
- }
154
-
155
- struct RubyValueSeed<'ruby> {
156
- ruby: &'ruby magnus::Ruby,
157
- }
158
-
159
- impl<'ruby, 'de> DeserializeSeed<'de> for RubyValueSeed<'ruby> {
160
- type Value = RubyDecodedValue;
161
-
162
- fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
163
- where
164
- D: Deserializer<'de>,
165
- {
166
- deserializer.deserialize_any(RubyValueVisitor { ruby: self.ruby })
167
- }
168
- }
169
-
170
- struct RubyValueVisitor<'ruby> {
171
- ruby: &'ruby magnus::Ruby,
172
- }
173
-
174
- impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
175
- type Value = RubyDecodedValue;
176
-
177
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
178
- formatter.write_str("any valid MaxMind DB value")
179
- }
180
-
181
- fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
182
- where
183
- E: de::Error,
184
- {
185
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
186
- }
187
-
188
- fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
189
- where
190
- E: de::Error,
191
- {
192
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
193
- }
194
-
195
- fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
196
- where
197
- E: de::Error,
198
- {
199
- if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
200
- Ok(RubyDecodedValue::new(
201
- (value as i32).into_value_with(self.ruby),
202
- ))
203
- } else {
204
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
205
- }
206
- }
207
-
208
- fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
209
- where
210
- E: de::Error,
211
- {
212
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
213
- }
214
-
215
- fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
216
- where
217
- E: de::Error,
218
- {
219
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
220
- }
221
-
222
- fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
223
- where
224
- E: de::Error,
225
- {
226
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
227
- }
228
-
229
- fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
230
- where
231
- E: de::Error,
232
- {
233
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
234
- }
235
-
236
- fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
237
- where
238
- E: de::Error,
239
- {
240
- Ok(RubyDecodedValue::new(
241
- (value as f64).into_value_with(self.ruby),
242
- ))
243
- }
244
-
245
- fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
246
- where
247
- E: de::Error,
248
- {
249
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
250
- }
251
-
252
- fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
253
- where
254
- E: de::Error,
255
- {
256
- Ok(RubyDecodedValue::new(cached_string(self.ruby, value)))
257
- }
258
-
259
- fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
260
- where
261
- E: de::Error,
262
- {
263
- Ok(RubyDecodedValue::new(cached_string(self.ruby, &value)))
264
- }
265
-
266
- fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
267
- where
268
- E: de::Error,
269
- {
270
- Ok(RubyDecodedValue::new(
271
- self.ruby.str_from_slice(value).into_value_with(self.ruby),
272
- ))
273
- }
274
-
275
- fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
276
- where
277
- E: de::Error,
278
- {
279
- Ok(RubyDecodedValue::new(
280
- self.ruby.str_from_slice(&value).into_value_with(self.ruby),
281
- ))
282
- }
283
-
284
- fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
285
- where
286
- A: SeqAccess<'de>,
287
- {
288
- let arr = match seq.size_hint() {
289
- Some(cap) => self.ruby.ary_new_capa(cap),
290
- None => self.ruby.ary_new(),
291
- };
292
- while let Some(elem) = seq.next_element_seed(RubyValueSeed { ruby: self.ruby })? {
293
- arr.push(elem.into_value())
294
- .map_err(|e| de::Error::custom(e.to_string()))?;
295
- }
296
- Ok(RubyDecodedValue::new(arr.into_value_with(self.ruby)))
297
- }
298
-
299
- fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
300
- where
301
- A: MapAccess<'de>,
302
- {
303
- let hash = match map.size_hint() {
304
- Some(cap) => self.ruby.hash_new_capa(cap),
305
- None => self.ruby.hash_new(),
306
- };
307
- while let Some(key) = map.next_key::<&'de str>()? {
308
- let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
309
- let key_val = cached_string(self.ruby, key);
310
- hash.aset(key_val, value.into_value())
311
- .map_err(|e| de::Error::custom(e.to_string()))?;
312
- }
313
- Ok(RubyDecodedValue::new(hash.into_value_with(self.ruby)))
314
- }
315
- }
316
-
317
- /// Enum to handle different reader source types
318
- enum ReaderSource {
319
- Mmap(MaxMindReader<Mmap>),
320
- Memory(MaxMindReader<Vec<u8>>),
321
- }
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
-
359
- impl ReaderSource {
360
- #[inline]
361
- fn lookup(
362
- &self,
363
- ip: IpAddr,
364
- ) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
365
- match self {
366
- ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode(),
367
- ReaderSource::Memory(reader) => reader.lookup(ip)?.decode(),
368
- }
369
- }
370
-
371
- #[inline]
372
- fn lookup_prefix(
373
- &self,
374
- ip: IpAddr,
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> {
397
- match self {
398
- ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode_path(path_elements),
399
- ReaderSource::Memory(reader) => reader.lookup(ip)?.decode_path(path_elements),
400
- }
401
- }
402
-
403
- #[inline]
404
- fn metadata(&self) -> &maxminddb_crate::Metadata {
405
- match self {
406
- ReaderSource::Mmap(reader) => &reader.metadata,
407
- ReaderSource::Memory(reader) => &reader.metadata,
408
- }
409
- }
410
-
411
- #[inline]
412
- fn within(&self, network: IpNetwork) -> Result<ReaderWithin<'_>, MaxMindDbError> {
413
- match self {
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
- )),
420
- }
421
- }
422
- }
423
-
424
- /// Wrapper enum for Within iterators
425
- enum ReaderWithin<'reader> {
426
- Mmap(Within<'reader, Mmap>),
427
- Memory(Within<'reader, Vec<u8>>),
428
- }
429
-
430
- impl ReaderWithin<'_> {
431
- fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
432
- match self {
433
- ReaderWithin::Mmap(iter) => next_within_result(iter),
434
- ReaderWithin::Memory(iter) => next_within_result(iter),
435
- }
436
- }
437
- }
438
-
439
- #[inline]
440
- // prefix_len_for_ip_network uses 0 as a sentinel for ip.is_ipv4() && network.is_ipv6().
441
- // In this case, 0 is not a real prefix length; it signals an IPv4-in-IPv6 mapping path,
442
- // and callers must treat it specially (distinct from "no network found").
443
- fn prefix_len_for_ip_network(ip: IpAddr, network: IpNetwork) -> usize {
444
- if ip.is_ipv4() && network.is_ipv6() {
445
- 0
446
- } else {
447
- network.prefix() as usize
448
- }
449
- }
450
-
451
- #[inline]
452
- fn next_within_result<S: AsRef<[u8]>>(
453
- iter: &mut Within<'_, S>,
454
- ) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
455
- loop {
456
- match iter.next() {
457
- None => return None,
458
- Some(Err(e)) => return Some(Err(e)),
459
- Some(Ok(lookup_result)) => {
460
- let network = match lookup_result.network() {
461
- Ok(n) => n,
462
- Err(e) => return Some(Err(e)),
463
- };
464
- match lookup_result.decode::<RubyDecodedValue>() {
465
- Ok(Some(data)) => return Some(Ok((network, data))),
466
- Ok(None) => continue, // Skip networks without data
467
- Err(e) => return Some(Err(e)),
468
- }
469
- }
470
- }
471
- }
472
- }
473
-
474
- /// Metadata about the MaxMind DB database
475
- #[derive(Clone)]
476
- #[magnus::wrap(class = "MaxMind::DB::Rust::Metadata")]
477
- struct Metadata {
478
- /// The major version number of the binary format used when creating the database.
479
- binary_format_major_version: u16,
480
- /// The minor version number of the binary format used when creating the database.
481
- binary_format_minor_version: u16,
482
- /// The Unix epoch timestamp for when the database was built.
483
- build_epoch: u64,
484
- /// A string identifying the database type (e.g., 'GeoIP2-City', 'GeoLite2-Country').
485
- database_type: String,
486
- description_map: BTreeMap<String, String>,
487
- /// The IP version of the data in a database. A value of 4 means IPv4 only; 6 supports both IPv4 and IPv6.
488
- ip_version: u16,
489
- languages_list: Vec<String>,
490
- /// The number of nodes in the search tree.
491
- node_count: u32,
492
- /// The record size in bits (24, 28, or 32).
493
- record_size: u16,
494
- }
495
-
496
- impl Metadata {
497
- fn binary_format_major_version(&self) -> u16 {
498
- self.binary_format_major_version
499
- }
500
-
501
- fn binary_format_minor_version(&self) -> u16 {
502
- self.binary_format_minor_version
503
- }
504
-
505
- fn build_epoch(&self) -> u64 {
506
- self.build_epoch
507
- }
508
-
509
- fn database_type(&self) -> String {
510
- self.database_type.clone()
511
- }
512
-
513
- fn description(&self) -> RHash {
514
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
515
- let hash = ruby.hash_new();
516
- for (k, v) in &self.description_map {
517
- let _ = hash.aset(k.as_str(), v.as_str());
518
- }
519
- hash
520
- }
521
-
522
- fn ip_version(&self) -> u16 {
523
- self.ip_version
524
- }
525
-
526
- fn languages(&self) -> Vec<String> {
527
- self.languages_list.clone()
528
- }
529
-
530
- fn node_count(&self) -> u32 {
531
- self.node_count
532
- }
533
-
534
- fn record_size(&self) -> u16 {
535
- self.record_size
536
- }
537
-
538
- fn node_byte_size(&self) -> u16 {
539
- self.record_size / 4
540
- }
541
-
542
- fn search_tree_size(&self) -> u32 {
543
- self.node_count * (self.record_size as u32 / 4)
544
- }
545
- }
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.
551
- unsafe impl Send for Metadata {}
552
-
553
- /// A Ruby wrapper around the MaxMind DB reader
554
- #[derive(Clone)]
555
- #[magnus::wrap(class = "MaxMind::DB::Rust::Reader")]
556
- struct Reader {
557
- reader: Arc<ArcSwapOption<ReaderSource>>,
558
- closed: Arc<AtomicBool>,
559
- path_cache: Arc<Mutex<VecDeque<CachedPath>>>,
560
- ip_version: u16,
561
- }
562
-
563
- impl Reader {
564
- fn new(args: &[Value]) -> Result<Self, Error> {
565
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
566
-
567
- let args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
568
- let (database,) = args.required;
569
- let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
570
- let (mode,) = kw.optional;
571
-
572
- // Parse mode from options hash
573
- let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
574
-
575
- let open_mode = OpenMode::from_symbol(mode, &ruby)?;
576
-
577
- // Open database with appropriate mode
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)?),
582
- }
583
- }
584
-
585
- #[inline]
586
- fn get(&self, ip_address: Value) -> Result<Value, Error> {
587
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
588
-
589
- let guard = self.get_reader(&ruby)?;
590
- let reader_option = guard.as_ref();
591
- let reader = reader_option.as_ref().unwrap();
592
-
593
- let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
594
-
595
- lookup_result_to_value(&ruby, reader.lookup(parsed_ip), "Database lookup failed")
596
- }
597
-
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
- )
615
- }
616
-
617
- #[inline]
618
- fn get_with_prefix_length(&self, ip_address: Value) -> Result<RArray, Error> {
619
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
620
-
621
- let guard = self.get_reader(&ruby)?;
622
- let reader_option = guard.as_ref();
623
- let reader = reader_option.as_ref().unwrap();
624
-
625
- let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
626
-
627
- // Perform lookup with prefix
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)?)?;
647
- }
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)?)?;
674
- }
675
- return Ok(results);
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)
684
- }
685
-
686
- fn metadata(&self) -> Result<Metadata, Error> {
687
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
688
-
689
- let guard = self.get_reader(&ruby)?;
690
- let reader_option = guard.as_ref();
691
- let reader = reader_option.as_ref().unwrap();
692
- let meta = reader.metadata();
693
-
694
- Ok(Metadata {
695
- binary_format_major_version: meta.binary_format_major_version,
696
- binary_format_minor_version: meta.binary_format_minor_version,
697
- build_epoch: meta.build_epoch,
698
- database_type: meta.database_type.clone(),
699
- description_map: meta.description.clone(),
700
- ip_version: meta.ip_version,
701
- languages_list: meta.languages.clone(),
702
- node_count: meta.node_count,
703
- record_size: meta.record_size,
704
- })
705
- }
706
-
707
- fn close(&self) {
708
- if self.closed.swap(true, Ordering::AcqRel) {
709
- return;
710
- }
711
- self.reader.store(None);
712
- }
9
+ pub(crate) const STRING_CACHE_ROOTS_CONST: &str = "__STRING_CACHE_ROOTS__";
10
+ pub(crate) const STRING_CACHE_MAX: usize = 4096;
11
+ pub(crate) const STRING_CACHE_MIN_LEN: usize = 2;
12
+ pub(crate) const STRING_CACHE_MAX_LEN: usize = 64;
713
13
 
714
- fn closed(&self) -> bool {
715
- self.closed.load(Ordering::Acquire)
716
- }
717
-
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
- }
726
-
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)?;
731
- let reader_option = guard.as_ref();
732
- let reader = reader_option.as_ref().unwrap();
733
-
734
- // If no block given, return enumerator
735
- if !ruby.block_given() {
736
- return Ok(rb_self.enumeratorize("each", args).as_value());
737
- }
738
-
739
- let ip_version = reader.metadata().ip_version;
740
-
741
- // Determine the network to iterate over
742
- let network_str = if args.is_empty() {
743
- // No argument: use default (full database)
744
- if ip_version == 4 {
745
- "0.0.0.0/0".to_string()
746
- } else {
747
- "::/0".to_string()
748
- }
749
- } else {
750
- // Argument provided: extract network CIDR string
751
- let network_arg = args[0];
752
-
753
- // Try to get string representation
754
- // Accept both String and IPAddr objects
755
- let network_str_val = if let Ok(s) = RString::try_convert(network_arg) {
756
- // It's already a string
757
- s.to_string()?
758
- } else {
759
- // Check if it's an IPAddr object
760
- let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
761
- if network_arg.is_kind_of(ipaddr_class) {
762
- // It's an IPAddr - need to get both address and prefix
763
- let ip_str: String = network_arg.funcall("to_s", ())?;
764
-
765
- // Get the prefix length from IPAddr
766
- // IPAddr stores prefix as a netmask, need to convert
767
- let prefix_len: u8 = network_arg.funcall("prefix", ())?;
768
-
769
- // Construct CIDR notation
770
- format!("{}/{}", ip_str, prefix_len)
771
- } else {
772
- // Try to call to_s on it (works for other objects)
773
- let to_s_result: Value = network_arg.funcall("to_s", ())?;
774
- RString::try_convert(to_s_result)
775
- .map_err(|_| {
776
- Error::new(
777
- ruby.exception_arg_error(),
778
- "Network parameter must be a String or IPAddr",
779
- )
780
- })?
781
- .to_string()?
782
- }
783
- };
784
-
785
- network_str_val
786
- };
787
-
788
- let network = IpNetwork::from_str(&network_str).map_err(|e| {
789
- Error::new(
790
- ruby.exception_arg_error(),
791
- format!("Invalid network CIDR '{}': {}", network_str, e),
792
- )
793
- })?;
794
-
795
- // Validate network matches database IP version
796
- // IPv4 in IPv6 DB is OK (IPv4-mapped), IPv6 in IPv6 DB is OK
797
- if let (4, IpNetwork::V6(_)) = (ip_version, network) {
798
- return Err(Error::new(
799
- ruby.exception_arg_error(),
800
- format!(
801
- "Cannot search for IPv6 network '{}' in an IPv4-only database",
802
- network_str
803
- ),
804
- ));
805
- }
806
-
807
- let mut iter = reader
808
- .within(network)
809
- .map_err(|e| invalid_database_exception(&format!("Failed to iterate: {}", e)))?;
810
- // Get IPAddr class
811
- let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
812
-
813
- // Iterate over all networks
814
- while let Some(result) = iter.next() {
815
- match result {
816
- Ok((network, data)) => {
817
- // Convert IpNetwork to IPAddr
818
- let ip_str = network.to_string();
819
- let ipaddr = ipaddr_class.funcall::<_, _, Value>("new", (ip_str,))?;
820
-
821
- // Yield [network, data] to block
822
- let values = (ipaddr, data.into_value());
823
- ruby.yield_values::<(Value, Value), Value>(values)?;
824
- }
825
- Err(MaxMindDbError::InvalidDatabase { .. })
826
- | Err(MaxMindDbError::Decoding { .. })
827
- | Err(MaxMindDbError::Io(_)) => {
828
- return Err(invalid_database_exception(ERR_BAD_DATA));
829
- }
830
- Err(e) => {
831
- return Err(Error::new(
832
- ruby.exception_runtime_error(),
833
- format!("Database iteration failed: {}", e),
834
- ));
835
- }
836
- }
837
- }
838
-
839
- Ok(ruby.qnil().as_value())
840
- }
841
-
842
- /// Helper method to get the reader from the ArcSwapOption
843
- fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Guard<Option<Arc<ReaderSource>>>, Error> {
844
- let guard = self.reader.load();
845
- if guard.is_none() {
846
- return Err(Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB));
847
- }
848
- Ok(guard)
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
- }
951
- }
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.
958
- unsafe impl Send for Reader {}
959
-
960
- /// Helper function to create a Reader from a ReaderSource
961
- fn create_reader(source: ReaderSource) -> Reader {
962
- let ip_version = source.metadata().ip_version;
963
- let source = Arc::new(source);
964
- Reader {
965
- reader: Arc::new(ArcSwapOption::from(Some(source))),
966
- closed: Arc::new(AtomicBool::new(false)),
967
- path_cache: Arc::new(Mutex::new(VecDeque::with_capacity(PATH_CACHE_MAX_ENTRIES))),
968
- ip_version,
969
- }
970
- }
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
-
1113
- /// Parse IP address from Ruby value (String or IPAddr) - optimized version
1114
- #[inline(always)]
1115
- fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
1116
- // Fast path: Try as RString first (most common case) - zero-copy
1117
- if let Some(rstring) = RString::from_value(value) {
1118
- // SAFETY: as_str() returns a &str that's valid as long as the Ruby string isn't modified
1119
- // We use it immediately for parsing, so this is safe
1120
- let ip_str = unsafe { rstring.as_str() }.map_err(|e| {
1121
- Error::new(
1122
- ruby.exception_arg_error(),
1123
- format!("Invalid UTF-8 in IP address string: {}", e),
1124
- )
1125
- })?;
1126
-
1127
- return parse_ip_string(ip_str, ruby);
1128
- }
1129
-
1130
- // Slow path: Try as IPAddr object
1131
- if let Ok(ipaddr_class) = ruby.class_object().const_get::<_, RClass>("IPAddr") {
1132
- if value.is_kind_of(ipaddr_class) {
1133
- let packed: Value = value.funcall("hton", ())?;
1134
- if let Some(packed_str) = RString::from_value(packed) {
1135
- // SAFETY: `bytes` is used immediately and `packed`/`packed_str` stay alive and
1136
- // unmodified through the end of this match. This block must not introduce calls
1137
- // that could move, collect, or mutate the Ruby string between `as_slice()` and
1138
- // the final byte-pattern match handling.
1139
- let bytes = unsafe { packed_str.as_slice() };
1140
- return match bytes {
1141
- [a, b, c, d] => Ok(IpAddr::from([*a, *b, *c, *d])),
1142
- [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] => {
1143
- Ok(IpAddr::from([
1144
- *a0, *a1, *a2, *a3, *a4, *a5, *a6, *a7, *a8, *a9, *a10, *a11, *a12,
1145
- *a13, *a14, *a15,
1146
- ]))
1147
- }
1148
- _ => Err(Error::new(
1149
- ruby.exception_arg_error(),
1150
- format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
1151
- )),
1152
- };
1153
- }
1154
- }
1155
- }
1156
-
1157
- if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
1158
- return parse_ip_string(&ipaddr_obj, ruby);
1159
- }
1160
-
1161
- Err(Error::new(
1162
- ruby.exception_arg_error(),
1163
- format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
1164
- ))
1165
- }
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
-
1268
- /// Generate error message for IPv6 in IPv4-only database
1269
- fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
1270
- format!(
1271
- "Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database.",
1272
- ip
1273
- )
1274
- }
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
-
1287
- /// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
1288
- fn open_database_mmap(path: &str) -> Result<Reader, Error> {
1289
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
1290
- let file = open_database_file(path, &ruby)?;
1291
-
1292
- let mmap = unsafe { Mmap::map(&file) }.map_err(|e| {
1293
- Error::new(
1294
- ruby.exception_io_error(),
1295
- format!("Failed to memory-map database file: {}", e),
1296
- )
1297
- })?;
1298
- let reader = MaxMindReader::from_source(mmap).map_err(|_| {
1299
- invalid_database_exception(&format!(
1300
- "Error opening database file ({}). Is this a valid MaxMind DB file?",
1301
- path
1302
- ))
1303
- })?;
1304
-
1305
- Ok(create_reader(ReaderSource::Mmap(reader)))
1306
- }
1307
-
1308
- /// Open a MaxMind DB by loading entire file into memory (MODE_MEMORY)
1309
- fn open_database_memory(path: &str) -> Result<Reader, Error> {
1310
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
1311
- let mut file = open_database_file(path, &ruby)?;
1312
-
1313
- let mut buffer = Vec::new();
1314
- file.read_to_end(&mut buffer).map_err(|e| {
1315
- Error::new(
1316
- ruby.exception_io_error(),
1317
- format!("Failed to read database file: {}", e),
1318
- )
1319
- })?;
1320
-
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()))?;
1340
-
1341
- Ok(create_reader(ReaderSource::Memory(reader)))
1342
- }
1343
-
1344
- fn open_database_file(path: &str, ruby: &magnus::Ruby) -> Result<File, Error> {
1345
- File::open(Path::new(path)).map_err(|e| {
1346
- if e.kind() == std::io::ErrorKind::NotFound {
1347
- open_not_found_error(ruby, e)
1348
- } else {
1349
- Error::new(ruby.exception_io_error(), e.to_string())
1350
- }
1351
- })
1352
- }
1353
-
1354
- fn open_not_found_error(ruby: &magnus::Ruby, err: std::io::Error) -> Error {
1355
- let errno = ruby
1356
- .class_object()
1357
- .const_get::<_, RModule>("Errno")
1358
- .expect("Errno module should exist");
1359
- let enoent = errno
1360
- .const_get::<_, RClass>("ENOENT")
1361
- .expect("Errno::ENOENT should exist");
1362
- Error::new(
1363
- ExceptionClass::from_value(enoent.as_value())
1364
- .expect("ENOENT should convert to ExceptionClass"),
1365
- err.to_string(),
1366
- )
1367
- }
1368
-
1369
- /// Get the InvalidDatabaseError class
1370
- fn invalid_database_error() -> RClass {
1371
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
1372
- let rust = rust_module(&ruby);
1373
- rust.const_get::<_, RClass>("InvalidDatabaseError")
1374
- .expect("InvalidDatabaseError class should exist")
1375
- }
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
-
1385
- fn rust_module(ruby: &magnus::Ruby) -> RModule {
14
+ pub(crate) fn rust_module(ruby: &magnus::Ruby) -> RModule {
1386
15
  let maxmind = ruby
1387
16
  .class_object()
1388
17
  .const_get::<_, RModule>("MaxMind")
@@ -1398,162 +27,5 @@ fn rust_module(ruby: &magnus::Ruby) -> RModule {
1398
27
 
1399
28
  #[magnus::init]
1400
29
  fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
1401
- // Define module hierarchy: MaxMind::DB::Rust
1402
- // Handle case where official maxmind-db gem may have already defined MaxMind::DB as a Class
1403
- let maxmind = ruby.define_module("MaxMind")?;
1404
-
1405
- // Try to get or define DB - it might be a Class (official gem) or Module (ours)
1406
- let db_value = maxmind.const_get::<_, Value>("DB");
1407
- let rust = match db_value {
1408
- Ok(existing) if existing.is_kind_of(ruby.class_class()) => {
1409
- // MaxMind::DB exists as a Class (official gem loaded first)
1410
- // Reuse existing Rust constant if present to avoid replacing classes.
1411
- if let Ok(rust_value) = existing.funcall::<_, _, Value>("const_get", ("Rust", false)) {
1412
- RModule::from_value(rust_value).ok_or_else(|| {
1413
- Error::new(
1414
- ruby.exception_type_error(),
1415
- "MaxMind::DB::Rust exists but is not a module",
1416
- )
1417
- })?
1418
- } else {
1419
- // Define Rust module directly as a constant on the class.
1420
- let rust_value: Value = ruby.module_new().as_value();
1421
- let rust_mod = RModule::from_value(rust_value).ok_or_else(|| {
1422
- Error::new(
1423
- ruby.exception_type_error(),
1424
- "Failed to create anonymous module for MaxMind::DB::Rust",
1425
- )
1426
- })?;
1427
- let _ = existing.funcall::<_, _, Value>("const_set", ("Rust", rust_mod))?;
1428
- rust_mod
1429
- }
1430
- }
1431
- Ok(existing) => {
1432
- // MaxMind::DB exists as a Module (our gem loaded first)
1433
- let db_mod = RModule::from_value(existing).ok_or_else(|| {
1434
- Error::new(ruby.exception_type_error(), "MaxMind::DB is not a module")
1435
- })?;
1436
- db_mod.define_module("Rust")?
1437
- }
1438
- Err(_) => {
1439
- // MaxMind::DB doesn't exist, define it as a module
1440
- let db = maxmind.define_module("DB")?;
1441
- db.define_module("Rust")?
1442
- }
1443
- };
1444
-
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))?;
1454
- }
1455
-
1456
- // The extension can be loaded more than once from different paths.
1457
- // Reusing previously defined classes avoids typed-data incompatibilities.
1458
- if rust.const_get::<_, Value>("Reader").is_ok() {
1459
- return Ok(());
1460
- }
1461
- // Define InvalidDatabaseError
1462
- let runtime_error = ruby.exception_runtime_error();
1463
- rust.define_error("InvalidDatabaseError", runtime_error)?;
1464
-
1465
- // Define Reader class
1466
- let reader_class = rust.define_class("Reader", ruby.class_object())?;
1467
- reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
1468
- reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
1469
- reader_class.define_method("get_path", magnus::method!(Reader::get_path, 2))?;
1470
- reader_class.define_method(
1471
- "get_with_prefix_length",
1472
- magnus::method!(Reader::get_with_prefix_length, 1),
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))?;
1476
- reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
1477
- reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
1478
- reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
1479
- reader_class.define_method("inspect", magnus::method!(Reader::inspect, 0))?;
1480
- reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
1481
-
1482
- // Include Enumerable module
1483
- let enumerable = ruby.class_object().const_get::<_, RModule>("Enumerable")?;
1484
- reader_class.include_module(enumerable)?;
1485
-
1486
- // Define Metadata class
1487
- let metadata_class = rust.define_class("Metadata", ruby.class_object())?;
1488
- metadata_class.define_method(
1489
- "binary_format_major_version",
1490
- magnus::method!(Metadata::binary_format_major_version, 0),
1491
- )?;
1492
- metadata_class.define_method(
1493
- "binary_format_minor_version",
1494
- magnus::method!(Metadata::binary_format_minor_version, 0),
1495
- )?;
1496
- metadata_class.define_method("build_epoch", magnus::method!(Metadata::build_epoch, 0))?;
1497
- metadata_class.define_method("database_type", magnus::method!(Metadata::database_type, 0))?;
1498
- metadata_class.define_method("description", magnus::method!(Metadata::description, 0))?;
1499
- metadata_class.define_method("ip_version", magnus::method!(Metadata::ip_version, 0))?;
1500
- metadata_class.define_method("languages", magnus::method!(Metadata::languages, 0))?;
1501
- metadata_class.define_method("node_count", magnus::method!(Metadata::node_count, 0))?;
1502
- metadata_class.define_method("record_size", magnus::method!(Metadata::record_size, 0))?;
1503
- metadata_class.define_method(
1504
- "node_byte_size",
1505
- magnus::method!(Metadata::node_byte_size, 0),
1506
- )?;
1507
- metadata_class.define_method(
1508
- "search_tree_size",
1509
- magnus::method!(Metadata::search_tree_size, 0),
1510
- )?;
1511
-
1512
- // Define MODE constants
1513
- rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
1514
- rust.const_set("MODE_FILE", ruby.to_symbol("MODE_FILE"))?;
1515
- rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
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
- )?;
1521
-
1522
- Ok(())
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
- }
30
+ initialization::initialize(ruby)
1559
31
  }