maxmind-db-rust 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +29 -1
- data/ext/maxmind_db_rust/Cargo.toml +1 -1
- data/ext/maxmind_db_rust/src/decoder.rs +324 -0
- data/ext/maxmind_db_rust/src/initialization.rs +75 -0
- data/ext/maxmind_db_rust/src/lib.rs +12 -1540
- data/ext/maxmind_db_rust/src/metadata.rs +123 -0
- data/ext/maxmind_db_rust/src/path.rs +157 -0
- data/ext/maxmind_db_rust/src/reader.rs +940 -0
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4da26d1a897424541b516fa63b4b5bc7a29c22f22b287f1da695464fe3f65dfb
|
|
4
|
+
data.tar.gz: f6bb57652b26da3cd5639f853be1ccd8cd6eac8c5d8f928d2220a41b76961ec2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8cdcdff702b4f7fbe6c01900f9d623038994ce1fbd7c4ce202689ee08432ff4e58c6cc269459e3fd2a9680bdee22f88ee10981a66d92679780bf2cce631246b
|
|
7
|
+
data.tar.gz: f5e1f10ea7ba02421a16d7eec91ff3435eaad367c9fd97d345105a769e7aec00fcc991e9e6ac85c682d5ec4f6ddfabf10cdc2cee55be1d29c05aa3ecb7700ead
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,64 @@ 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.6.0] - 2026-07-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added `Reader#verify` for opt-in comprehensive database integrity checks
|
|
13
|
+
using the verifier introduced by `maxminddb` 0.30.0.
|
|
14
|
+
- Added iteration and shared-reader thread-scaling benchmarks, including a
|
|
15
|
+
cache-root retention check across thread churn.
|
|
16
|
+
|
|
17
|
+
### Performance
|
|
18
|
+
|
|
19
|
+
- Batched Ruby array and hash insertion while decoding MMDB records to reduce
|
|
20
|
+
protected Ruby C API calls during full-record lookups.
|
|
21
|
+
- Replaced per-thread string caches with one fixed-size process cache that uses
|
|
22
|
+
its Ruby root array directly, improving reuse while bounding retained memory.
|
|
23
|
+
- Batched `get_many` and `get_many_path` result construction to reduce Ruby
|
|
24
|
+
array insertion overhead for large input collections.
|
|
25
|
+
- Improved `IPAddr` lookup performance by converting its public integer
|
|
26
|
+
representation directly instead of allocating packed address strings.
|
|
27
|
+
- Avoided allocating a temporary candidate vector on parsed-path cache hits.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- Upgraded the `maxminddb` crate to 0.30.0, improving selective path decoding
|
|
32
|
+
and hardening iteration and verification of corrupt databases.
|
|
33
|
+
- Documented selective lookup and input-type performance guidance, and
|
|
34
|
+
clarified that thread-safe lookups remain serialized by MRI's global VM lock.
|
|
35
|
+
- Documented the file-lifecycle safety contract for memory-mapped readers and
|
|
36
|
+
the requirement to atomically replace database paths during updates.
|
|
37
|
+
- Simplified reader ownership by removing redundant outer `Arc` wrappers and a
|
|
38
|
+
duplicate closed flag while preserving atomic close semantics.
|
|
39
|
+
- Removed the redundant extension-level Cargo lockfile; workspace builds use
|
|
40
|
+
the root lockfile as the single dependency lock.
|
|
41
|
+
- Moved MMDB-to-Ruby decoding and string-cache logic into a focused Rust module.
|
|
42
|
+
- Moved metadata storage and Ruby method registration into a focused Rust
|
|
43
|
+
module.
|
|
44
|
+
- Moved parsed-path ownership, hashing, matching, and conversion into a focused
|
|
45
|
+
Rust module.
|
|
46
|
+
- Moved reader behavior and Ruby method registration into a focused Rust module,
|
|
47
|
+
leaving the crate root responsible only for extension initialization.
|
|
48
|
+
- Moved extension namespace and cache-root setup into a focused initialization
|
|
49
|
+
module.
|
|
50
|
+
- Alternated baseline and candidate subprocesses between benchmark samples to
|
|
51
|
+
reduce order and thermal bias in git-ref comparisons.
|
|
52
|
+
- Added fixed-record and prebuilt-`IPAddr` benchmark cases for cache-hot and
|
|
53
|
+
input-conversion performance comparisons.
|
|
54
|
+
- Extended allocation benchmarking to cover selective batch lookups.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Made decoded MMDB strings consistently frozen regardless of whether their
|
|
59
|
+
length qualifies for the bounded string cache.
|
|
60
|
+
- Decoded MMDB UTF-8 strings and map keys directly into Ruby strings from raw
|
|
61
|
+
bytes, avoiding unchecked Rust strings while preserving Ruby's behavior for
|
|
62
|
+
invalid UTF-8 in corrupt databases.
|
|
63
|
+
- Propagated Ruby hash insertion errors from `Metadata#description` instead of
|
|
64
|
+
silently returning a partial description.
|
|
65
|
+
|
|
8
66
|
## [0.5.0] - 2026-06-14
|
|
9
67
|
|
|
10
68
|
### Added
|
|
@@ -156,3 +214,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
156
214
|
|
|
157
215
|
- MODE_FILE support (use MODE_MMAP instead)
|
|
158
216
|
- File descriptor support in constructor
|
|
217
|
+
|
|
218
|
+
[0.6.0]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.5.0...v0.6.0
|
|
219
|
+
[0.5.0]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.4.0...v0.5.0
|
|
220
|
+
[0.4.0]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.3.0...v0.4.0
|
|
221
|
+
[0.3.0]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.2.1...v0.3.0
|
|
222
|
+
[0.2.1]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.2.0...v0.2.1
|
|
223
|
+
[0.2.0]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.1.4...v0.2.0
|
|
224
|
+
[0.1.4]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.1.3...v0.1.4
|
|
225
|
+
[0.1.3]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.1.2...v0.1.3
|
|
226
|
+
[0.1.2]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.1.1...v0.1.2
|
|
227
|
+
[0.1.1]: https://github.com/oschwald/maxmind-db-rust-ruby/compare/v0.1.0...v0.1.1
|
|
228
|
+
[0.1.0]: https://github.com/oschwald/maxmind-db-rust-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -102,6 +102,9 @@ iso_codes = reader.get_many_path(ips, ['country', 'iso_code'])
|
|
|
102
102
|
reader.close
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
+
Decoded MMDB strings, including hash keys, are frozen. Call `dup` before
|
|
106
|
+
modifying a decoded string.
|
|
107
|
+
|
|
105
108
|
### Using IPAddr Objects
|
|
106
109
|
|
|
107
110
|
```ruby
|
|
@@ -153,6 +156,11 @@ reader = MaxMind::DB::Rust::Reader.new(
|
|
|
153
156
|
)
|
|
154
157
|
```
|
|
155
158
|
|
|
159
|
+
`MODE_AUTO`, `MODE_FILE`, and `MODE_MMAP` require the underlying file not to be
|
|
160
|
+
modified or truncated while the reader is alive. To refresh a database, write
|
|
161
|
+
the replacement to a new file and atomically rename it over the old path. Use
|
|
162
|
+
`MODE_MEMORY` if the file lifecycle cannot be controlled this way.
|
|
163
|
+
|
|
156
164
|
### Accessing Metadata
|
|
157
165
|
|
|
158
166
|
```ruby
|
|
@@ -290,6 +298,18 @@ Get metadata about the database.
|
|
|
290
298
|
|
|
291
299
|
**Returns:** `MaxMind::DB::Rust::Metadata` instance
|
|
292
300
|
|
|
301
|
+
#### `verify()`
|
|
302
|
+
|
|
303
|
+
Perform a comprehensive validation of the database metadata, search tree, data
|
|
304
|
+
section separator, and referenced data records. Verification traverses the
|
|
305
|
+
entire database and may use memory proportional to the number of distinct
|
|
306
|
+
referenced values, so it is intended for explicit integrity checks rather than
|
|
307
|
+
the lookup hot path.
|
|
308
|
+
|
|
309
|
+
**Returns:** `true` when the database passes verification
|
|
310
|
+
|
|
311
|
+
**Raises:** `MaxMind::DB::Rust::InvalidDatabaseError` when verification fails
|
|
312
|
+
|
|
293
313
|
#### `close()`
|
|
294
314
|
|
|
295
315
|
Close the database and release resources.
|
|
@@ -365,8 +385,16 @@ Lookup performance depends on hardware, Ruby version, database, and workload.
|
|
|
365
385
|
|
|
366
386
|
- In this project’s random-lookup benchmarks, this gem is consistently faster than the official Ruby implementation.
|
|
367
387
|
- `MODE_MMAP` and `MODE_MEMORY` both perform well; which is faster can vary by environment.
|
|
388
|
+
- Prefer `get_path` or `get_many_path` when only a small part of a record is
|
|
389
|
+
needed. Selective decoding avoids constructing the rest of the Ruby object
|
|
390
|
+
graph and can be substantially faster than a full-record lookup.
|
|
391
|
+
- Textual IP address strings use the fastest input path. `IPAddr` objects are
|
|
392
|
+
supported but require Ruby method calls to extract their address family and
|
|
393
|
+
integer value.
|
|
368
394
|
- For current, reproducible numbers on your own data and Ruby version, run `benchmark/compare_lookups.rb` against your database.
|
|
369
|
-
-
|
|
395
|
+
- Readers are safe to share across Ruby threads. Ruby-facing lookup and decode
|
|
396
|
+
work currently runs under MRI's global VM lock, so additional Ruby threads do
|
|
397
|
+
not make one process perform lookups in parallel.
|
|
370
398
|
|
|
371
399
|
## Development
|
|
372
400
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
rust_module, STRING_CACHE_MAX, STRING_CACHE_MAX_LEN, STRING_CACHE_MIN_LEN,
|
|
3
|
+
STRING_CACHE_ROOTS_CONST,
|
|
4
|
+
};
|
|
5
|
+
use ::maxminddb as maxminddb_crate;
|
|
6
|
+
use magnus::{prelude::*, IntoValue, RArray, RString, Value};
|
|
7
|
+
use rustc_hash::FxHasher;
|
|
8
|
+
use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
|
|
9
|
+
use std::{
|
|
10
|
+
cell::OnceCell,
|
|
11
|
+
fmt,
|
|
12
|
+
hash::{Hash, Hasher},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
thread_local! {
|
|
16
|
+
static STRING_CACHE_ROOTS: OnceCell<RArray> = const { OnceCell::new() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[inline]
|
|
20
|
+
fn global_string_cache_roots(ruby: &magnus::Ruby) -> RArray {
|
|
21
|
+
let value = rust_module(ruby)
|
|
22
|
+
.const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
|
|
23
|
+
.expect("string cache roots constant should exist");
|
|
24
|
+
RArray::from_value(value).expect("string cache roots constant should be an array")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[inline]
|
|
28
|
+
fn string_cache_roots(ruby: &magnus::Ruby) -> RArray {
|
|
29
|
+
STRING_CACHE_ROOTS.with(|roots| *roots.get_or_init(|| global_string_cache_roots(ruby)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#[inline]
|
|
33
|
+
fn cached_utf8_string(ruby: &magnus::Ruby, value: &[u8]) -> Value {
|
|
34
|
+
if !(STRING_CACHE_MIN_LEN..=STRING_CACHE_MAX_LEN).contains(&value.len()) {
|
|
35
|
+
let string = ruby.enc_str_new(value, ruby.utf8_encoding());
|
|
36
|
+
string.freeze();
|
|
37
|
+
return string.into_value_with(ruby);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let mut hasher = FxHasher::default();
|
|
41
|
+
value.hash(&mut hasher);
|
|
42
|
+
let hash = hasher.finish();
|
|
43
|
+
let slot = (hash as usize) & (STRING_CACHE_MAX - 1);
|
|
44
|
+
|
|
45
|
+
let roots = string_cache_roots(ruby);
|
|
46
|
+
let cached = roots
|
|
47
|
+
.entry::<Value>(slot as isize)
|
|
48
|
+
.expect("string cache roots lookup should succeed");
|
|
49
|
+
if let Some(cached) = RString::from_value(cached) {
|
|
50
|
+
// SAFETY: the bytes are compared immediately while the globally rooted
|
|
51
|
+
// frozen Ruby string remains alive and cannot be mutated.
|
|
52
|
+
if unsafe { cached.as_slice() } == value {
|
|
53
|
+
return cached.into_value_with(ruby);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let string = ruby.enc_str_new(value, ruby.utf8_encoding());
|
|
58
|
+
string.freeze();
|
|
59
|
+
let cached = string.as_value();
|
|
60
|
+
roots
|
|
61
|
+
.store(slot as isize, cached)
|
|
62
|
+
.expect("string cache roots update should succeed");
|
|
63
|
+
cached
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Wrapper that owns the Ruby value produced by deserializing a MaxMind record.
|
|
67
|
+
#[derive(Clone)]
|
|
68
|
+
pub(crate) struct RubyDecodedValue {
|
|
69
|
+
value: Value,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
impl RubyDecodedValue {
|
|
73
|
+
#[inline]
|
|
74
|
+
fn new(value: Value) -> Self {
|
|
75
|
+
Self { value }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#[inline]
|
|
79
|
+
pub(crate) fn into_value(self) -> Value {
|
|
80
|
+
self.value
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
impl<'de> Deserialize<'de> for RubyDecodedValue {
|
|
85
|
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
86
|
+
where
|
|
87
|
+
D: Deserializer<'de>,
|
|
88
|
+
{
|
|
89
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in deserializer");
|
|
90
|
+
RubyValueSeed { ruby: &ruby }.deserialize(deserializer)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
struct RubyValueSeed<'ruby> {
|
|
95
|
+
ruby: &'ruby magnus::Ruby,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
impl<'ruby, 'de> DeserializeSeed<'de> for RubyValueSeed<'ruby> {
|
|
99
|
+
type Value = RubyDecodedValue;
|
|
100
|
+
|
|
101
|
+
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
102
|
+
where
|
|
103
|
+
D: Deserializer<'de>,
|
|
104
|
+
{
|
|
105
|
+
maxminddb_crate::deserialize_any_with_raw_strings(
|
|
106
|
+
deserializer,
|
|
107
|
+
RubyValueVisitor { ruby: self.ruby },
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
struct RubyValueVisitor<'ruby> {
|
|
113
|
+
ruby: &'ruby magnus::Ruby,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
|
|
117
|
+
type Value = RubyDecodedValue;
|
|
118
|
+
|
|
119
|
+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
120
|
+
formatter.write_str("any valid MaxMind DB value")
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
|
124
|
+
where
|
|
125
|
+
E: de::Error,
|
|
126
|
+
{
|
|
127
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
|
|
131
|
+
where
|
|
132
|
+
E: de::Error,
|
|
133
|
+
{
|
|
134
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
|
138
|
+
where
|
|
139
|
+
E: de::Error,
|
|
140
|
+
{
|
|
141
|
+
if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
|
|
142
|
+
Ok(RubyDecodedValue::new(
|
|
143
|
+
(value as i32).into_value_with(self.ruby),
|
|
144
|
+
))
|
|
145
|
+
} else {
|
|
146
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
|
|
151
|
+
where
|
|
152
|
+
E: de::Error,
|
|
153
|
+
{
|
|
154
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
|
|
158
|
+
where
|
|
159
|
+
E: de::Error,
|
|
160
|
+
{
|
|
161
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
|
165
|
+
where
|
|
166
|
+
E: de::Error,
|
|
167
|
+
{
|
|
168
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
|
|
172
|
+
where
|
|
173
|
+
E: de::Error,
|
|
174
|
+
{
|
|
175
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
|
|
179
|
+
where
|
|
180
|
+
E: de::Error,
|
|
181
|
+
{
|
|
182
|
+
Ok(RubyDecodedValue::new(
|
|
183
|
+
(value as f64).into_value_with(self.ruby),
|
|
184
|
+
))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
|
|
188
|
+
where
|
|
189
|
+
E: de::Error,
|
|
190
|
+
{
|
|
191
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
195
|
+
where
|
|
196
|
+
D: Deserializer<'de>,
|
|
197
|
+
{
|
|
198
|
+
deserializer
|
|
199
|
+
.deserialize_bytes(RubyUtf8StringVisitor { ruby: self.ruby })
|
|
200
|
+
.map(RubyDecodedValue::new)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
204
|
+
where
|
|
205
|
+
E: de::Error,
|
|
206
|
+
{
|
|
207
|
+
Ok(RubyDecodedValue::new(cached_utf8_string(
|
|
208
|
+
self.ruby,
|
|
209
|
+
value.as_bytes(),
|
|
210
|
+
)))
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
|
214
|
+
where
|
|
215
|
+
E: de::Error,
|
|
216
|
+
{
|
|
217
|
+
Ok(RubyDecodedValue::new(cached_utf8_string(
|
|
218
|
+
self.ruby,
|
|
219
|
+
value.as_bytes(),
|
|
220
|
+
)))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
|
|
224
|
+
where
|
|
225
|
+
E: de::Error,
|
|
226
|
+
{
|
|
227
|
+
Ok(RubyDecodedValue::new(
|
|
228
|
+
self.ruby.str_from_slice(value).into_value_with(self.ruby),
|
|
229
|
+
))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
|
|
233
|
+
where
|
|
234
|
+
E: de::Error,
|
|
235
|
+
{
|
|
236
|
+
Ok(RubyDecodedValue::new(
|
|
237
|
+
self.ruby.str_from_slice(&value).into_value_with(self.ruby),
|
|
238
|
+
))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
|
242
|
+
where
|
|
243
|
+
A: SeqAccess<'de>,
|
|
244
|
+
{
|
|
245
|
+
let arr = match seq.size_hint() {
|
|
246
|
+
Some(cap) => self.ruby.ary_new_capa(cap),
|
|
247
|
+
None => self.ruby.ary_new(),
|
|
248
|
+
};
|
|
249
|
+
let mut buffer = [self.ruby.qnil().as_value(); 128];
|
|
250
|
+
let mut buffer_len = 0;
|
|
251
|
+
while let Some(elem) = seq.next_element_seed(RubyValueSeed { ruby: self.ruby })? {
|
|
252
|
+
buffer[buffer_len] = elem.into_value();
|
|
253
|
+
buffer_len += 1;
|
|
254
|
+
if buffer_len == buffer.len() {
|
|
255
|
+
arr.cat(&buffer)
|
|
256
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
257
|
+
buffer_len = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
arr.cat(&buffer[..buffer_len])
|
|
261
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
262
|
+
Ok(RubyDecodedValue::new(arr.into_value_with(self.ruby)))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
266
|
+
where
|
|
267
|
+
A: MapAccess<'de>,
|
|
268
|
+
{
|
|
269
|
+
let hash = match map.size_hint() {
|
|
270
|
+
Some(cap) => self.ruby.hash_new_capa(cap),
|
|
271
|
+
None => self.ruby.hash_new(),
|
|
272
|
+
};
|
|
273
|
+
let mut buffer = [self.ruby.qnil().as_value(); 128];
|
|
274
|
+
let mut buffer_len = 0;
|
|
275
|
+
while let Some(key_val) = map.next_key_seed(RubyMapKeySeed { ruby: self.ruby })? {
|
|
276
|
+
let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
|
|
277
|
+
buffer[buffer_len] = key_val;
|
|
278
|
+
buffer[buffer_len + 1] = value.into_value();
|
|
279
|
+
buffer_len += 2;
|
|
280
|
+
if buffer_len == buffer.len() {
|
|
281
|
+
hash.bulk_insert(&buffer)
|
|
282
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
283
|
+
buffer_len = 0;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
hash.bulk_insert(&buffer[..buffer_len])
|
|
287
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
288
|
+
Ok(RubyDecodedValue::new(hash.into_value_with(self.ruby)))
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
struct RubyUtf8StringVisitor<'ruby> {
|
|
293
|
+
ruby: &'ruby magnus::Ruby,
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
impl<'de, 'ruby> Visitor<'de> for RubyUtf8StringVisitor<'ruby> {
|
|
297
|
+
type Value = Value;
|
|
298
|
+
|
|
299
|
+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
300
|
+
formatter.write_str("MMDB UTF-8 string bytes")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fn visit_borrowed_bytes<E>(self, value: &'de [u8]) -> Result<Self::Value, E>
|
|
304
|
+
where
|
|
305
|
+
E: de::Error,
|
|
306
|
+
{
|
|
307
|
+
Ok(cached_utf8_string(self.ruby, value))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
struct RubyMapKeySeed<'ruby> {
|
|
312
|
+
ruby: &'ruby magnus::Ruby,
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
impl<'ruby, 'de> DeserializeSeed<'de> for RubyMapKeySeed<'ruby> {
|
|
316
|
+
type Value = Value;
|
|
317
|
+
|
|
318
|
+
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
319
|
+
where
|
|
320
|
+
D: Deserializer<'de>,
|
|
321
|
+
{
|
|
322
|
+
deserializer.deserialize_identifier(RubyUtf8StringVisitor { ruby: self.ruby })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
use crate::{metadata, reader, STRING_CACHE_MAX, STRING_CACHE_ROOTS_CONST};
|
|
2
|
+
use magnus::{error::Error, prelude::*, RModule, Value};
|
|
3
|
+
|
|
4
|
+
const MAP_KEY_ROOTS_CONST: &str = "__MAP_KEY_ROOTS__";
|
|
5
|
+
|
|
6
|
+
pub(crate) fn initialize(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
7
|
+
// Define module hierarchy: MaxMind::DB::Rust
|
|
8
|
+
// Handle case where official maxmind-db gem may have already defined MaxMind::DB as a Class
|
|
9
|
+
let maxmind = ruby.define_module("MaxMind")?;
|
|
10
|
+
|
|
11
|
+
// Try to get or define DB - it might be a Class (official gem) or Module (ours)
|
|
12
|
+
let db_value = maxmind.const_get::<_, Value>("DB");
|
|
13
|
+
let rust = match db_value {
|
|
14
|
+
Ok(existing) if existing.is_kind_of(ruby.class_class()) => {
|
|
15
|
+
// MaxMind::DB exists as a Class (official gem loaded first)
|
|
16
|
+
// Reuse existing Rust constant if present to avoid replacing classes.
|
|
17
|
+
if let Ok(rust_value) = existing.funcall::<_, _, Value>("const_get", ("Rust", false)) {
|
|
18
|
+
RModule::from_value(rust_value).ok_or_else(|| {
|
|
19
|
+
Error::new(
|
|
20
|
+
ruby.exception_type_error(),
|
|
21
|
+
"MaxMind::DB::Rust exists but is not a module",
|
|
22
|
+
)
|
|
23
|
+
})?
|
|
24
|
+
} else {
|
|
25
|
+
// Define Rust module directly as a constant on the class.
|
|
26
|
+
let rust_value: Value = ruby.module_new().as_value();
|
|
27
|
+
let rust_mod = RModule::from_value(rust_value).ok_or_else(|| {
|
|
28
|
+
Error::new(
|
|
29
|
+
ruby.exception_type_error(),
|
|
30
|
+
"Failed to create anonymous module for MaxMind::DB::Rust",
|
|
31
|
+
)
|
|
32
|
+
})?;
|
|
33
|
+
let _ = existing.funcall::<_, _, Value>("const_set", ("Rust", rust_mod))?;
|
|
34
|
+
rust_mod
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
Ok(existing) => {
|
|
38
|
+
// MaxMind::DB exists as a Module (our gem loaded first)
|
|
39
|
+
let db_mod = RModule::from_value(existing).ok_or_else(|| {
|
|
40
|
+
Error::new(ruby.exception_type_error(), "MaxMind::DB is not a module")
|
|
41
|
+
})?;
|
|
42
|
+
db_mod.define_module("Rust")?
|
|
43
|
+
}
|
|
44
|
+
Err(_) => {
|
|
45
|
+
// MaxMind::DB doesn't exist, define it as a module
|
|
46
|
+
let db = maxmind.define_module("DB")?;
|
|
47
|
+
db.define_module("Rust")?
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if rust
|
|
52
|
+
.const_get::<_, Value>(STRING_CACHE_ROOTS_CONST)
|
|
53
|
+
.is_err()
|
|
54
|
+
{
|
|
55
|
+
let roots = ruby.ary_new_capa(STRING_CACHE_MAX);
|
|
56
|
+
for _ in 0..STRING_CACHE_MAX {
|
|
57
|
+
roots.push(ruby.qnil().as_value())?;
|
|
58
|
+
}
|
|
59
|
+
rust.const_set(STRING_CACHE_ROOTS_CONST, roots)?;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if rust.const_get::<_, Value>(MAP_KEY_ROOTS_CONST).is_ok() {
|
|
63
|
+
let _ = rust.funcall::<_, _, Value>("send", ("remove_const", MAP_KEY_ROOTS_CONST))?;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The extension can be loaded more than once from different paths.
|
|
67
|
+
// Reusing previously defined classes avoids typed-data incompatibilities.
|
|
68
|
+
if rust.const_get::<_, Value>("Reader").is_ok() {
|
|
69
|
+
return Ok(());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
reader::define(ruby, rust)?;
|
|
73
|
+
metadata::define(ruby, rust)?;
|
|
74
|
+
Ok(())
|
|
75
|
+
}
|