maxmind-db-rust 0.1.2
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 +7 -0
- data/CHANGELOG.md +52 -0
- data/CONTRIBUTING.md +496 -0
- data/LICENSE +15 -0
- data/README.md +343 -0
- data/ext/maxmind_db_rust/Cargo.toml +25 -0
- data/ext/maxmind_db_rust/extconf.rb +5 -0
- data/ext/maxmind_db_rust/lib/maxmind/db/rust.rb +16 -0
- data/ext/maxmind_db_rust/src/lib.rs +970 -0
- data/lib/maxmind/db/rust.rb +135 -0
- metadata +183 -0
|
@@ -0,0 +1,970 @@
|
|
|
1
|
+
// SAFETY: the `maxminddb` crate is built with the `unsafe-str-decode` feature enabled.
|
|
2
|
+
// Ruby validates UTF-8 when we construct `RString`s, so skipping the redundant check in
|
|
3
|
+
// the decoder is safe and avoids re-validating every string record twice.
|
|
4
|
+
use ::maxminddb as maxminddb_crate;
|
|
5
|
+
use arc_swap::ArcSwapOption;
|
|
6
|
+
use ipnetwork::IpNetwork;
|
|
7
|
+
use magnus::{
|
|
8
|
+
error::Error, prelude::*, scan_args::get_kwargs, scan_args::scan_args, value::Lazy,
|
|
9
|
+
ExceptionClass, IntoValue, RArray, RClass, RHash, RModule, RString, Symbol, Value,
|
|
10
|
+
};
|
|
11
|
+
use maxminddb_crate::{MaxMindDbError, Reader as MaxMindReader, Within, WithinItem};
|
|
12
|
+
use memmap2::Mmap;
|
|
13
|
+
use serde::de::{self, Deserialize, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
|
|
14
|
+
use std::{
|
|
15
|
+
borrow::Cow,
|
|
16
|
+
collections::BTreeMap,
|
|
17
|
+
fmt,
|
|
18
|
+
fs::File,
|
|
19
|
+
io::Read as IoRead,
|
|
20
|
+
net::IpAddr,
|
|
21
|
+
path::Path,
|
|
22
|
+
str::FromStr,
|
|
23
|
+
sync::{
|
|
24
|
+
atomic::{AtomicBool, Ordering},
|
|
25
|
+
Arc,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Error constants
|
|
30
|
+
const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
|
|
31
|
+
const ERR_BAD_DATA: &str =
|
|
32
|
+
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)";
|
|
33
|
+
|
|
34
|
+
macro_rules! define_interned_keys {
|
|
35
|
+
( $( $const_ident:ident => $str:expr ),* $(,)? ) => {
|
|
36
|
+
$(
|
|
37
|
+
static $const_ident: Lazy<RString> = Lazy::new(|ruby| {
|
|
38
|
+
let s = ruby.str_new($str);
|
|
39
|
+
s.freeze();
|
|
40
|
+
s
|
|
41
|
+
});
|
|
42
|
+
)*
|
|
43
|
+
|
|
44
|
+
fn interned_key(ruby: &magnus::Ruby, key: &str) -> Option<Value> {
|
|
45
|
+
match key {
|
|
46
|
+
$(
|
|
47
|
+
$str => Some(ruby.get_inner(&$const_ident).as_value()),
|
|
48
|
+
)*
|
|
49
|
+
_ => None,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
define_interned_keys!(
|
|
56
|
+
CITY_KEY => "city",
|
|
57
|
+
CONTINENT_KEY => "continent",
|
|
58
|
+
COUNTRY_KEY => "country",
|
|
59
|
+
REGISTERED_COUNTRY_KEY => "registered_country",
|
|
60
|
+
REPRESENTED_COUNTRY_KEY => "represented_country",
|
|
61
|
+
SUBDIVISIONS_KEY => "subdivisions",
|
|
62
|
+
LOCATION_KEY => "location",
|
|
63
|
+
POSTAL_KEY => "postal",
|
|
64
|
+
TRAITS_KEY => "traits",
|
|
65
|
+
NAMES_KEY => "names",
|
|
66
|
+
GEONAME_ID_KEY => "geoname_id",
|
|
67
|
+
ISO_CODE_KEY => "iso_code",
|
|
68
|
+
CONFIDENCE_KEY => "confidence",
|
|
69
|
+
ACCURACY_RADIUS_KEY => "accuracy_radius",
|
|
70
|
+
LATITUDE_KEY => "latitude",
|
|
71
|
+
LONGITUDE_KEY => "longitude",
|
|
72
|
+
TIME_ZONE_KEY => "time_zone",
|
|
73
|
+
METRO_CODE_KEY => "metro_code",
|
|
74
|
+
POPULATION_DENSITY_KEY => "population_density",
|
|
75
|
+
EN_KEY => "en",
|
|
76
|
+
ES_KEY => "es",
|
|
77
|
+
FR_KEY => "fr",
|
|
78
|
+
JA_KEY => "ja",
|
|
79
|
+
PT_BR_KEY => "pt-BR",
|
|
80
|
+
RU_KEY => "ru",
|
|
81
|
+
ZH_CN_KEY => "zh-CN",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
/// Wrapper that owns the Ruby value produced by deserializing a MaxMind record
|
|
85
|
+
#[derive(Clone)]
|
|
86
|
+
struct RubyDecodedValue {
|
|
87
|
+
value: Value,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl RubyDecodedValue {
|
|
91
|
+
#[inline]
|
|
92
|
+
fn new(value: Value) -> Self {
|
|
93
|
+
Self { value }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[inline]
|
|
97
|
+
fn into_value(self) -> Value {
|
|
98
|
+
self.value
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
impl<'de> Deserialize<'de> for RubyDecodedValue {
|
|
103
|
+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
104
|
+
where
|
|
105
|
+
D: Deserializer<'de>,
|
|
106
|
+
{
|
|
107
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in deserializer");
|
|
108
|
+
RubyValueSeed { ruby: &ruby }.deserialize(deserializer)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
struct RubyValueSeed<'ruby> {
|
|
113
|
+
ruby: &'ruby magnus::Ruby,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl<'ruby, 'de> DeserializeSeed<'de> for RubyValueSeed<'ruby> {
|
|
117
|
+
type Value = RubyDecodedValue;
|
|
118
|
+
|
|
119
|
+
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
120
|
+
where
|
|
121
|
+
D: Deserializer<'de>,
|
|
122
|
+
{
|
|
123
|
+
deserializer.deserialize_any(RubyValueVisitor { ruby: self.ruby })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
struct RubyValueVisitor<'ruby> {
|
|
128
|
+
ruby: &'ruby magnus::Ruby,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
impl<'de, 'ruby> Visitor<'de> for RubyValueVisitor<'ruby> {
|
|
132
|
+
type Value = RubyDecodedValue;
|
|
133
|
+
|
|
134
|
+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
135
|
+
formatter.write_str("any valid MaxMind DB value")
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
|
|
139
|
+
where
|
|
140
|
+
E: de::Error,
|
|
141
|
+
{
|
|
142
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn visit_i32<E>(self, value: i32) -> Result<Self::Value, E>
|
|
146
|
+
where
|
|
147
|
+
E: de::Error,
|
|
148
|
+
{
|
|
149
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
|
|
153
|
+
where
|
|
154
|
+
E: de::Error,
|
|
155
|
+
{
|
|
156
|
+
if value >= i32::MIN as i64 && value <= i32::MAX as i64 {
|
|
157
|
+
Ok(RubyDecodedValue::new(
|
|
158
|
+
(value as i32).into_value_with(self.ruby),
|
|
159
|
+
))
|
|
160
|
+
} else {
|
|
161
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn visit_u16<E>(self, value: u16) -> Result<Self::Value, E>
|
|
166
|
+
where
|
|
167
|
+
E: de::Error,
|
|
168
|
+
{
|
|
169
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn visit_u32<E>(self, value: u32) -> Result<Self::Value, E>
|
|
173
|
+
where
|
|
174
|
+
E: de::Error,
|
|
175
|
+
{
|
|
176
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
|
|
180
|
+
where
|
|
181
|
+
E: de::Error,
|
|
182
|
+
{
|
|
183
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
fn visit_u128<E>(self, value: u128) -> Result<Self::Value, E>
|
|
187
|
+
where
|
|
188
|
+
E: de::Error,
|
|
189
|
+
{
|
|
190
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn visit_f32<E>(self, value: f32) -> Result<Self::Value, E>
|
|
194
|
+
where
|
|
195
|
+
E: de::Error,
|
|
196
|
+
{
|
|
197
|
+
Ok(RubyDecodedValue::new(
|
|
198
|
+
(value as f64).into_value_with(self.ruby),
|
|
199
|
+
))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
|
|
203
|
+
where
|
|
204
|
+
E: de::Error,
|
|
205
|
+
{
|
|
206
|
+
Ok(RubyDecodedValue::new(value.into_value_with(self.ruby)))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
|
210
|
+
where
|
|
211
|
+
E: de::Error,
|
|
212
|
+
{
|
|
213
|
+
let val = interned_key(self.ruby, value)
|
|
214
|
+
.unwrap_or_else(|| self.ruby.str_new(value).into_value_with(self.ruby));
|
|
215
|
+
Ok(RubyDecodedValue::new(val))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
|
|
219
|
+
where
|
|
220
|
+
E: de::Error,
|
|
221
|
+
{
|
|
222
|
+
let val = interned_key(self.ruby, &value)
|
|
223
|
+
.unwrap_or_else(|| self.ruby.str_new(&value).into_value_with(self.ruby));
|
|
224
|
+
Ok(RubyDecodedValue::new(val))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
|
|
228
|
+
where
|
|
229
|
+
E: de::Error,
|
|
230
|
+
{
|
|
231
|
+
Ok(RubyDecodedValue::new(
|
|
232
|
+
self.ruby.str_from_slice(value).into_value_with(self.ruby),
|
|
233
|
+
))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn visit_byte_buf<E>(self, value: Vec<u8>) -> Result<Self::Value, E>
|
|
237
|
+
where
|
|
238
|
+
E: de::Error,
|
|
239
|
+
{
|
|
240
|
+
Ok(RubyDecodedValue::new(
|
|
241
|
+
self.ruby.str_from_slice(&value).into_value_with(self.ruby),
|
|
242
|
+
))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
|
246
|
+
where
|
|
247
|
+
A: SeqAccess<'de>,
|
|
248
|
+
{
|
|
249
|
+
let arr = match seq.size_hint() {
|
|
250
|
+
Some(cap) => self.ruby.ary_new_capa(cap),
|
|
251
|
+
None => self.ruby.ary_new(),
|
|
252
|
+
};
|
|
253
|
+
while let Some(elem) = seq.next_element_seed(RubyValueSeed { ruby: self.ruby })? {
|
|
254
|
+
arr.push(elem.into_value())
|
|
255
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
256
|
+
}
|
|
257
|
+
Ok(RubyDecodedValue::new(arr.into_value_with(self.ruby)))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
|
261
|
+
where
|
|
262
|
+
A: MapAccess<'de>,
|
|
263
|
+
{
|
|
264
|
+
let hash = match map.size_hint() {
|
|
265
|
+
Some(cap) => self.ruby.hash_new_capa(cap),
|
|
266
|
+
None => self.ruby.hash_new(),
|
|
267
|
+
};
|
|
268
|
+
while let Some(key) = map.next_key::<Cow<'de, str>>()? {
|
|
269
|
+
let value = map.next_value_seed(RubyValueSeed { ruby: self.ruby })?;
|
|
270
|
+
let key_val = interned_key(self.ruby, key.as_ref())
|
|
271
|
+
.unwrap_or_else(|| self.ruby.str_new(key.as_ref()).into_value_with(self.ruby));
|
|
272
|
+
hash.aset(key_val, value.into_value())
|
|
273
|
+
.map_err(|e| de::Error::custom(e.to_string()))?;
|
|
274
|
+
}
|
|
275
|
+
Ok(RubyDecodedValue::new(hash.into_value_with(self.ruby)))
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/// Enum to handle different reader source types
|
|
280
|
+
enum ReaderSource {
|
|
281
|
+
Mmap(MaxMindReader<Mmap>),
|
|
282
|
+
Memory(MaxMindReader<Vec<u8>>),
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
impl ReaderSource {
|
|
286
|
+
#[inline]
|
|
287
|
+
fn lookup(
|
|
288
|
+
&self,
|
|
289
|
+
ip: IpAddr,
|
|
290
|
+
) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
|
|
291
|
+
match self {
|
|
292
|
+
ReaderSource::Mmap(reader) => reader.lookup(ip),
|
|
293
|
+
ReaderSource::Memory(reader) => reader.lookup(ip),
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[inline]
|
|
298
|
+
fn lookup_prefix(
|
|
299
|
+
&self,
|
|
300
|
+
ip: IpAddr,
|
|
301
|
+
) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
|
|
302
|
+
match self {
|
|
303
|
+
ReaderSource::Mmap(reader) => reader.lookup_prefix(ip),
|
|
304
|
+
ReaderSource::Memory(reader) => reader.lookup_prefix(ip),
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[inline]
|
|
309
|
+
fn metadata(&self) -> &maxminddb_crate::Metadata {
|
|
310
|
+
match self {
|
|
311
|
+
ReaderSource::Mmap(reader) => &reader.metadata,
|
|
312
|
+
ReaderSource::Memory(reader) => &reader.metadata,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
#[inline]
|
|
317
|
+
fn within(&self, network: IpNetwork) -> Result<ReaderWithin, MaxMindDbError> {
|
|
318
|
+
match self {
|
|
319
|
+
ReaderSource::Mmap(reader) => {
|
|
320
|
+
let iter = reader.within::<RubyDecodedValue>(network)?;
|
|
321
|
+
// SAFETY: the iterator holds a reference into `reader`. We'll store an Arc guard
|
|
322
|
+
// alongside it so the reader outlives the transmuted iterator.
|
|
323
|
+
Ok(ReaderWithin::Mmap(unsafe {
|
|
324
|
+
std::mem::transmute::<
|
|
325
|
+
Within<'_, RubyDecodedValue, Mmap>,
|
|
326
|
+
Within<'static, RubyDecodedValue, Mmap>,
|
|
327
|
+
>(iter)
|
|
328
|
+
}))
|
|
329
|
+
}
|
|
330
|
+
ReaderSource::Memory(reader) => {
|
|
331
|
+
let iter = reader.within::<RubyDecodedValue>(network)?;
|
|
332
|
+
// SAFETY: same as above, the Arc guard keeps the reader alive.
|
|
333
|
+
Ok(ReaderWithin::Memory(unsafe {
|
|
334
|
+
std::mem::transmute::<
|
|
335
|
+
Within<'_, RubyDecodedValue, Vec<u8>>,
|
|
336
|
+
Within<'static, RubyDecodedValue, Vec<u8>>,
|
|
337
|
+
>(iter)
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/// Wrapper enum for Within iterators
|
|
345
|
+
enum ReaderWithin {
|
|
346
|
+
Mmap(Within<'static, RubyDecodedValue, Mmap>),
|
|
347
|
+
Memory(Within<'static, RubyDecodedValue, Vec<u8>>),
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
impl ReaderWithin {
|
|
351
|
+
fn next(&mut self) -> Option<Result<WithinItem<RubyDecodedValue>, MaxMindDbError>> {
|
|
352
|
+
match self {
|
|
353
|
+
ReaderWithin::Mmap(iter) => iter.next(),
|
|
354
|
+
ReaderWithin::Memory(iter) => iter.next(),
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// Metadata about the MaxMind DB database
|
|
360
|
+
#[derive(Clone)]
|
|
361
|
+
#[magnus::wrap(class = "MaxMind::DB::Rust::Metadata")]
|
|
362
|
+
struct Metadata {
|
|
363
|
+
/// The major version number of the binary format used when creating the database.
|
|
364
|
+
binary_format_major_version: u16,
|
|
365
|
+
/// The minor version number of the binary format used when creating the database.
|
|
366
|
+
binary_format_minor_version: u16,
|
|
367
|
+
/// The Unix epoch timestamp for when the database was built.
|
|
368
|
+
build_epoch: u64,
|
|
369
|
+
/// A string identifying the database type (e.g., 'GeoIP2-City', 'GeoLite2-Country').
|
|
370
|
+
database_type: String,
|
|
371
|
+
description_map: BTreeMap<String, String>,
|
|
372
|
+
/// The IP version of the data in a database. A value of 4 means IPv4 only; 6 supports both IPv4 and IPv6.
|
|
373
|
+
ip_version: u16,
|
|
374
|
+
languages_list: Vec<String>,
|
|
375
|
+
/// The number of nodes in the search tree.
|
|
376
|
+
node_count: u32,
|
|
377
|
+
/// The record size in bits (24, 28, or 32).
|
|
378
|
+
record_size: u16,
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
impl Metadata {
|
|
382
|
+
fn binary_format_major_version(&self) -> u16 {
|
|
383
|
+
self.binary_format_major_version
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
fn binary_format_minor_version(&self) -> u16 {
|
|
387
|
+
self.binary_format_minor_version
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fn build_epoch(&self) -> u64 {
|
|
391
|
+
self.build_epoch
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
fn database_type(&self) -> String {
|
|
395
|
+
self.database_type.clone()
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
fn description(&self) -> RHash {
|
|
399
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
400
|
+
let hash = ruby.hash_new();
|
|
401
|
+
for (k, v) in &self.description_map {
|
|
402
|
+
let _ = hash.aset(k.as_str(), v.as_str());
|
|
403
|
+
}
|
|
404
|
+
hash
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
fn ip_version(&self) -> u16 {
|
|
408
|
+
self.ip_version
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
fn languages(&self) -> Vec<String> {
|
|
412
|
+
self.languages_list.clone()
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
fn node_count(&self) -> u32 {
|
|
416
|
+
self.node_count
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
fn record_size(&self) -> u16 {
|
|
420
|
+
self.record_size
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fn node_byte_size(&self) -> u16 {
|
|
424
|
+
self.record_size / 4
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
fn search_tree_size(&self) -> u32 {
|
|
428
|
+
self.node_count * (self.record_size as u32 / 4)
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
unsafe impl Send for Metadata {}
|
|
433
|
+
|
|
434
|
+
/// A Ruby wrapper around the MaxMind DB reader
|
|
435
|
+
#[derive(Clone)]
|
|
436
|
+
#[magnus::wrap(class = "MaxMind::DB::Rust::Reader")]
|
|
437
|
+
struct Reader {
|
|
438
|
+
reader: Arc<ArcSwapOption<ReaderSource>>,
|
|
439
|
+
closed: Arc<AtomicBool>,
|
|
440
|
+
ip_version: u16,
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
impl Reader {
|
|
444
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
445
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
446
|
+
|
|
447
|
+
let args = scan_args::<(String,), (), (), (), _, ()>(args)?;
|
|
448
|
+
let (database,) = args.required;
|
|
449
|
+
let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
|
|
450
|
+
let (mode,) = kw.optional;
|
|
451
|
+
|
|
452
|
+
// Parse mode from options hash
|
|
453
|
+
let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
|
|
454
|
+
|
|
455
|
+
let mode_str = mode.name()?;
|
|
456
|
+
let mode_str: &str = &mode_str;
|
|
457
|
+
|
|
458
|
+
// Determine actual mode to use
|
|
459
|
+
let actual_mode = match mode_str {
|
|
460
|
+
"MODE_AUTO" | "MODE_MMAP" => "MMAP",
|
|
461
|
+
"MODE_MEMORY" => "MEMORY",
|
|
462
|
+
_ => {
|
|
463
|
+
return Err(Error::new(
|
|
464
|
+
ruby.exception_arg_error(),
|
|
465
|
+
format!("Unsupported mode: {}", mode_str),
|
|
466
|
+
))
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// Open database with appropriate mode
|
|
471
|
+
match actual_mode {
|
|
472
|
+
"MMAP" => open_database_mmap(&database),
|
|
473
|
+
"MEMORY" => open_database_memory(&database),
|
|
474
|
+
_ => Err(Error::new(
|
|
475
|
+
ruby.exception_arg_error(),
|
|
476
|
+
format!("Invalid mode: {}", actual_mode),
|
|
477
|
+
)),
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
#[inline]
|
|
482
|
+
fn get(&self, ip_address: Value) -> Result<Value, Error> {
|
|
483
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
484
|
+
|
|
485
|
+
let reader = self.get_reader(&ruby)?;
|
|
486
|
+
|
|
487
|
+
// Parse IP address
|
|
488
|
+
let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
|
|
489
|
+
|
|
490
|
+
if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
|
|
491
|
+
return Err(Error::new(
|
|
492
|
+
ruby.exception_arg_error(),
|
|
493
|
+
ipv6_in_ipv4_error(&parsed_ip),
|
|
494
|
+
));
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Perform lookup
|
|
498
|
+
match reader.lookup(parsed_ip) {
|
|
499
|
+
Ok(Some(data)) => Ok(data.into_value()),
|
|
500
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
501
|
+
Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
|
|
502
|
+
Err(Error::new(
|
|
503
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
504
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
505
|
+
ERR_BAD_DATA,
|
|
506
|
+
))
|
|
507
|
+
}
|
|
508
|
+
Err(e) => Err(Error::new(
|
|
509
|
+
ruby.exception_runtime_error(),
|
|
510
|
+
format!("Database lookup failed: {}", e),
|
|
511
|
+
)),
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[inline]
|
|
516
|
+
fn get_with_prefix_length(&self, ip_address: Value) -> Result<RArray, Error> {
|
|
517
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
518
|
+
|
|
519
|
+
let reader = self.get_reader(&ruby)?;
|
|
520
|
+
|
|
521
|
+
// Parse IP address
|
|
522
|
+
let parsed_ip = parse_ip_address_fast(ip_address, &ruby)?;
|
|
523
|
+
|
|
524
|
+
if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
|
|
525
|
+
return Err(Error::new(
|
|
526
|
+
ruby.exception_arg_error(),
|
|
527
|
+
ipv6_in_ipv4_error(&parsed_ip),
|
|
528
|
+
));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Perform lookup with prefix
|
|
532
|
+
match reader.lookup_prefix(parsed_ip) {
|
|
533
|
+
Ok((Some(data), prefix)) => {
|
|
534
|
+
let arr = ruby.ary_new();
|
|
535
|
+
arr.push(data.into_value())?;
|
|
536
|
+
arr.push(prefix.into_value_with(&ruby))?;
|
|
537
|
+
Ok(arr)
|
|
538
|
+
}
|
|
539
|
+
Ok((None, prefix)) => {
|
|
540
|
+
let arr = ruby.ary_new();
|
|
541
|
+
arr.push(ruby.qnil().as_value())?;
|
|
542
|
+
arr.push(prefix.into_value_with(&ruby))?;
|
|
543
|
+
Ok(arr)
|
|
544
|
+
}
|
|
545
|
+
Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
|
|
546
|
+
Err(Error::new(
|
|
547
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
548
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
549
|
+
ERR_BAD_DATA,
|
|
550
|
+
))
|
|
551
|
+
}
|
|
552
|
+
Err(e) => Err(Error::new(
|
|
553
|
+
ruby.exception_runtime_error(),
|
|
554
|
+
format!("Database lookup failed: {}", e),
|
|
555
|
+
)),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
fn metadata(&self) -> Result<Metadata, Error> {
|
|
560
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
561
|
+
|
|
562
|
+
let reader = self.get_reader(&ruby)?;
|
|
563
|
+
let meta = reader.metadata();
|
|
564
|
+
|
|
565
|
+
Ok(Metadata {
|
|
566
|
+
binary_format_major_version: meta.binary_format_major_version,
|
|
567
|
+
binary_format_minor_version: meta.binary_format_minor_version,
|
|
568
|
+
build_epoch: meta.build_epoch,
|
|
569
|
+
database_type: meta.database_type.clone(),
|
|
570
|
+
description_map: meta.description.clone(),
|
|
571
|
+
ip_version: meta.ip_version,
|
|
572
|
+
languages_list: meta.languages.clone(),
|
|
573
|
+
node_count: meta.node_count,
|
|
574
|
+
record_size: meta.record_size,
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
fn close(&self) {
|
|
579
|
+
if self.closed.swap(true, Ordering::AcqRel) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
self.reader.store(None);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
fn closed(&self) -> bool {
|
|
586
|
+
self.closed.load(Ordering::Acquire)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
fn each(&self, args: &[Value]) -> Result<Value, Error> {
|
|
590
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
591
|
+
|
|
592
|
+
let reader = self.get_reader(&ruby)?;
|
|
593
|
+
|
|
594
|
+
// If no block given, return enumerator
|
|
595
|
+
if !ruby.block_given() {
|
|
596
|
+
return Err(Error::new(
|
|
597
|
+
ruby.exception_runtime_error(),
|
|
598
|
+
"Enumerator support not yet implemented, please provide a block",
|
|
599
|
+
));
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
let ip_version = reader.metadata().ip_version;
|
|
603
|
+
|
|
604
|
+
// Determine the network to iterate over
|
|
605
|
+
let network_str = if args.is_empty() {
|
|
606
|
+
// No argument: use default (full database)
|
|
607
|
+
if ip_version == 4 {
|
|
608
|
+
"0.0.0.0/0".to_string()
|
|
609
|
+
} else {
|
|
610
|
+
"::/0".to_string()
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
// Argument provided: extract network CIDR string
|
|
614
|
+
let network_arg = args[0];
|
|
615
|
+
|
|
616
|
+
// Try to get string representation
|
|
617
|
+
// Accept both String and IPAddr objects
|
|
618
|
+
let network_str_val = if let Ok(s) = RString::try_convert(network_arg) {
|
|
619
|
+
// It's already a string
|
|
620
|
+
s.to_string()?
|
|
621
|
+
} else {
|
|
622
|
+
// Check if it's an IPAddr object
|
|
623
|
+
let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
|
|
624
|
+
if network_arg.is_kind_of(ipaddr_class) {
|
|
625
|
+
// It's an IPAddr - need to get both address and prefix
|
|
626
|
+
let ip_str: String = network_arg.funcall("to_s", ())?;
|
|
627
|
+
|
|
628
|
+
// Get the prefix length from IPAddr
|
|
629
|
+
// IPAddr stores prefix as a netmask, need to convert
|
|
630
|
+
let prefix_len: u8 = network_arg.funcall("prefix", ())?;
|
|
631
|
+
|
|
632
|
+
// Construct CIDR notation
|
|
633
|
+
format!("{}/{}", ip_str, prefix_len)
|
|
634
|
+
} else {
|
|
635
|
+
// Try to call to_s on it (works for other objects)
|
|
636
|
+
let to_s_result: Value = network_arg.funcall("to_s", ())?;
|
|
637
|
+
RString::try_convert(to_s_result)
|
|
638
|
+
.map_err(|_| {
|
|
639
|
+
Error::new(
|
|
640
|
+
ruby.exception_arg_error(),
|
|
641
|
+
"Network parameter must be a String or IPAddr",
|
|
642
|
+
)
|
|
643
|
+
})?
|
|
644
|
+
.to_string()?
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
network_str_val
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
let network = IpNetwork::from_str(&network_str).map_err(|e| {
|
|
652
|
+
Error::new(
|
|
653
|
+
ruby.exception_arg_error(),
|
|
654
|
+
format!("Invalid network CIDR '{}': {}", network_str, e),
|
|
655
|
+
)
|
|
656
|
+
})?;
|
|
657
|
+
|
|
658
|
+
// Validate network matches database IP version
|
|
659
|
+
// IPv4 in IPv6 DB is OK (IPv4-mapped), IPv6 in IPv6 DB is OK
|
|
660
|
+
if let (4, IpNetwork::V6(_)) = (ip_version, network) {
|
|
661
|
+
return Err(Error::new(
|
|
662
|
+
ruby.exception_arg_error(),
|
|
663
|
+
format!(
|
|
664
|
+
"Cannot search for IPv6 network '{}' in an IPv4-only database",
|
|
665
|
+
network_str
|
|
666
|
+
),
|
|
667
|
+
));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
let mut iter = reader.within(network).map_err(|e| {
|
|
671
|
+
Error::new(
|
|
672
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
673
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
674
|
+
format!("Failed to iterate: {}", e),
|
|
675
|
+
)
|
|
676
|
+
})?;
|
|
677
|
+
|
|
678
|
+
// Get IPAddr class
|
|
679
|
+
let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
|
|
680
|
+
|
|
681
|
+
// Iterate over all networks
|
|
682
|
+
while let Some(result) = iter.next() {
|
|
683
|
+
match result {
|
|
684
|
+
Ok(item) => {
|
|
685
|
+
// Convert IpNetwork to IPAddr
|
|
686
|
+
let ip_str = item.ip_net.to_string();
|
|
687
|
+
let ipaddr = ipaddr_class.funcall::<_, _, Value>("new", (ip_str,))?;
|
|
688
|
+
|
|
689
|
+
// Yield [network, data] to block
|
|
690
|
+
let values = (ipaddr, item.info.into_value());
|
|
691
|
+
ruby.yield_values::<(Value, Value), Value>(values)?;
|
|
692
|
+
}
|
|
693
|
+
Err(MaxMindDbError::InvalidDatabase(_)) | Err(MaxMindDbError::Io(_)) => {
|
|
694
|
+
return Err(Error::new(
|
|
695
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
696
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
697
|
+
ERR_BAD_DATA,
|
|
698
|
+
));
|
|
699
|
+
}
|
|
700
|
+
Err(e) => {
|
|
701
|
+
return Err(Error::new(
|
|
702
|
+
ruby.exception_runtime_error(),
|
|
703
|
+
format!("Database iteration failed: {}", e),
|
|
704
|
+
));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
Ok(ruby.qnil().as_value())
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/// Helper method to get the reader from the ArcSwapOption
|
|
713
|
+
fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Arc<ReaderSource>, Error> {
|
|
714
|
+
self.reader
|
|
715
|
+
.load_full()
|
|
716
|
+
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB))
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
unsafe impl Send for Reader {}
|
|
721
|
+
|
|
722
|
+
/// Helper function to create a Reader from a ReaderSource
|
|
723
|
+
fn create_reader(source: ReaderSource) -> Reader {
|
|
724
|
+
let ip_version = source.metadata().ip_version;
|
|
725
|
+
let source = Arc::new(source);
|
|
726
|
+
Reader {
|
|
727
|
+
reader: Arc::new(ArcSwapOption::from(Some(source))),
|
|
728
|
+
closed: Arc::new(AtomicBool::new(false)),
|
|
729
|
+
ip_version,
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/// Parse IP address from Ruby value (String or IPAddr) - optimized version
|
|
734
|
+
#[inline(always)]
|
|
735
|
+
fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
736
|
+
// Fast path: Try as RString first (most common case) - zero-copy
|
|
737
|
+
if let Some(rstring) = RString::from_value(value) {
|
|
738
|
+
// SAFETY: as_str() returns a &str that's valid as long as the Ruby string isn't modified
|
|
739
|
+
// We use it immediately for parsing, so this is safe
|
|
740
|
+
let ip_str = unsafe { rstring.as_str() }.map_err(|e| {
|
|
741
|
+
Error::new(
|
|
742
|
+
ruby.exception_arg_error(),
|
|
743
|
+
format!("Invalid UTF-8 in IP address string: {}", e),
|
|
744
|
+
)
|
|
745
|
+
})?;
|
|
746
|
+
|
|
747
|
+
return IpAddr::from_str(ip_str).map_err(|_| {
|
|
748
|
+
Error::new(
|
|
749
|
+
ruby.exception_arg_error(),
|
|
750
|
+
format!("'{}' does not appear to be an IPv4 or IPv6 address", ip_str),
|
|
751
|
+
)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Slow path: Try as IPAddr object
|
|
756
|
+
if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
|
|
757
|
+
return IpAddr::from_str(&ipaddr_obj).map_err(|_| {
|
|
758
|
+
Error::new(
|
|
759
|
+
ruby.exception_arg_error(),
|
|
760
|
+
format!(
|
|
761
|
+
"'{}' does not appear to be an IPv4 or IPv6 address",
|
|
762
|
+
ipaddr_obj
|
|
763
|
+
),
|
|
764
|
+
)
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
Err(Error::new(
|
|
769
|
+
ruby.exception_arg_error(),
|
|
770
|
+
format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
|
|
771
|
+
))
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/// Generate error message for IPv6 in IPv4-only database
|
|
775
|
+
fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
|
|
776
|
+
format!(
|
|
777
|
+
"Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database",
|
|
778
|
+
ip
|
|
779
|
+
)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
|
|
783
|
+
fn open_database_mmap(path: &str) -> Result<Reader, Error> {
|
|
784
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
785
|
+
|
|
786
|
+
let file = File::open(Path::new(path)).map_err(|e| match e.kind() {
|
|
787
|
+
std::io::ErrorKind::NotFound => {
|
|
788
|
+
let errno = ruby
|
|
789
|
+
.class_object()
|
|
790
|
+
.const_get::<_, RModule>("Errno")
|
|
791
|
+
.expect("Errno module should exist");
|
|
792
|
+
let enoent = errno
|
|
793
|
+
.const_get::<_, RClass>("ENOENT")
|
|
794
|
+
.expect("Errno::ENOENT should exist");
|
|
795
|
+
Error::new(
|
|
796
|
+
ExceptionClass::from_value(enoent.as_value())
|
|
797
|
+
.expect("ENOENT should convert to ExceptionClass"),
|
|
798
|
+
e.to_string(),
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
_ => Error::new(ruby.exception_io_error(), e.to_string()),
|
|
802
|
+
})?;
|
|
803
|
+
|
|
804
|
+
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| {
|
|
805
|
+
Error::new(
|
|
806
|
+
ruby.exception_io_error(),
|
|
807
|
+
format!("Failed to memory-map database file: {}", e),
|
|
808
|
+
)
|
|
809
|
+
})?;
|
|
810
|
+
|
|
811
|
+
let reader = MaxMindReader::from_source(mmap).map_err(|_| {
|
|
812
|
+
Error::new(
|
|
813
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
814
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
815
|
+
format!(
|
|
816
|
+
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
817
|
+
path
|
|
818
|
+
),
|
|
819
|
+
)
|
|
820
|
+
})?;
|
|
821
|
+
|
|
822
|
+
Ok(create_reader(ReaderSource::Mmap(reader)))
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/// Open a MaxMind DB by loading entire file into memory (MODE_MEMORY)
|
|
826
|
+
fn open_database_memory(path: &str) -> Result<Reader, Error> {
|
|
827
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
828
|
+
|
|
829
|
+
let mut file = File::open(Path::new(path)).map_err(|e| match e.kind() {
|
|
830
|
+
std::io::ErrorKind::NotFound => {
|
|
831
|
+
let errno = ruby
|
|
832
|
+
.class_object()
|
|
833
|
+
.const_get::<_, RModule>("Errno")
|
|
834
|
+
.expect("Errno module should exist");
|
|
835
|
+
let enoent = errno
|
|
836
|
+
.const_get::<_, RClass>("ENOENT")
|
|
837
|
+
.expect("Errno::ENOENT should exist");
|
|
838
|
+
Error::new(
|
|
839
|
+
ExceptionClass::from_value(enoent.as_value())
|
|
840
|
+
.expect("ENOENT should convert to ExceptionClass"),
|
|
841
|
+
e.to_string(),
|
|
842
|
+
)
|
|
843
|
+
}
|
|
844
|
+
_ => Error::new(ruby.exception_io_error(), e.to_string()),
|
|
845
|
+
})?;
|
|
846
|
+
|
|
847
|
+
let mut buffer = Vec::new();
|
|
848
|
+
file.read_to_end(&mut buffer).map_err(|e| {
|
|
849
|
+
Error::new(
|
|
850
|
+
ruby.exception_io_error(),
|
|
851
|
+
format!("Failed to read database file: {}", e),
|
|
852
|
+
)
|
|
853
|
+
})?;
|
|
854
|
+
|
|
855
|
+
let reader = MaxMindReader::from_source(buffer).map_err(|_| {
|
|
856
|
+
Error::new(
|
|
857
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
858
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
859
|
+
format!(
|
|
860
|
+
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
861
|
+
path
|
|
862
|
+
),
|
|
863
|
+
)
|
|
864
|
+
})?;
|
|
865
|
+
|
|
866
|
+
Ok(create_reader(ReaderSource::Memory(reader)))
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/// Get the InvalidDatabaseError class
|
|
870
|
+
fn invalid_database_error() -> RClass {
|
|
871
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
872
|
+
let maxmind = ruby
|
|
873
|
+
.class_object()
|
|
874
|
+
.const_get::<_, RModule>("MaxMind")
|
|
875
|
+
.expect("MaxMind module should exist");
|
|
876
|
+
let db = maxmind
|
|
877
|
+
.const_get::<_, RModule>("DB")
|
|
878
|
+
.expect("MaxMind::DB module should exist");
|
|
879
|
+
let rust = db
|
|
880
|
+
.const_get::<_, RModule>("Rust")
|
|
881
|
+
.expect("MaxMind::DB::Rust module should exist");
|
|
882
|
+
rust.const_get::<_, RClass>("InvalidDatabaseError")
|
|
883
|
+
.expect("InvalidDatabaseError class should exist")
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
#[magnus::init]
|
|
887
|
+
fn init(ruby: &magnus::Ruby) -> Result<(), Error> {
|
|
888
|
+
// Define module hierarchy: MaxMind::DB::Rust
|
|
889
|
+
// Handle case where official maxmind-db gem may have already defined MaxMind::DB as a Class
|
|
890
|
+
let maxmind = ruby.define_module("MaxMind")?;
|
|
891
|
+
|
|
892
|
+
// Try to get or define DB - it might be a Class (official gem) or Module (ours)
|
|
893
|
+
let db_value = maxmind.const_get::<_, Value>("DB");
|
|
894
|
+
let rust = match db_value {
|
|
895
|
+
Ok(existing) if existing.is_kind_of(ruby.class_class()) => {
|
|
896
|
+
// MaxMind::DB exists as a Class (official gem loaded first)
|
|
897
|
+
// Define Rust module directly as a constant on the class using funcall
|
|
898
|
+
let rust_mod = ruby.define_module("MaxMindDBRustTemp")?;
|
|
899
|
+
// Use const_set via funcall on the existing class/module
|
|
900
|
+
let _ = existing.funcall::<_, _, Value>("const_set", ("Rust", rust_mod))?;
|
|
901
|
+
rust_mod
|
|
902
|
+
}
|
|
903
|
+
Ok(existing) => {
|
|
904
|
+
// MaxMind::DB exists as a Module (our gem loaded first)
|
|
905
|
+
let db_mod = RModule::from_value(existing).ok_or_else(|| {
|
|
906
|
+
Error::new(ruby.exception_type_error(), "MaxMind::DB is not a module")
|
|
907
|
+
})?;
|
|
908
|
+
db_mod.define_module("Rust")?
|
|
909
|
+
}
|
|
910
|
+
Err(_) => {
|
|
911
|
+
// MaxMind::DB doesn't exist, define it as a module
|
|
912
|
+
let db = maxmind.define_module("DB")?;
|
|
913
|
+
db.define_module("Rust")?
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
917
|
+
// Define InvalidDatabaseError
|
|
918
|
+
let runtime_error = ruby.exception_runtime_error();
|
|
919
|
+
rust.define_error("InvalidDatabaseError", runtime_error)?;
|
|
920
|
+
|
|
921
|
+
// Define Reader class
|
|
922
|
+
let reader_class = rust.define_class("Reader", ruby.class_object())?;
|
|
923
|
+
reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
|
|
924
|
+
reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
|
|
925
|
+
reader_class.define_method(
|
|
926
|
+
"get_with_prefix_length",
|
|
927
|
+
magnus::method!(Reader::get_with_prefix_length, 1),
|
|
928
|
+
)?;
|
|
929
|
+
reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
|
|
930
|
+
reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
|
|
931
|
+
reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
|
|
932
|
+
reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
|
|
933
|
+
|
|
934
|
+
// Include Enumerable module
|
|
935
|
+
let enumerable = ruby.class_object().const_get::<_, RModule>("Enumerable")?;
|
|
936
|
+
reader_class.include_module(enumerable)?;
|
|
937
|
+
|
|
938
|
+
// Define Metadata class
|
|
939
|
+
let metadata_class = rust.define_class("Metadata", ruby.class_object())?;
|
|
940
|
+
metadata_class.define_method(
|
|
941
|
+
"binary_format_major_version",
|
|
942
|
+
magnus::method!(Metadata::binary_format_major_version, 0),
|
|
943
|
+
)?;
|
|
944
|
+
metadata_class.define_method(
|
|
945
|
+
"binary_format_minor_version",
|
|
946
|
+
magnus::method!(Metadata::binary_format_minor_version, 0),
|
|
947
|
+
)?;
|
|
948
|
+
metadata_class.define_method("build_epoch", magnus::method!(Metadata::build_epoch, 0))?;
|
|
949
|
+
metadata_class.define_method("database_type", magnus::method!(Metadata::database_type, 0))?;
|
|
950
|
+
metadata_class.define_method("description", magnus::method!(Metadata::description, 0))?;
|
|
951
|
+
metadata_class.define_method("ip_version", magnus::method!(Metadata::ip_version, 0))?;
|
|
952
|
+
metadata_class.define_method("languages", magnus::method!(Metadata::languages, 0))?;
|
|
953
|
+
metadata_class.define_method("node_count", magnus::method!(Metadata::node_count, 0))?;
|
|
954
|
+
metadata_class.define_method("record_size", magnus::method!(Metadata::record_size, 0))?;
|
|
955
|
+
metadata_class.define_method(
|
|
956
|
+
"node_byte_size",
|
|
957
|
+
magnus::method!(Metadata::node_byte_size, 0),
|
|
958
|
+
)?;
|
|
959
|
+
metadata_class.define_method(
|
|
960
|
+
"search_tree_size",
|
|
961
|
+
magnus::method!(Metadata::search_tree_size, 0),
|
|
962
|
+
)?;
|
|
963
|
+
|
|
964
|
+
// Define MODE constants
|
|
965
|
+
rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
|
|
966
|
+
rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
|
|
967
|
+
rust.const_set("MODE_MMAP", ruby.to_symbol("MODE_MMAP"))?;
|
|
968
|
+
|
|
969
|
+
Ok(())
|
|
970
|
+
}
|