maxmind-db-rust 0.4.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,977 +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, ExceptionClass,
9
- IntoValue, RArray, RClass, RHash, RModule, RString, Symbol, Value,
10
- };
11
- use maxminddb_crate::{MaxMindDbError, 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,
18
- fmt,
19
- fs::File,
20
- hash::{Hash, Hasher},
21
- io::Read as IoRead,
22
- net::IpAddr,
23
- path::Path,
24
- str::FromStr,
25
- sync::{
26
- atomic::{AtomicBool, Ordering},
27
- Arc,
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;
7
+ use magnus::{error::Error, prelude::*, RModule, Value};
40
8
 
41
- #[derive(Default)]
42
- struct StringCacheEntry {
43
- hash: u64,
44
- value: String,
45
- }
46
-
47
- struct StringCache {
48
- entries: Box<[StringCacheEntry]>,
49
- }
50
-
51
- impl StringCache {
52
- fn new() -> Self {
53
- let entries = (0..STRING_CACHE_MAX)
54
- .map(|_| StringCacheEntry::default())
55
- .collect::<Vec<_>>()
56
- .into_boxed_slice();
57
- Self { entries }
58
- }
59
- }
60
-
61
- thread_local! {
62
- static STRING_CACHE: RefCell<StringCache> = RefCell::new(StringCache::new());
63
- static STRING_CACHE_ROOTS: OnceCell<RArray> = const { OnceCell::new() };
64
- }
65
-
66
- #[inline]
67
- fn string_cache_roots_owner(ruby: &magnus::Ruby) -> RArray {
68
- let value = rust_module(ruby)
69
- .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
70
- .expect("string cache roots constant should exist");
71
- RArray::from_value(value).expect("string cache roots constant should be an array")
72
- }
73
-
74
- #[inline]
75
- fn init_thread_string_cache_roots(ruby: &magnus::Ruby) -> RArray {
76
- let roots = ruby.ary_new_capa(STRING_CACHE_MAX);
77
- for _ in 0..STRING_CACHE_MAX {
78
- roots
79
- .push(ruby.qnil().as_value())
80
- .expect("string cache roots initialization should succeed");
81
- }
82
- string_cache_roots_owner(ruby)
83
- .push(roots.as_value())
84
- .expect("string cache roots owner should retain per-thread roots");
85
- roots
86
- }
87
-
88
- #[inline]
89
- fn string_cache_roots(ruby: &magnus::Ruby) -> RArray {
90
- STRING_CACHE_ROOTS.with(|roots| *roots.get_or_init(|| init_thread_string_cache_roots(ruby)))
91
- }
92
-
93
- #[inline]
94
- fn cached_string(ruby: &magnus::Ruby, value: &str) -> Value {
95
- if !(STRING_CACHE_MIN_LEN..=STRING_CACHE_MAX_LEN).contains(&value.len()) {
96
- return ruby.str_new(value).into_value_with(ruby);
97
- }
98
-
99
- let mut hasher = FxHasher::default();
100
- value.hash(&mut hasher);
101
- let hash = hasher.finish();
102
- let slot = (hash as usize) & (STRING_CACHE_MAX - 1);
103
-
104
- STRING_CACHE.with(|cache_cell| {
105
- let mut cache = cache_cell.borrow_mut();
106
- let entry = &mut cache.entries[slot];
107
- if entry.hash == hash && entry.value == value {
108
- return string_cache_roots(ruby)
109
- .entry::<Value>(slot as isize)
110
- .expect("string cache roots lookup should succeed");
111
- }
112
-
113
- let string = ruby.str_new(value);
114
- string.freeze();
115
- let cached = string.as_value();
116
- string_cache_roots(ruby)
117
- .store(slot as isize, cached)
118
- .expect("string cache roots update should succeed");
119
- entry.hash = hash;
120
- entry.value.clear();
121
- entry.value.push_str(value);
122
- cached
123
- })
124
- }
125
-
126
- /// Wrapper that owns the Ruby value produced by deserializing a MaxMind record
127
- #[derive(Clone)]
128
- struct RubyDecodedValue {
129
- value: Value,
130
- }
131
-
132
- impl RubyDecodedValue {
133
- #[inline]
134
- fn new(value: Value) -> Self {
135
- Self { value }
136
- }
137
-
138
- #[inline]
139
- fn into_value(self) -> Value {
140
- self.value
141
- }
142
- }
143
-
144
- impl<'de> Deserialize<'de> for RubyDecodedValue {
145
- fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
146
- where
147
- D: Deserializer<'de>,
148
- {
149
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in deserializer");
150
- RubyValueSeed { ruby: &ruby }.deserialize(deserializer)
151
- }
152
- }
153
-
154
- struct RubyValueSeed<'ruby> {
155
- ruby: &'ruby magnus::Ruby,
156
- }
157
-
158
- impl<'ruby, 'de> DeserializeSeed<'de> for RubyValueSeed<'ruby> {
159
- type Value = RubyDecodedValue;
160
-
161
- fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
162
- where
163
- D: Deserializer<'de>,
164
- {
165
- deserializer.deserialize_any(RubyValueVisitor { ruby: self.ruby })
166
- }
167
- }
168
-
169
- struct RubyValueVisitor<'ruby> {
170
- ruby: &'ruby magnus::Ruby,
171
- }
172
-
173
- impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
174
- type Value = RubyDecodedValue;
175
-
176
- fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
177
- formatter.write_str("any valid MaxMind DB value")
178
- }
179
-
180
- fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
181
- where
182
- E: de::Error,
183
- {
184
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
185
- }
186
-
187
- fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
188
- where
189
- E: de::Error,
190
- {
191
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
192
- }
193
-
194
- fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
195
- where
196
- E: de::Error,
197
- {
198
- if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
199
- Ok(RubyDecodedValue::new(
200
- (value as i32).into_value_with(self.ruby),
201
- ))
202
- } else {
203
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
204
- }
205
- }
206
-
207
- fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
208
- where
209
- E: de::Error,
210
- {
211
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
212
- }
213
-
214
- fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
215
- where
216
- E: de::Error,
217
- {
218
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
219
- }
220
-
221
- fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
222
- where
223
- E: de::Error,
224
- {
225
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
226
- }
227
-
228
- fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
229
- where
230
- E: de::Error,
231
- {
232
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
233
- }
234
-
235
- fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
236
- where
237
- E: de::Error,
238
- {
239
- Ok(RubyDecodedValue::new(
240
- (value as f64).into_value_with(self.ruby),
241
- ))
242
- }
243
-
244
- fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
245
- where
246
- E: de::Error,
247
- {
248
- Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
249
- }
250
-
251
- fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
252
- where
253
- E: de::Error,
254
- {
255
- Ok(RubyDecodedValue::new(cached_string(self.ruby, value)))
256
- }
257
-
258
- fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
259
- where
260
- E: de::Error,
261
- {
262
- Ok(RubyDecodedValue::new(cached_string(self.ruby, &value)))
263
- }
264
-
265
- fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
266
- where
267
- E: de::Error,
268
- {
269
- Ok(RubyDecodedValue::new(
270
- self.ruby.str_from_slice(value).into_value_with(self.ruby),
271
- ))
272
- }
273
-
274
- fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
275
- where
276
- E: de::Error,
277
- {
278
- Ok(RubyDecodedValue::new(
279
- self.ruby.str_from_slice(&value).into_value_with(self.ruby),
280
- ))
281
- }
282
-
283
- fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
284
- where
285
- A: SeqAccess<'de>,
286
- {
287
- let arr = match seq.size_hint() {
288
- Some(cap) => self.ruby.ary_new_capa(cap),
289
- None => self.ruby.ary_new(),
290
- };
291
- while let Some(elem) = seq.next_element_seed(RubyValueSeed { ruby: self.ruby })? {
292
- arr.push(elem.into_value())
293
- .map_err(|e| de::Error::custom(e.to_string()))?;
294
- }
295
- Ok(RubyDecodedValue::new(arr.into_value_with(self.ruby)))
296
- }
297
-
298
- fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
299
- where
300
- A: MapAccess<'de>,
301
- {
302
- let hash = match map.size_hint() {
303
- Some(cap) => self.ruby.hash_new_capa(cap),
304
- None => self.ruby.hash_new(),
305
- };
306
- while let Some(key) = map.next_key::<&'de str>()? {
307
- let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
308
- let key_val = cached_string(self.ruby, key);
309
- hash.aset(key_val, value.into_value())
310
- .map_err(|e| de::Error::custom(e.to_string()))?;
311
- }
312
- Ok(RubyDecodedValue::new(hash.into_value_with(self.ruby)))
313
- }
314
- }
315
-
316
- /// Enum to handle different reader source types
317
- enum ReaderSource {
318
- Mmap(MaxMindReader<Mmap>),
319
- Memory(MaxMindReader<Vec<u8>>),
320
- }
321
-
322
- impl ReaderSource {
323
- #[inline]
324
- fn lookup(
325
- &self,
326
- ip: IpAddr,
327
- ) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
328
- match self {
329
- ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode(),
330
- ReaderSource::Memory(reader) => reader.lookup(ip)?.decode(),
331
- }
332
- }
333
-
334
- #[inline]
335
- fn lookup_prefix(
336
- &self,
337
- ip: IpAddr,
338
- ) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
339
- let (result, prefix_len) = match self {
340
- ReaderSource::Mmap(reader) => {
341
- let result = reader.lookup(ip)?;
342
- let network = result.network()?;
343
- (result.decode()?, prefix_len_for_ip_network(ip, network))
344
- }
345
- ReaderSource::Memory(reader) => {
346
- let result = reader.lookup(ip)?;
347
- let network = result.network()?;
348
- (result.decode()?, prefix_len_for_ip_network(ip, network))
349
- }
350
- };
351
- Ok((result, prefix_len))
352
- }
353
-
354
- #[inline]
355
- fn metadata(&self) -> &maxminddb_crate::Metadata {
356
- match self {
357
- ReaderSource::Mmap(reader) => &reader.metadata,
358
- ReaderSource::Memory(reader) => &reader.metadata,
359
- }
360
- }
361
-
362
- #[inline]
363
- fn within(&self, network: IpNetwork) -> Result<ReaderWithin, MaxMindDbError> {
364
- match self {
365
- ReaderSource::Mmap(reader) => {
366
- let iter = reader.within(network, Default::default())?;
367
- Ok(ReaderWithin::Mmap(unsafe {
368
- std::mem::transmute::<Within<'_, Mmap>, Within<'static, Mmap>>(iter)
369
- }))
370
- }
371
- ReaderSource::Memory(reader) => {
372
- let iter = reader.within(network, Default::default())?;
373
- Ok(ReaderWithin::Memory(unsafe {
374
- std::mem::transmute::<Within<'_, Vec<u8>>, Within<'static, Vec<u8>>>(iter)
375
- }))
376
- }
377
- }
378
- }
379
- }
380
-
381
- /// Wrapper enum for Within iterators
382
- enum ReaderWithin {
383
- Mmap(Within<'static, Mmap>),
384
- Memory(Within<'static, Vec<u8>>),
385
- }
386
-
387
- impl ReaderWithin {
388
- fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
389
- match self {
390
- ReaderWithin::Mmap(iter) => next_within_result(iter),
391
- ReaderWithin::Memory(iter) => next_within_result(iter),
392
- }
393
- }
394
- }
395
-
396
- #[inline]
397
- // prefix_len_for_ip_network uses 0 as a sentinel for ip.is_ipv4() && network.is_ipv6().
398
- // In this case, 0 is not a real prefix length; it signals an IPv4-in-IPv6 mapping path,
399
- // and callers must treat it specially (distinct from "no network found").
400
- fn prefix_len_for_ip_network(ip: IpAddr, network: IpNetwork) -> usize {
401
- if ip.is_ipv4() && network.is_ipv6() {
402
- 0
403
- } else {
404
- network.prefix() as usize
405
- }
406
- }
407
-
408
- #[inline]
409
- fn next_within_result<S: AsRef<[u8]>>(
410
- iter: &mut Within<'static, S>,
411
- ) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
412
- loop {
413
- match iter.next() {
414
- None => return None,
415
- Some(Err(e)) => return Some(Err(e)),
416
- Some(Ok(lookup_result)) => {
417
- let network = match lookup_result.network() {
418
- Ok(n) => n,
419
- Err(e) => return Some(Err(e)),
420
- };
421
- match lookup_result.decode::<RubyDecodedValue>() {
422
- Ok(Some(data)) => return Some(Ok((network, data))),
423
- Ok(None) => continue, // Skip networks without data
424
- Err(e) => return Some(Err(e)),
425
- }
426
- }
427
- }
428
- }
429
- }
430
-
431
- /// Metadata about the MaxMind DB database
432
- #[derive(Clone)]
433
- #[magnus::wrap(class = "MaxMind::DB::Rust::Metadata")]
434
- struct Metadata {
435
- /// The major version number of the binary format used when creating the database.
436
- binary_format_major_version: u16,
437
- /// The minor version number of the binary format used when creating the database.
438
- binary_format_minor_version: u16,
439
- /// The Unix epoch timestamp for when the database was built.
440
- build_epoch: u64,
441
- /// A string identifying the database type (e.g., 'GeoIP2-City', 'GeoLite2-Country').
442
- database_type: String,
443
- description_map: BTreeMap<String, String>,
444
- /// The IP version of the data in a database. A value of 4 means IPv4 only; 6 supports both IPv4 and IPv6.
445
- ip_version: u16,
446
- languages_list: Vec<String>,
447
- /// The number of nodes in the search tree.
448
- node_count: u32,
449
- /// The record size in bits (24, 28, or 32).
450
- record_size: u16,
451
- }
452
-
453
- impl Metadata {
454
- fn binary_format_major_version(&self) -> u16 {
455
- self.binary_format_major_version
456
- }
457
-
458
- fn binary_format_minor_version(&self) -> u16 {
459
- self.binary_format_minor_version
460
- }
461
-
462
- fn build_epoch(&self) -> u64 {
463
- self.build_epoch
464
- }
465
-
466
- fn database_type(&self) -> String {
467
- self.database_type.clone()
468
- }
469
-
470
- fn description(&self) -> RHash {
471
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
472
- let hash = ruby.hash_new();
473
- for (k, v) in &self.description_map {
474
- let _ = hash.aset(k.as_str(), v.as_str());
475
- }
476
- hash
477
- }
478
-
479
- fn ip_version(&self) -> u16 {
480
- self.ip_version
481
- }
482
-
483
- fn languages(&self) -> Vec<String> {
484
- self.languages_list.clone()
485
- }
486
-
487
- fn node_count(&self) -> u32 {
488
- self.node_count
489
- }
490
-
491
- fn record_size(&self) -> u16 {
492
- self.record_size
493
- }
494
-
495
- fn node_byte_size(&self) -> u16 {
496
- self.record_size / 4
497
- }
498
-
499
- fn search_tree_size(&self) -> u32 {
500
- self.node_count * (self.record_size as u32 / 4)
501
- }
502
- }
503
-
504
- unsafe impl Send for Metadata {}
505
-
506
- /// A Ruby wrapper around the MaxMind DB reader
507
- #[derive(Clone)]
508
- #[magnus::wrap(class = "MaxMind::DB::Rust::Reader")]
509
- struct Reader {
510
- reader: Arc<ArcSwapOption<ReaderSource>>,
511
- closed: Arc<AtomicBool>,
512
- ip_version: u16,
513
- }
514
-
515
- impl Reader {
516
- fn new(args: &[Value]) -> Result<Self, Error> {
517
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
518
-
519
- let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
520
- let (database,) = args.required;
521
- let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
522
- let (mode,) = kw.optional;
523
-
524
- // Parse mode from options hash
525
- let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
526
-
527
- let mode_str = mode.name()?;
528
- let mode_str: &str = &mode_str;
529
-
530
- // Determine actual mode to use
531
- let actual_mode = match mode_str {
532
- "MODE_AUTO" | "MODE_MMAP" => "MMAP",
533
- "MODE_MEMORY" => "MEMORY",
534
- _ => {
535
- return Err(Error::new(
536
- ruby.exception_arg_error(),
537
- format!("Unsupported mode: {}", mode_str),
538
- ))
539
- }
540
- };
541
-
542
- // Open database with appropriate mode
543
- match actual_mode {
544
- "MMAP" => open_database_mmap(&database),
545
- "MEMORY" => open_database_memory(&database),
546
- _ => Err(Error::new(
547
- ruby.exception_arg_error(),
548
- format!("Invalid mode: {}", actual_mode),
549
- )),
550
- }
551
- }
552
-
553
- #[inline]
554
- fn get(&self, ip_address: Value) -> Result<Value, Error> {
555
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
556
-
557
- let guard = self.get_reader(&ruby)?;
558
- let reader_option = guard.as_ref();
559
- let reader = reader_option.as_ref().unwrap();
560
-
561
- // Parse IP address
562
- let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
563
-
564
- if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
565
- return Err(Error::new(
566
- ruby.exception_arg_error(),
567
- ipv6_in_ipv4_error(&parsed_ip),
568
- ));
569
- }
570
-
571
- // Perform lookup
572
- match reader.lookup(parsed_ip) {
573
- Ok(Some(data)) => Ok(data.into_value()),
574
- Ok(None) => Ok(ruby.qnil().as_value()),
575
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
576
- Err(Error::new(
577
- ExceptionClass::from_value(invalid_database_error().as_value())
578
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
579
- ERR_BAD_DATA,
580
- ))
581
- }
582
- Err(e) => Err(Error::new(
583
- ruby.exception_runtime_error(),
584
- format!("Database lookup failed: {}", e),
585
- )),
586
- }
587
- }
588
-
589
- #[inline]
590
- fn get_with_prefix_length(&self, ip_address: Value) -> Result<RArray, Error> {
591
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
592
-
593
- let guard = self.get_reader(&ruby)?;
594
- let reader_option = guard.as_ref();
595
- let reader = reader_option.as_ref().unwrap();
596
-
597
- // Parse IP address
598
- let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
599
-
600
- if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
601
- return Err(Error::new(
602
- ruby.exception_arg_error(),
603
- ipv6_in_ipv4_error(&parsed_ip),
604
- ));
605
- }
606
-
607
- // Perform lookup with prefix
608
- match reader.lookup_prefix(parsed_ip) {
609
- Ok((Some(data), prefix)) => {
610
- let arr = ruby.ary_new();
611
- arr.push(data.into_value())?;
612
- arr.push(prefix.into_value_with(&ruby))?;
613
- Ok(arr)
614
- }
615
- Ok((None, prefix)) => {
616
- let arr = ruby.ary_new();
617
- arr.push(ruby.qnil().as_value())?;
618
- arr.push(prefix.into_value_with(&ruby))?;
619
- Ok(arr)
620
- }
621
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
622
- Err(Error::new(
623
- ExceptionClass::from_value(invalid_database_error().as_value())
624
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
625
- ERR_BAD_DATA,
626
- ))
627
- }
628
- Err(e) => Err(Error::new(
629
- ruby.exception_runtime_error(),
630
- format!("Database lookup failed: {}", e),
631
- )),
632
- }
633
- }
634
-
635
- fn metadata(&self) -> Result<Metadata, 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
- let meta = reader.metadata();
642
-
643
- Ok(Metadata {
644
- binary_format_major_version: meta.binary_format_major_version,
645
- binary_format_minor_version: meta.binary_format_minor_version,
646
- build_epoch: meta.build_epoch,
647
- database_type: meta.database_type.clone(),
648
- description_map: meta.description.clone(),
649
- ip_version: meta.ip_version,
650
- languages_list: meta.languages.clone(),
651
- node_count: meta.node_count,
652
- record_size: meta.record_size,
653
- })
654
- }
655
-
656
- fn close(&self) {
657
- if self.closed.swap(true, Ordering::AcqRel) {
658
- return;
659
- }
660
- self.reader.store(None);
661
- }
662
-
663
- fn closed(&self) -> bool {
664
- self.closed.load(Ordering::Acquire)
665
- }
666
-
667
- fn each(&self, args: &[Value]) -> Result<Value, Error> {
668
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
669
-
670
- let guard = self.get_reader(&ruby)?;
671
- let reader_option = guard.as_ref();
672
- let reader = reader_option.as_ref().unwrap();
673
-
674
- // If no block given, return enumerator
675
- if !ruby.block_given() {
676
- return Err(Error::new(
677
- ruby.exception_runtime_error(),
678
- "Enumerator support not yet implemented, please provide a block",
679
- ));
680
- }
681
-
682
- let ip_version = reader.metadata().ip_version;
683
-
684
- // Determine the network to iterate over
685
- let network_str = if args.is_empty() {
686
- // No argument: use default (full database)
687
- if ip_version == 4 {
688
- "0.0.0.0/0".to_string()
689
- } else {
690
- "::/0".to_string()
691
- }
692
- } else {
693
- // Argument provided: extract network CIDR string
694
- let network_arg = args[0];
695
-
696
- // Try to get string representation
697
- // Accept both String and IPAddr objects
698
- let network_str_val = if let Ok(s) = RString::try_convert(network_arg) {
699
- // It's already a string
700
- s.to_string()?
701
- } else {
702
- // Check if it's an IPAddr object
703
- let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
704
- if network_arg.is_kind_of(ipaddr_class) {
705
- // It's an IPAddr - need to get both address and prefix
706
- let ip_str: String = network_arg.funcall("to_s", ())?;
707
-
708
- // Get the prefix length from IPAddr
709
- // IPAddr stores prefix as a netmask, need to convert
710
- let prefix_len: u8 = network_arg.funcall("prefix", ())?;
711
-
712
- // Construct CIDR notation
713
- format!("{}/{}", ip_str, prefix_len)
714
- } else {
715
- // Try to call to_s on it (works for other objects)
716
- let to_s_result: Value = network_arg.funcall("to_s", ())?;
717
- RString::try_convert(to_s_result)
718
- .map_err(|_| {
719
- Error::new(
720
- ruby.exception_arg_error(),
721
- "Network parameter must be a String or IPAddr",
722
- )
723
- })?
724
- .to_string()?
725
- }
726
- };
727
-
728
- network_str_val
729
- };
730
-
731
- let network = IpNetwork::from_str(&network_str).map_err(|e| {
732
- Error::new(
733
- ruby.exception_arg_error(),
734
- format!("Invalid network CIDR '{}': {}", network_str, e),
735
- )
736
- })?;
737
-
738
- // Validate network matches database IP version
739
- // IPv4 in IPv6 DB is OK (IPv4-mapped), IPv6 in IPv6 DB is OK
740
- if let (4, IpNetwork::V6(_)) = (ip_version, network) {
741
- return Err(Error::new(
742
- ruby.exception_arg_error(),
743
- format!(
744
- "Cannot search for IPv6 network '{}' in an IPv4-only database",
745
- network_str
746
- ),
747
- ));
748
- }
749
-
750
- let mut iter = reader.within(network).map_err(|e| {
751
- Error::new(
752
- ExceptionClass::from_value(invalid_database_error().as_value())
753
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
754
- format!("Failed to iterate: {}", e),
755
- )
756
- })?;
757
- // Get IPAddr class
758
- let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
759
-
760
- // Iterate over all networks
761
- while let Some(result) = iter.next() {
762
- match result {
763
- Ok((network, data)) => {
764
- // Convert IpNetwork to IPAddr
765
- let ip_str = network.to_string();
766
- let ipaddr = ipaddr_class.funcall::<_, _, Value>("new", (ip_str,))?;
767
-
768
- // Yield [network, data] to block
769
- let values = (ipaddr, data.into_value());
770
- ruby.yield_values::<(Value, Value), Value>(values)?;
771
- }
772
- Err(MaxMindDbError::InvalidDatabase { .. }) | Err(MaxMindDbError::Io(_)) => {
773
- return Err(Error::new(
774
- ExceptionClass::from_value(invalid_database_error().as_value())
775
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
776
- ERR_BAD_DATA,
777
- ));
778
- }
779
- Err(e) => {
780
- return Err(Error::new(
781
- ruby.exception_runtime_error(),
782
- format!("Database iteration failed: {}", e),
783
- ));
784
- }
785
- }
786
- }
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;
787
13
 
788
- Ok(ruby.qnil().as_value())
789
- }
790
-
791
- /// Helper method to get the reader from the ArcSwapOption
792
- fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Guard<Option<Arc<ReaderSource>>>, Error> {
793
- let guard = self.reader.load();
794
- if guard.is_none() {
795
- return Err(Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB));
796
- }
797
- Ok(guard)
798
- }
799
- }
800
-
801
- unsafe impl Send for Reader {}
802
-
803
- /// Helper function to create a Reader from a ReaderSource
804
- fn create_reader(source: ReaderSource) -> Reader {
805
- let ip_version = source.metadata().ip_version;
806
- let source = Arc::new(source);
807
- Reader {
808
- reader: Arc::new(ArcSwapOption::from(Some(source))),
809
- closed: Arc::new(AtomicBool::new(false)),
810
- ip_version,
811
- }
812
- }
813
-
814
- /// Parse IP address from Ruby value (String or IPAddr) - optimized version
815
- #[inline(always)]
816
- fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
817
- // Fast path: Try as RString first (most common case) - zero-copy
818
- if let Some(rstring) = RString::from_value(value) {
819
- // SAFETY: as_str() returns a &str that's valid as long as the Ruby string isn't modified
820
- // We use it immediately for parsing, so this is safe
821
- let ip_str = unsafe { rstring.as_str() }.map_err(|e| {
822
- Error::new(
823
- ruby.exception_arg_error(),
824
- format!("Invalid UTF-8 in IP address string: {}", e),
825
- )
826
- })?;
827
-
828
- return IpAddr::from_str(ip_str).map_err(|_| {
829
- Error::new(
830
- ruby.exception_arg_error(),
831
- format!("'{}' does not appear to be an IPv4 or IPv6 address", ip_str),
832
- )
833
- });
834
- }
835
-
836
- // Slow path: Try as IPAddr object
837
- if let Ok(ipaddr_class) = ruby.class_object().const_get::<_, RClass>("IPAddr") {
838
- if value.is_kind_of(ipaddr_class) {
839
- let packed: Value = value.funcall("hton", ())?;
840
- if let Some(packed_str) = RString::from_value(packed) {
841
- // SAFETY: `bytes` is used immediately and `packed`/`packed_str` stay alive and
842
- // unmodified through the end of this match. This block must not introduce calls
843
- // that could move, collect, or mutate the Ruby string between `as_slice()` and
844
- // the final byte-pattern match handling.
845
- let bytes = unsafe { packed_str.as_slice() };
846
- return match bytes {
847
- [a, b, c, d] => Ok(IpAddr::from([*a, *b, *c, *d])),
848
- [a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15] => {
849
- Ok(IpAddr::from([
850
- *a0, *a1, *a2, *a3, *a4, *a5, *a6, *a7, *a8, *a9, *a10, *a11, *a12,
851
- *a13, *a14, *a15,
852
- ]))
853
- }
854
- _ => Err(Error::new(
855
- ruby.exception_arg_error(),
856
- format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
857
- )),
858
- };
859
- }
860
- }
861
- }
862
-
863
- if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
864
- return IpAddr::from_str(&ipaddr_obj).map_err(|_| {
865
- Error::new(
866
- ruby.exception_arg_error(),
867
- format!(
868
- "'{}' does not appear to be an IPv4 or IPv6 address",
869
- ipaddr_obj
870
- ),
871
- )
872
- });
873
- }
874
-
875
- Err(Error::new(
876
- ruby.exception_arg_error(),
877
- format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
878
- ))
879
- }
880
-
881
- /// Generate error message for IPv6 in IPv4-only database
882
- fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
883
- format!(
884
- "Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database",
885
- ip
886
- )
887
- }
888
-
889
- /// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
890
- fn open_database_mmap(path: &str) -> Result<Reader, Error> {
891
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
892
- let file = open_database_file(path, &ruby)?;
893
-
894
- let mmap = unsafe { Mmap::map(&file) }.map_err(|e| {
895
- Error::new(
896
- ruby.exception_io_error(),
897
- format!("Failed to memory-map database file: {}", e),
898
- )
899
- })?;
900
- let reader = MaxMindReader::from_source(mmap).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
- })?;
910
-
911
- Ok(create_reader(ReaderSource::Mmap(reader)))
912
- }
913
-
914
- /// Open a MaxMind DB by loading entire file into memory (MODE_MEMORY)
915
- fn open_database_memory(path: &str) -> Result<Reader, Error> {
916
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
917
- let mut file = open_database_file(path, &ruby)?;
918
-
919
- let mut buffer = Vec::new();
920
- file.read_to_end(&mut buffer).map_err(|e| {
921
- Error::new(
922
- ruby.exception_io_error(),
923
- format!("Failed to read database file: {}", e),
924
- )
925
- })?;
926
-
927
- let reader = MaxMindReader::from_source(buffer).map_err(|_| {
928
- Error::new(
929
- ExceptionClass::from_value(invalid_database_error().as_value())
930
- .expect("InvalidDatabaseError should convert to ExceptionClass"),
931
- format!(
932
- "Error opening database file ({}). Is this a valid MaxMind DB file?",
933
- path
934
- ),
935
- )
936
- })?;
937
-
938
- Ok(create_reader(ReaderSource::Memory(reader)))
939
- }
940
-
941
- fn open_database_file(path: &str, ruby: &magnus::Ruby) -> Result<File, Error> {
942
- File::open(Path::new(path)).map_err(|e| {
943
- if e.kind() == std::io::ErrorKind::NotFound {
944
- open_not_found_error(ruby, e)
945
- } else {
946
- Error::new(ruby.exception_io_error(), e.to_string())
947
- }
948
- })
949
- }
950
-
951
- fn open_not_found_error(ruby: &magnus::Ruby, err: std::io::Error) -> Error {
952
- let errno = ruby
953
- .class_object()
954
- .const_get::<_, RModule>("Errno")
955
- .expect("Errno module should exist");
956
- let enoent = errno
957
- .const_get::<_, RClass>("ENOENT")
958
- .expect("Errno::ENOENT should exist");
959
- Error::new(
960
- ExceptionClass::from_value(enoent.as_value())
961
- .expect("ENOENT should convert to ExceptionClass"),
962
- err.to_string(),
963
- )
964
- }
965
-
966
- /// Get the InvalidDatabaseError class
967
- fn invalid_database_error() -> RClass {
968
- let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
969
- let rust = rust_module(&ruby);
970
- rust.const_get::<_, RClass>("InvalidDatabaseError")
971
- .expect("InvalidDatabaseError class should exist")
972
- }
973
-
974
- fn rust_module(ruby: &magnus::Ruby) -> RModule {
14
+ pub(crate) fn rust_module(ruby: &magnus::Ruby) -> RModule {
975
15
  let maxmind = ruby
976
16
  .class_object()
977
17
  .const_get::<_, RModule>("MaxMind")
@@ -987,117 +27,5 @@ fn rust_module(ruby: &magnus::Ruby) -> RModule {
987
27
 
988
28
  #[magnus::init]
989
29
  fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
990
- // Define module hierarchy: MaxMind::DB::Rust
991
- // Handle case where official maxmind-db gem may have already defined MaxMind::DB as a Class
992
- let maxmind = ruby.define_module("MaxMind")?;
993
-
994
- // Try to get or define DB - it might be a Class (official gem) or Module (ours)
995
- let db_value = maxmind.const_get::<_, Value>("DB");
996
- let rust = match db_value {
997
- Ok(existing) if existing.is_kind_of(ruby.class_class()) => {
998
- // MaxMind::DB exists as a Class (official gem loaded first)
999
- // Reuse existing Rust constant if present to avoid replacing classes.
1000
- if let Ok(rust_value) = existing.funcall::<_, _, Value>("const_get", ("Rust", false)) {
1001
- RModule::from_value(rust_value).ok_or_else(|| {
1002
- Error::new(
1003
- ruby.exception_type_error(),
1004
- "MaxMind::DB::Rust exists but is not a module",
1005
- )
1006
- })?
1007
- } else {
1008
- // Define Rust module directly as a constant on the class.
1009
- let rust_value: Value = ruby.module_new().as_value();
1010
- let rust_mod = RModule::from_value(rust_value).ok_or_else(|| {
1011
- Error::new(
1012
- ruby.exception_type_error(),
1013
- "Failed to create anonymous module for MaxMind::DB::Rust",
1014
- )
1015
- })?;
1016
- let _ = existing.funcall::<_, _, Value>("const_set", ("Rust", rust_mod))?;
1017
- rust_mod
1018
- }
1019
- }
1020
- Ok(existing) => {
1021
- // MaxMind::DB exists as a Module (our gem loaded first)
1022
- let db_mod = RModule::from_value(existing).ok_or_else(|| {
1023
- Error::new(ruby.exception_type_error(), "MaxMind::DB is not a module")
1024
- })?;
1025
- db_mod.define_module("Rust")?
1026
- }
1027
- Err(_) => {
1028
- // MaxMind::DB doesn't exist, define it as a module
1029
- let db = maxmind.define_module("DB")?;
1030
- db.define_module("Rust")?
1031
- }
1032
- };
1033
-
1034
- if rust
1035
- .const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
1036
- .is_err()
1037
- {
1038
- rust.const_set(STRING_CACHE_ROOTS_CONST, ruby.ary_new())?;
1039
- }
1040
-
1041
- if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_ok() {
1042
- let _ = rust.funcall::<_, _, Value>("send", ("remove_const", MAP_KEY_ROOTS_CONST))?;
1043
- }
1044
-
1045
- // The extension can be loaded more than once from different paths.
1046
- // Reusing previously defined classes avoids typed-data incompatibilities.
1047
- if rust.const_get::<_, Value>("Reader").is_ok() {
1048
- return Ok(());
1049
- }
1050
- // Define InvalidDatabaseError
1051
- let runtime_error = ruby.exception_runtime_error();
1052
- rust.define_error("InvalidDatabaseError", runtime_error)?;
1053
-
1054
- // Define Reader class
1055
- let reader_class = rust.define_class("Reader", ruby.class_object())?;
1056
- reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
1057
- reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
1058
- reader_class.define_method(
1059
- "get_with_prefix_length",
1060
- magnus::method!(Reader::get_with_prefix_length, 1),
1061
- )?;
1062
- reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
1063
- reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
1064
- reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
1065
- reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
1066
-
1067
- // Include Enumerable module
1068
- let enumerable = ruby.class_object().const_get::<_, RModule>("Enumerable")?;
1069
- reader_class.include_module(enumerable)?;
1070
-
1071
- // Define Metadata class
1072
- let metadata_class = rust.define_class("Metadata", ruby.class_object())?;
1073
- metadata_class.define_method(
1074
- "binary_format_major_version",
1075
- magnus::method!(Metadata::binary_format_major_version, 0),
1076
- )?;
1077
- metadata_class.define_method(
1078
- "binary_format_minor_version",
1079
- magnus::method!(Metadata::binary_format_minor_version, 0),
1080
- )?;
1081
- metadata_class.define_method("build_epoch", magnus::method!(Metadata::build_epoch, 0))?;
1082
- metadata_class.define_method("database_type", magnus::method!(Metadata::database_type, 0))?;
1083
- metadata_class.define_method("description", magnus::method!(Metadata::description, 0))?;
1084
- metadata_class.define_method("ip_version", magnus::method!(Metadata::ip_version, 0))?;
1085
- metadata_class.define_method("languages", magnus::method!(Metadata::languages, 0))?;
1086
- metadata_class.define_method("node_count", magnus::method!(Metadata::node_count, 0))?;
1087
- metadata_class.define_method("record_size", magnus::method!(Metadata::record_size, 0))?;
1088
- metadata_class.define_method(
1089
- "node_byte_size",
1090
- magnus::method!(Metadata::node_byte_size, 0),
1091
- )?;
1092
- metadata_class.define_method(
1093
- "search_tree_size",
1094
- magnus::method!(Metadata::search_tree_size, 0),
1095
- )?;
1096
-
1097
- // Define MODE constants
1098
- rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
1099
- rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
1100
- rust.const_set("MODE_MMAP", ruby.to_symbol("MODE_MMAP"))?;
1101
-
1102
- Ok(())
30
+ initialization::initialize(ruby)
1103
31
  }