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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +85 -17
- data/ext/maxmind_db_rust/Cargo.toml +2 -2
- data/ext/maxmind_db_rust/extconf.rb +1 -1
- data/ext/maxmind_db_rust/lib/maxmind/db/rust.rb +1 -1
- data/ext/maxmind_db_rust/src/lib.rs +710 -220
- data/lib/maxmind/db/rust.rb +2 -2
- metadata +2 -2
|
@@ -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,
|
|
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::
|
|
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
|
-
|
|
36
|
-
|
|
48
|
+
struct StringCache {
|
|
49
|
+
entries: Box<[StringCacheEntry]>,
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
57
|
-
|
|
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
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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) =>
|
|
307
|
-
ReaderSource::Memory(reader) =>
|
|
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
|
|
412
|
+
fn within(&self, network: IpNetwork) -> Result<ReaderWithin<'_>, MaxMindDbError> {
|
|
321
413
|
match self {
|
|
322
|
-
ReaderSource::Mmap(reader) =>
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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<'
|
|
344
|
-
Memory(Within<'
|
|
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<'
|
|
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::<(
|
|
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
|
|
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
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
-
|
|
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
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
-
|
|
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
|
|
639
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
722
|
-
|
|
723
|
-
|
|
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 { .. })
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
|
1008
|
-
|
|
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
|
+
}
|