maxmind-db-rust 0.4.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 +43 -0
- data/README.md +85 -17
- data/ext/maxmind_db_rust/Cargo.toml +1 -1
- 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 +611 -155
- data/lib/maxmind/db/rust.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 477024cc8d20f8e13fa22fa0e6d8a94554c91f16812e81ef2154ae5cc821739c
|
|
4
|
+
data.tar.gz: 4a5b94f8b821af36bed45f0b164ea1c5061c734a3827afc8f0ed059ec986c4b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad9547ef7bf1d3371325d7fd519f78f0d01f302e5fecc4e5181d9a88e57aa1ebed4c51b1f6ae08491133aab05a63ad31516bae33051a78362308ec77ece1e204
|
|
7
|
+
data.tar.gz: f5be53fe4952c00ad82c3824a0b17aa7ae420a9580261686847f684ab8834fbbaa6d5b316405f6a70ee14fc9fd25717fcb367d49af6c5e9d29fb10ed33e2736b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.5.0] - 2026-06-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `MODE_FILE` as an official-gem compatibility alias for path-backed
|
|
13
|
+
memory-mapped readers.
|
|
14
|
+
- Added `MODE_PARAM_IS_BUFFER` for constructing readers from Ruby strings
|
|
15
|
+
containing database bytes.
|
|
16
|
+
- Added `Reader#get_path` and `Reader#get_many_path` for selective path lookups.
|
|
17
|
+
- Added `Reader#get_many` for batch lookups over arrays and enumerables.
|
|
18
|
+
- Added Enumerator return support for `Reader#each` when called without a block.
|
|
19
|
+
- Added `Reader#inspect` with closed state and database IP version.
|
|
20
|
+
- Added benchmark tooling for comparing git refs and measuring Ruby object
|
|
21
|
+
allocations.
|
|
22
|
+
- Added official gem parity tests, compatibility audit tests, bad data corpus
|
|
23
|
+
tests, and reader concurrency stress tests.
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Upgraded the `maxminddb` crate to 0.28.1.
|
|
28
|
+
- Improved IPv4 string lookup performance with a strict fast-path parser.
|
|
29
|
+
- Streamed non-array `get_many` and `get_many_path` inputs instead of
|
|
30
|
+
materializing enumerables.
|
|
31
|
+
- Cached parsed lookup paths per reader to reduce repeated path parsing.
|
|
32
|
+
- Simplified reader open mode parsing and centralized lookup error handling.
|
|
33
|
+
- Documented Rust extension safety invariants and `Send` requirements.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Fixed source gem native extension installation so `require "maxmind/db/rust"`
|
|
38
|
+
works after installing the source gem.
|
|
39
|
+
- Removed an unsafe reader iterator transmute.
|
|
40
|
+
- Improved invalid database error construction consistency across reader open
|
|
41
|
+
and iteration paths.
|
|
42
|
+
- Fixed dead test assertions and made adapted MaxMind tests independent of the
|
|
43
|
+
current working directory.
|
|
44
|
+
|
|
45
|
+
### Security
|
|
46
|
+
|
|
47
|
+
- Pinned GitHub Actions to commit SHAs.
|
|
48
|
+
- Restricted workflow permissions and disabled persisted checkout credentials.
|
|
49
|
+
- Added a zizmor workflow and source-gem install smoke test to release checks.
|
|
50
|
+
|
|
8
51
|
## [0.4.0] - 2026-04-25
|
|
9
52
|
|
|
10
53
|
### Performance
|
data/README.md
CHANGED
|
@@ -13,9 +13,10 @@ It keeps the API close to the official `maxmind-db` gem while adding Rust-backed
|
|
|
13
13
|
- Rust implementation focused on fast lookups
|
|
14
14
|
- API modeled after the official `maxmind-db` gem
|
|
15
15
|
- Thread-safe lookups
|
|
16
|
-
- Supports MMAP
|
|
16
|
+
- Supports file-backed, MMAP, in-memory, and buffer-backed modes
|
|
17
17
|
- Includes network iteration support
|
|
18
18
|
- Accepts both `String` and `IPAddr` inputs
|
|
19
|
+
- Includes selective path lookup and batch lookup extensions
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
21
22
|
|
|
@@ -81,6 +82,26 @@ puts "Prefix length: #{prefix_length}"
|
|
|
81
82
|
reader.close
|
|
82
83
|
```
|
|
83
84
|
|
|
85
|
+
### Selective and Batch Lookups
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
require 'maxmind/db/rust'
|
|
89
|
+
|
|
90
|
+
reader = MaxMind::DB::Rust::Reader.new('GeoIP2-City.mmdb')
|
|
91
|
+
|
|
92
|
+
# Decode one field without materializing the full record.
|
|
93
|
+
iso_code = reader.get_path('8.8.8.8', ['country', 'iso_code'])
|
|
94
|
+
|
|
95
|
+
# Batch full-record lookups.
|
|
96
|
+
ips = ['8.8.8.8', '1.1.1.1', '208.67.222.222']
|
|
97
|
+
records = reader.get_many(ips)
|
|
98
|
+
|
|
99
|
+
# Batch one-field lookups.
|
|
100
|
+
iso_codes = reader.get_many_path(ips, ['country', 'iso_code'])
|
|
101
|
+
|
|
102
|
+
reader.close
|
|
103
|
+
```
|
|
104
|
+
|
|
84
105
|
### Using IPAddr Objects
|
|
85
106
|
|
|
86
107
|
```ruby
|
|
@@ -112,11 +133,24 @@ reader = MaxMind::DB::Rust::Reader.new(
|
|
|
112
133
|
mode: MaxMind::DB::Rust::MODE_MMAP
|
|
113
134
|
)
|
|
114
135
|
|
|
136
|
+
# MODE_FILE: Official-gem compatibility alias for path-backed MMAP
|
|
137
|
+
reader = MaxMind::DB::Rust::Reader.new(
|
|
138
|
+
'GeoIP2-City.mmdb',
|
|
139
|
+
mode: MaxMind::DB::Rust::MODE_FILE
|
|
140
|
+
)
|
|
141
|
+
|
|
115
142
|
# MODE_MEMORY: Load entire database into memory
|
|
116
143
|
reader = MaxMind::DB::Rust::Reader.new(
|
|
117
144
|
'GeoIP2-City.mmdb',
|
|
118
145
|
mode: MaxMind::DB::Rust::MODE_MEMORY
|
|
119
146
|
)
|
|
147
|
+
|
|
148
|
+
# MODE_PARAM_IS_BUFFER: Read from a String containing database bytes
|
|
149
|
+
buffer = File.binread('GeoIP2-City.mmdb')
|
|
150
|
+
reader = MaxMind::DB::Rust::Reader.new(
|
|
151
|
+
buffer,
|
|
152
|
+
mode: MaxMind::DB::Rust::MODE_PARAM_IS_BUFFER
|
|
153
|
+
)
|
|
120
154
|
```
|
|
121
155
|
|
|
122
156
|
### Accessing Metadata
|
|
@@ -176,15 +210,15 @@ reader.close
|
|
|
176
210
|
|
|
177
211
|
### `MaxMind::DB::Rust::Reader`
|
|
178
212
|
|
|
179
|
-
#### `new(
|
|
213
|
+
#### `new(database, options = {})`
|
|
180
214
|
|
|
181
215
|
Create a new Reader instance.
|
|
182
216
|
|
|
183
217
|
**Parameters:**
|
|
184
218
|
|
|
185
|
-
- `
|
|
219
|
+
- `database` (String): Path to the MaxMind DB file, or database bytes when using `:MODE_PARAM_IS_BUFFER`
|
|
186
220
|
- `options` (Hash): Optional configuration
|
|
187
|
-
- `:mode` (Symbol): One of `:MODE_AUTO`, `:MODE_MEMORY`, or `:
|
|
221
|
+
- `:mode` (Symbol): One of `:MODE_AUTO`, `:MODE_FILE`, `:MODE_MEMORY`, `:MODE_MMAP`, or `:MODE_PARAM_IS_BUFFER`
|
|
188
222
|
|
|
189
223
|
**Returns:** Reader instance
|
|
190
224
|
|
|
@@ -208,6 +242,17 @@ Look up an IP address in the database.
|
|
|
208
242
|
- `ArgumentError`: If looking up IPv6 in an IPv4-only database
|
|
209
243
|
- `MaxMind::DB::Rust::InvalidDatabaseError`: If the database is corrupt
|
|
210
244
|
|
|
245
|
+
#### `get_path(ip_address, path)`
|
|
246
|
+
|
|
247
|
+
Look up an IP address and return only the value at `path`.
|
|
248
|
+
|
|
249
|
+
**Parameters:**
|
|
250
|
+
|
|
251
|
+
- `ip_address` (String or IPAddr): The IP address to look up
|
|
252
|
+
- `path` (Array): String map keys and Integer array indexes. Negative indexes count from the end.
|
|
253
|
+
|
|
254
|
+
**Returns:** The value at the path, or `nil` if the record or path is not found
|
|
255
|
+
|
|
211
256
|
#### `get_with_prefix_length(ip_address)`
|
|
212
257
|
|
|
213
258
|
Look up an IP address and return the prefix length.
|
|
@@ -218,6 +263,27 @@ Look up an IP address and return the prefix length.
|
|
|
218
263
|
|
|
219
264
|
**Returns:** Array `[record, prefix_length]` where record is a Hash or `nil`
|
|
220
265
|
|
|
266
|
+
#### `get_many(ip_addresses)`
|
|
267
|
+
|
|
268
|
+
Look up multiple IP addresses.
|
|
269
|
+
|
|
270
|
+
**Parameters:**
|
|
271
|
+
|
|
272
|
+
- `ip_addresses` (Array or Enumerable): IP address strings or IPAddr objects
|
|
273
|
+
|
|
274
|
+
**Returns:** Array of record values in input order
|
|
275
|
+
|
|
276
|
+
#### `get_many_path(ip_addresses, path)`
|
|
277
|
+
|
|
278
|
+
Look up one path for multiple IP addresses.
|
|
279
|
+
|
|
280
|
+
**Parameters:**
|
|
281
|
+
|
|
282
|
+
- `ip_addresses` (Array or Enumerable): IP address strings or IPAddr objects
|
|
283
|
+
- `path` (Array): String map keys and Integer array indexes
|
|
284
|
+
|
|
285
|
+
**Returns:** Array of path values in input order
|
|
286
|
+
|
|
221
287
|
#### `metadata()`
|
|
222
288
|
|
|
223
289
|
Get metadata about the database.
|
|
@@ -269,8 +335,10 @@ Metadata attributes:
|
|
|
269
335
|
### Constants
|
|
270
336
|
|
|
271
337
|
- `MaxMind::DB::Rust::MODE_AUTO` - Automatically choose the best mode (uses MMAP)
|
|
338
|
+
- `MaxMind::DB::Rust::MODE_FILE` - Official-gem compatibility alias for path-backed MMAP
|
|
272
339
|
- `MaxMind::DB::Rust::MODE_MEMORY` - Load entire database into memory
|
|
273
340
|
- `MaxMind::DB::Rust::MODE_MMAP` - Use memory-mapped file I/O (recommended)
|
|
341
|
+
- `MaxMind::DB::Rust::MODE_PARAM_IS_BUFFER` - Read database bytes from a Ruby String
|
|
274
342
|
|
|
275
343
|
### Exceptions
|
|
276
344
|
|
|
@@ -278,26 +346,26 @@ Metadata attributes:
|
|
|
278
346
|
|
|
279
347
|
## Comparison with Official Gem
|
|
280
348
|
|
|
281
|
-
| Feature
|
|
282
|
-
|
|
|
283
|
-
| Implementation
|
|
284
|
-
| Performance
|
|
285
|
-
| API
|
|
286
|
-
| MODE_FILE
|
|
287
|
-
| MODE_MEMORY
|
|
288
|
-
| MODE_AUTO
|
|
289
|
-
|
|
|
290
|
-
|
|
|
291
|
-
|
|
|
349
|
+
| Feature | maxmind-db (official) | maxmind-db-rust (this gem) |
|
|
350
|
+
| -------------------- | --------------------- | ------------------------------------------ |
|
|
351
|
+
| Implementation | Pure Ruby | Rust with Ruby bindings |
|
|
352
|
+
| Performance | Baseline | Faster lookup throughput in our benchmarks |
|
|
353
|
+
| API | MaxMind::DB | MaxMind::DB::Rust |
|
|
354
|
+
| MODE_FILE | ✓ | ✓ |
|
|
355
|
+
| MODE_MEMORY | ✓ | ✓ |
|
|
356
|
+
| MODE_AUTO | ✓ | ✓ |
|
|
357
|
+
| MODE_PARAM_IS_BUFFER | ✓ | ✓ |
|
|
358
|
+
| MODE_MMAP | ✗ | ✓ |
|
|
359
|
+
| Iterator support | ✗ | ✓ |
|
|
360
|
+
| Thread-safe | ✓ | ✓ |
|
|
292
361
|
|
|
293
362
|
## Performance
|
|
294
363
|
|
|
295
364
|
Lookup performance depends on hardware, Ruby version, database, and workload.
|
|
296
365
|
|
|
297
366
|
- In this project’s random-lookup benchmarks, this gem is consistently faster than the official Ruby implementation.
|
|
298
|
-
- On `/var/lib/GeoIP/GeoIP2-City.mmdb` in this environment, random lookup throughput was about `47x` higher than the official gem.
|
|
299
367
|
- `MODE_MMAP` and `MODE_MEMORY` both perform well; which is faster can vary by environment.
|
|
300
|
-
- For reproducible numbers on your own data, run `benchmark/compare_lookups.rb` against your database.
|
|
368
|
+
- For current, reproducible numbers on your own data and Ruby version, run `benchmark/compare_lookups.rb` against your database.
|
|
301
369
|
- Safe for concurrent lookups across threads.
|
|
302
370
|
|
|
303
371
|
## Development
|
|
@@ -15,7 +15,7 @@ crate-type = ["cdylib"]
|
|
|
15
15
|
arc-swap = "1.9"
|
|
16
16
|
ipnetwork = "0.21"
|
|
17
17
|
magnus = "0.8"
|
|
18
|
-
maxminddb = { version = "0.28", features = ["unsafe-str-decode"] }
|
|
18
|
+
maxminddb = { version = "0.28.1", features = ["unsafe-str-decode"] }
|
|
19
19
|
memmap2 = "0.9"
|
|
20
20
|
rustc-hash = "2.1"
|
|
21
21
|
serde = "1.0"
|
|
@@ -5,26 +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
13
|
use rustc_hash::FxHasher;
|
|
14
14
|
use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
|
|
15
15
|
use std::{
|
|
16
16
|
cell::{OnceCell, RefCell},
|
|
17
|
-
collections::BTreeMap,
|
|
17
|
+
collections::{BTreeMap, VecDeque},
|
|
18
18
|
fmt,
|
|
19
19
|
fs::File,
|
|
20
20
|
hash::{Hash, Hasher},
|
|
21
21
|
io::Read as IoRead,
|
|
22
|
-
net::IpAddr,
|
|
22
|
+
net::{IpAddr, Ipv4Addr},
|
|
23
23
|
path::Path,
|
|
24
24
|
str::FromStr,
|
|
25
25
|
sync::{
|
|
26
26
|
atomic::{AtomicBool, Ordering},
|
|
27
|
-
Arc,
|
|
27
|
+
Arc, Mutex,
|
|
28
28
|
},
|
|
29
29
|
};
|
|
30
30
|
|
|
@@ -37,6 +37,7 @@ const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
|
|
|
37
37
|
const STRING_CACHE_MAX: usize = 4096;
|
|
38
38
|
const STRING_CACHE_MIN_LEN: usize = 2;
|
|
39
39
|
const STRING_CACHE_MAX_LEN: usize = 64;
|
|
40
|
+
const PATH_CACHE_MAX_ENTRIES: usize = 64;
|
|
40
41
|
|
|
41
42
|
#[derive(Default)]
|
|
42
43
|
struct StringCacheEntry {
|
|
@@ -319,6 +320,42 @@ enum ReaderSource {
|
|
|
319
320
|
Memory(MaxMindReader<Vec<u8>>),
|
|
320
321
|
}
|
|
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
|
+
|
|
322
359
|
impl ReaderSource {
|
|
323
360
|
#[inline]
|
|
324
361
|
fn lookup(
|
|
@@ -351,6 +388,18 @@ impl ReaderSource {
|
|
|
351
388
|
Ok((result, prefix_len))
|
|
352
389
|
}
|
|
353
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
|
+
|
|
354
403
|
#[inline]
|
|
355
404
|
fn metadata(&self) -> &maxminddb_crate::Metadata {
|
|
356
405
|
match self {
|
|
@@ -360,31 +409,25 @@ impl ReaderSource {
|
|
|
360
409
|
}
|
|
361
410
|
|
|
362
411
|
#[inline]
|
|
363
|
-
fn within(&self, network: IpNetwork) -> Result<ReaderWithin
|
|
412
|
+
fn within(&self, network: IpNetwork) -> Result<ReaderWithin<'_>, MaxMindDbError> {
|
|
364
413
|
match self {
|
|
365
|
-
ReaderSource::Mmap(reader) =>
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
}
|
|
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
|
+
)),
|
|
377
420
|
}
|
|
378
421
|
}
|
|
379
422
|
}
|
|
380
423
|
|
|
381
424
|
/// Wrapper enum for Within iterators
|
|
382
|
-
enum ReaderWithin {
|
|
383
|
-
Mmap(Within<'
|
|
384
|
-
Memory(Within<'
|
|
425
|
+
enum ReaderWithin<'reader> {
|
|
426
|
+
Mmap(Within<'reader, Mmap>),
|
|
427
|
+
Memory(Within<'reader, Vec<u8>>),
|
|
385
428
|
}
|
|
386
429
|
|
|
387
|
-
impl ReaderWithin {
|
|
430
|
+
impl ReaderWithin<'_> {
|
|
388
431
|
fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
|
|
389
432
|
match self {
|
|
390
433
|
ReaderWithin::Mmap(iter) => next_within_result(iter),
|
|
@@ -407,7 +450,7 @@ fn prefix_len_for_ip_network(ip: IpAddr, network: IpNetwork) -> usize {
|
|
|
407
450
|
|
|
408
451
|
#[inline]
|
|
409
452
|
fn next_within_result<S: AsRef<[u8]>>(
|
|
410
|
-
iter: &mut Within<'
|
|
453
|
+
iter: &mut Within<'_, S>,
|
|
411
454
|
) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
|
|
412
455
|
loop {
|
|
413
456
|
match iter.next() {
|
|
@@ -501,6 +544,10 @@ impl Metadata {
|
|
|
501
544
|
}
|
|
502
545
|
}
|
|
503
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.
|
|
504
551
|
unsafe impl Send for Metadata {}
|
|
505
552
|
|
|
506
553
|
/// A Ruby wrapper around the MaxMind DB reader
|
|
@@ -509,6 +556,7 @@ unsafe impl Send for Metadata {}
|
|
|
509
556
|
struct Reader {
|
|
510
557
|
reader: Arc<ArcSwapOption<ReaderSource>>,
|
|
511
558
|
closed: Arc<AtomicBool>,
|
|
559
|
+
path_cache: Arc<Mutex<VecDeque<CachedPath>>>,
|
|
512
560
|
ip_version: u16,
|
|
513
561
|
}
|
|
514
562
|
|
|
@@ -516,7 +564,7 @@ impl Reader {
|
|
|
516
564
|
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
517
565
|
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
518
566
|
|
|
519
|
-
let args = scan_args::<(
|
|
567
|
+
let args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
|
|
520
568
|
let (database,) = args.required;
|
|
521
569
|
let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
|
|
522
570
|
let (mode,) = kw.optional;
|
|
@@ -524,29 +572,13 @@ impl Reader {
|
|
|
524
572
|
// Parse mode from options hash
|
|
525
573
|
let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
|
|
526
574
|
|
|
527
|
-
let
|
|
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
|
-
};
|
|
575
|
+
let open_mode = OpenMode::from_symbol(mode, &ruby)?;
|
|
541
576
|
|
|
542
577
|
// Open database with appropriate mode
|
|
543
|
-
match
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
ruby.exception_arg_error(),
|
|
548
|
-
format!("Invalid mode: {}", actual_mode),
|
|
549
|
-
)),
|
|
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)?),
|
|
550
582
|
}
|
|
551
583
|
}
|
|
552
584
|
|
|
@@ -558,32 +590,28 @@ impl Reader {
|
|
|
558
590
|
let reader_option = guard.as_ref();
|
|
559
591
|
let reader = reader_option.as_ref().unwrap();
|
|
560
592
|
|
|
561
|
-
|
|
562
|
-
let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
|
|
593
|
+
let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
|
|
563
594
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
ruby.exception_arg_error(),
|
|
567
|
-
ipv6_in_ipv4_error(&parsed_ip),
|
|
568
|
-
));
|
|
569
|
-
}
|
|
595
|
+
lookup_result_to_value(&ruby, reader.lookup(parsed_ip), "Database lookup failed")
|
|
596
|
+
}
|
|
570
597
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
)
|
|
586
|
-
|
|
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
|
+
)
|
|
587
615
|
}
|
|
588
616
|
|
|
589
617
|
#[inline]
|
|
@@ -594,42 +622,65 @@ impl Reader {
|
|
|
594
622
|
let reader_option = guard.as_ref();
|
|
595
623
|
let reader = reader_option.as_ref().unwrap();
|
|
596
624
|
|
|
597
|
-
|
|
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
|
-
}
|
|
625
|
+
let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
|
|
606
626
|
|
|
607
627
|
// Perform lookup with prefix
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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)?)?;
|
|
620
647
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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)?)?;
|
|
627
674
|
}
|
|
628
|
-
|
|
629
|
-
ruby.exception_runtime_error(),
|
|
630
|
-
format!("Database lookup failed: {}", e),
|
|
631
|
-
)),
|
|
675
|
+
return Ok(results);
|
|
632
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)
|
|
633
684
|
}
|
|
634
685
|
|
|
635
686
|
fn metadata(&self) -> Result<Metadata, Error> {
|
|
@@ -664,19 +715,25 @@ impl Reader {
|
|
|
664
715
|
self.closed.load(Ordering::Acquire)
|
|
665
716
|
}
|
|
666
717
|
|
|
667
|
-
fn
|
|
668
|
-
|
|
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
|
+
}
|
|
669
726
|
|
|
670
|
-
|
|
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)?;
|
|
671
731
|
let reader_option = guard.as_ref();
|
|
672
732
|
let reader = reader_option.as_ref().unwrap();
|
|
673
733
|
|
|
674
734
|
// If no block given, return enumerator
|
|
675
735
|
if !ruby.block_given() {
|
|
676
|
-
return
|
|
677
|
-
ruby.exception_runtime_error(),
|
|
678
|
-
"Enumerator support not yet implemented, please provide a block",
|
|
679
|
-
));
|
|
736
|
+
return Ok(rb_self.enumeratorize("each", args).as_value());
|
|
680
737
|
}
|
|
681
738
|
|
|
682
739
|
let ip_version = reader.metadata().ip_version;
|
|
@@ -747,13 +804,9 @@ impl Reader {
|
|
|
747
804
|
));
|
|
748
805
|
}
|
|
749
806
|
|
|
750
|
-
let mut iter = reader
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
754
|
-
format!("Failed to iterate: {}", e),
|
|
755
|
-
)
|
|
756
|
-
})?;
|
|
807
|
+
let mut iter = reader
|
|
808
|
+
.within(network)
|
|
809
|
+
.map_err(|e| invalid_database_exception(&format!("Failed to iterate: {}", e)))?;
|
|
757
810
|
// Get IPAddr class
|
|
758
811
|
let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
|
|
759
812
|
|
|
@@ -769,12 +822,10 @@ impl Reader {
|
|
|
769
822
|
let values = (ipaddr, data.into_value());
|
|
770
823
|
ruby.yield_values::<(Value, Value), Value>(values)?;
|
|
771
824
|
}
|
|
772
|
-
Err(MaxMindDbError::InvalidDatabase { .. })
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
ERR_BAD_DATA,
|
|
777
|
-
));
|
|
825
|
+
Err(MaxMindDbError::InvalidDatabase { .. })
|
|
826
|
+
| Err(MaxMindDbError::Decoding { .. })
|
|
827
|
+
| Err(MaxMindDbError::Io(_)) => {
|
|
828
|
+
return Err(invalid_database_exception(ERR_BAD_DATA));
|
|
778
829
|
}
|
|
779
830
|
Err(e) => {
|
|
780
831
|
return Err(Error::new(
|
|
@@ -796,8 +847,114 @@ impl Reader {
|
|
|
796
847
|
}
|
|
797
848
|
Ok(guard)
|
|
798
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
|
+
}
|
|
799
951
|
}
|
|
800
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.
|
|
801
958
|
unsafe impl Send for Reader {}
|
|
802
959
|
|
|
803
960
|
/// Helper function to create a Reader from a ReaderSource
|
|
@@ -807,10 +964,152 @@ fn create_reader(source: ReaderSource) -> Reader {
|
|
|
807
964
|
Reader {
|
|
808
965
|
reader: Arc::new(ArcSwapOption::from(Some(source))),
|
|
809
966
|
closed: Arc::new(AtomicBool::new(false)),
|
|
967
|
+
path_cache: Arc::new(Mutex::new(VecDeque::with_capacity(PATH_CACHE_MAX_ENTRIES))),
|
|
810
968
|
ip_version,
|
|
811
969
|
}
|
|
812
970
|
}
|
|
813
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
|
+
|
|
814
1113
|
/// Parse IP address from Ruby value (String or IPAddr) - optimized version
|
|
815
1114
|
#[inline(always)]
|
|
816
1115
|
fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
@@ -825,12 +1124,7 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
|
|
|
825
1124
|
)
|
|
826
1125
|
})?;
|
|
827
1126
|
|
|
828
|
-
return
|
|
829
|
-
Error::new(
|
|
830
|
-
ruby.exception_arg_error(),
|
|
831
|
-
format!("'{}' does not appear to be an IPv4 or IPv6 address", ip_str),
|
|
832
|
-
)
|
|
833
|
-
});
|
|
1127
|
+
return parse_ip_string(ip_str, ruby);
|
|
834
1128
|
}
|
|
835
1129
|
|
|
836
1130
|
// Slow path: Try as IPAddr object
|
|
@@ -861,15 +1155,7 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
|
|
|
861
1155
|
}
|
|
862
1156
|
|
|
863
1157
|
if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
|
|
864
|
-
return
|
|
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
|
-
});
|
|
1158
|
+
return parse_ip_string(&ipaddr_obj, ruby);
|
|
873
1159
|
}
|
|
874
1160
|
|
|
875
1161
|
Err(Error::new(
|
|
@@ -878,14 +1164,126 @@ fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Er
|
|
|
878
1164
|
))
|
|
879
1165
|
}
|
|
880
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
|
+
|
|
881
1268
|
/// Generate error message for IPv6 in IPv4-only database
|
|
882
1269
|
fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
|
|
883
1270
|
format!(
|
|
884
|
-
"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.",
|
|
885
1272
|
ip
|
|
886
1273
|
)
|
|
887
1274
|
}
|
|
888
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
|
+
|
|
889
1287
|
/// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
|
|
890
1288
|
fn open_database_mmap(path: &str) -> Result<Reader, Error> {
|
|
891
1289
|
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
@@ -898,14 +1296,10 @@ fn open_database_mmap(path: &str) -> Result<Reader, Error> {
|
|
|
898
1296
|
)
|
|
899
1297
|
})?;
|
|
900
1298
|
let reader = MaxMindReader::from_source(mmap).map_err(|_| {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
906
|
-
path
|
|
907
|
-
),
|
|
908
|
-
)
|
|
1299
|
+
invalid_database_exception(&format!(
|
|
1300
|
+
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
1301
|
+
path
|
|
1302
|
+
))
|
|
909
1303
|
})?;
|
|
910
1304
|
|
|
911
1305
|
Ok(create_reader(ReaderSource::Mmap(reader)))
|
|
@@ -924,16 +1318,25 @@ fn open_database_memory(path: &str) -> Result<Reader, Error> {
|
|
|
924
1318
|
)
|
|
925
1319
|
})?;
|
|
926
1320
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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()))?;
|
|
937
1340
|
|
|
938
1341
|
Ok(create_reader(ReaderSource::Memory(reader)))
|
|
939
1342
|
}
|
|
@@ -971,6 +1374,14 @@ fn invalid_database_error() -> RClass {
|
|
|
971
1374
|
.expect("InvalidDatabaseError class should exist")
|
|
972
1375
|
}
|
|
973
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
|
+
|
|
974
1385
|
fn rust_module(ruby: &magnus::Ruby) -> RModule {
|
|
975
1386
|
let maxmind = ruby
|
|
976
1387
|
.class_object()
|
|
@@ -1055,13 +1466,17 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
|
1055
1466
|
let reader_class = rust.define_class("Reader", ruby.class_object())?;
|
|
1056
1467
|
reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
|
|
1057
1468
|
reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
|
|
1469
|
+
reader_class.define_method("get_path", magnus::method!(Reader::get_path, 2))?;
|
|
1058
1470
|
reader_class.define_method(
|
|
1059
1471
|
"get_with_prefix_length",
|
|
1060
1472
|
magnus::method!(Reader::get_with_prefix_length, 1),
|
|
1061
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))?;
|
|
1062
1476
|
reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
|
|
1063
1477
|
reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
|
|
1064
1478
|
reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
|
|
1479
|
+
reader_class.define_method("inspect", magnus::method!(Reader::inspect, 0))?;
|
|
1065
1480
|
reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
|
|
1066
1481
|
|
|
1067
1482
|
// Include Enumerable module
|
|
@@ -1096,8 +1511,49 @@ fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
|
1096
1511
|
|
|
1097
1512
|
// Define MODE constants
|
|
1098
1513
|
rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
|
|
1514
|
+
rust.const_set("MODE_FILE", ruby.to_symbol("MODE_FILE"))?;
|
|
1099
1515
|
rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
|
|
1100
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
|
+
)?;
|
|
1101
1521
|
|
|
1102
1522
|
Ok(())
|
|
1103
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
|
+
}
|
data/lib/maxmind/db/rust.rb
CHANGED
|
@@ -66,7 +66,7 @@ module MaxMind
|
|
|
66
66
|
# - Reader class
|
|
67
67
|
# - Metadata class
|
|
68
68
|
# - InvalidDatabaseError exception
|
|
69
|
-
# - MODE_AUTO, MODE_MEMORY, MODE_MMAP constants
|
|
69
|
+
# - MODE_AUTO, MODE_FILE, MODE_MEMORY, MODE_MMAP, MODE_PARAM_IS_BUFFER constants
|
|
70
70
|
end
|
|
71
71
|
else
|
|
72
72
|
# Official gem not loaded - define DB as a module
|
|
@@ -128,7 +128,7 @@ module MaxMind
|
|
|
128
128
|
# - Reader class
|
|
129
129
|
# - Metadata class
|
|
130
130
|
# - InvalidDatabaseError exception
|
|
131
|
-
# - MODE_AUTO, MODE_MEMORY, MODE_MMAP constants
|
|
131
|
+
# - MODE_AUTO, MODE_FILE, MODE_MEMORY, MODE_MMAP, MODE_PARAM_IS_BUFFER constants
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: maxmind-db-rust
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gregory Oschwald
|
|
@@ -191,7 +191,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
191
191
|
- !ruby/object:Gem::Version
|
|
192
192
|
version: '0'
|
|
193
193
|
requirements: []
|
|
194
|
-
rubygems_version: 4.0.
|
|
194
|
+
rubygems_version: 4.0.10
|
|
195
195
|
specification_version: 4
|
|
196
196
|
summary: Unofficial high-performance Rust-based MaxMind DB reader for Ruby
|
|
197
197
|
test_files: []
|