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
|
@@ -0,0 +1,940 @@
|
|
|
1
|
+
use crate::{
|
|
2
|
+
decoder::RubyDecodedValue,
|
|
3
|
+
metadata::Metadata,
|
|
4
|
+
path::{
|
|
5
|
+
parse_path_array, path_array, path_cache_hash, path_elements_from_owned_path,
|
|
6
|
+
path_matches_cached, CachedPath, OwnedPathElement, PATH_CACHE_MAX_ENTRIES,
|
|
7
|
+
},
|
|
8
|
+
rust_module,
|
|
9
|
+
};
|
|
10
|
+
use ::maxminddb as maxminddb_crate;
|
|
11
|
+
use arc_swap::{ArcSwapOption, Guard};
|
|
12
|
+
use ipnetwork::IpNetwork;
|
|
13
|
+
use magnus::{
|
|
14
|
+
error::Error, prelude::*, scan_args::get_kwargs, scan_args::scan_args, typed_data::Obj,
|
|
15
|
+
ExceptionClass, IntoValue, RArray, RClass, RModule, RString, Symbol, Value,
|
|
16
|
+
};
|
|
17
|
+
use maxminddb_crate::{MaxMindDbError, PathElement, Reader as MaxMindReader, Within};
|
|
18
|
+
use memmap2::Mmap;
|
|
19
|
+
use std::{
|
|
20
|
+
collections::VecDeque,
|
|
21
|
+
fs::File,
|
|
22
|
+
io::Read as IoRead,
|
|
23
|
+
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
|
24
|
+
path::Path,
|
|
25
|
+
str::FromStr,
|
|
26
|
+
sync::{Arc, Mutex},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ERR_CLOSED_DB: &str = "Attempt to read from a closed MaxMind DB.";
|
|
30
|
+
const ERR_BAD_DATA: &str =
|
|
31
|
+
"The MaxMind DB file's data section contains bad data (unknown data type or corrupt data)";
|
|
32
|
+
|
|
33
|
+
/// Enum to handle different reader source types
|
|
34
|
+
enum ReaderSource {
|
|
35
|
+
Mmap(MaxMindReader<Mmap>),
|
|
36
|
+
Memory(MaxMindReader<Vec<u8>>),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Copy, Clone)]
|
|
40
|
+
enum OpenMode {
|
|
41
|
+
Mmap,
|
|
42
|
+
Memory,
|
|
43
|
+
Buffer,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
impl OpenMode {
|
|
47
|
+
fn from_symbol(mode: Symbol, ruby: &magnus::Ruby) -> Result<Self, Error> {
|
|
48
|
+
let mode_name = mode.name()?;
|
|
49
|
+
match mode_name.as_ref() {
|
|
50
|
+
// MODE_FILE is the official gem's file-backed mode; use the
|
|
51
|
+
// existing mmap reader for the same path-backed behavior.
|
|
52
|
+
"MODE_AUTO" | "MODE_FILE" | "MODE_MMAP" => Ok(Self::Mmap),
|
|
53
|
+
"MODE_MEMORY" => Ok(Self::Memory),
|
|
54
|
+
"MODE_PARAM_IS_BUFFER" => Ok(Self::Buffer),
|
|
55
|
+
_ => Err(Error::new(
|
|
56
|
+
ruby.exception_arg_error(),
|
|
57
|
+
format!("Unsupported mode: {}", mode_name),
|
|
58
|
+
)),
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl ReaderSource {
|
|
64
|
+
#[inline]
|
|
65
|
+
fn lookup(
|
|
66
|
+
&self,
|
|
67
|
+
ip: IpAddr,
|
|
68
|
+
) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
|
|
69
|
+
match self {
|
|
70
|
+
ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode(),
|
|
71
|
+
ReaderSource::Memory(reader) => reader.lookup(ip)?.decode(),
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[inline]
|
|
76
|
+
fn lookup_prefix(
|
|
77
|
+
&self,
|
|
78
|
+
ip: IpAddr,
|
|
79
|
+
) -> Result<(Option<RubyDecodedValue>, usize), maxminddb_crate::MaxMindDbError> {
|
|
80
|
+
let (result, prefix_len) = match self {
|
|
81
|
+
ReaderSource::Mmap(reader) => {
|
|
82
|
+
let result = reader.lookup(ip)?;
|
|
83
|
+
let network = result.network()?;
|
|
84
|
+
(result.decode()?, prefix_len_for_ip_network(ip, network))
|
|
85
|
+
}
|
|
86
|
+
ReaderSource::Memory(reader) => {
|
|
87
|
+
let result = reader.lookup(ip)?;
|
|
88
|
+
let network = result.network()?;
|
|
89
|
+
(result.decode()?, prefix_len_for_ip_network(ip, network))
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
Ok((result, prefix_len))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[inline]
|
|
96
|
+
fn lookup_path(
|
|
97
|
+
&self,
|
|
98
|
+
ip: IpAddr,
|
|
99
|
+
path_elements: &[PathElement<'_>],
|
|
100
|
+
) -> Result<Option<RubyDecodedValue>, maxminddb_crate::MaxMindDbError> {
|
|
101
|
+
match self {
|
|
102
|
+
ReaderSource::Mmap(reader) => reader.lookup(ip)?.decode_path(path_elements),
|
|
103
|
+
ReaderSource::Memory(reader) => reader.lookup(ip)?.decode_path(path_elements),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[inline]
|
|
108
|
+
fn metadata(&self) -> &maxminddb_crate::Metadata {
|
|
109
|
+
match self {
|
|
110
|
+
ReaderSource::Mmap(reader) => reader.metadata(),
|
|
111
|
+
ReaderSource::Memory(reader) => reader.metadata(),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fn verify(&self) -> Result<(), MaxMindDbError> {
|
|
116
|
+
match self {
|
|
117
|
+
ReaderSource::Mmap(reader) => reader.verify(),
|
|
118
|
+
ReaderSource::Memory(reader) => reader.verify(),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#[inline]
|
|
123
|
+
fn within(&self, network: IpNetwork) -> Result<ReaderWithin<'_>, MaxMindDbError> {
|
|
124
|
+
match self {
|
|
125
|
+
ReaderSource::Mmap(reader) => Ok(ReaderWithin::Mmap(
|
|
126
|
+
reader.within(network, Default::default())?,
|
|
127
|
+
)),
|
|
128
|
+
ReaderSource::Memory(reader) => Ok(ReaderWithin::Memory(
|
|
129
|
+
reader.within(network, Default::default())?,
|
|
130
|
+
)),
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Wrapper enum for Within iterators
|
|
136
|
+
enum ReaderWithin<'reader> {
|
|
137
|
+
Mmap(Within<'reader, Mmap>),
|
|
138
|
+
Memory(Within<'reader, Vec<u8>>),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
impl ReaderWithin<'_> {
|
|
142
|
+
fn next(&mut self) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
|
|
143
|
+
match self {
|
|
144
|
+
ReaderWithin::Mmap(iter) => next_within_result(iter),
|
|
145
|
+
ReaderWithin::Memory(iter) => next_within_result(iter),
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[inline]
|
|
151
|
+
// prefix_len_for_ip_network uses 0 as a sentinel for ip.is_ipv4() && network.is_ipv6().
|
|
152
|
+
// In this case, 0 is not a real prefix length; it signals an IPv4-in-IPv6 mapping path,
|
|
153
|
+
// and callers must treat it specially (distinct from "no network found").
|
|
154
|
+
fn prefix_len_for_ip_network(ip: IpAddr, network: IpNetwork) -> usize {
|
|
155
|
+
if ip.is_ipv4() && network.is_ipv6() {
|
|
156
|
+
0
|
|
157
|
+
} else {
|
|
158
|
+
network.prefix() as usize
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[inline]
|
|
163
|
+
fn next_within_result<S: AsRef<[u8]>>(
|
|
164
|
+
iter: &mut Within<'_, S>,
|
|
165
|
+
) -> Option<Result<(IpNetwork, RubyDecodedValue), MaxMindDbError>> {
|
|
166
|
+
loop {
|
|
167
|
+
match iter.next() {
|
|
168
|
+
None => return None,
|
|
169
|
+
Some(Err(e)) => return Some(Err(e)),
|
|
170
|
+
Some(Ok(lookup_result)) => {
|
|
171
|
+
let network = match lookup_result.network() {
|
|
172
|
+
Ok(n) => n,
|
|
173
|
+
Err(e) => return Some(Err(e)),
|
|
174
|
+
};
|
|
175
|
+
match lookup_result.decode::<RubyDecodedValue>() {
|
|
176
|
+
Ok(Some(data)) => return Some(Ok((network, data))),
|
|
177
|
+
Ok(None) => continue, // Skip networks without data
|
|
178
|
+
Err(e) => return Some(Err(e)),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/// A Ruby wrapper around the MaxMind DB reader
|
|
186
|
+
#[magnus::wrap(class = "MaxMind::DB::Rust::Reader")]
|
|
187
|
+
struct Reader {
|
|
188
|
+
reader: ArcSwapOption<ReaderSource>,
|
|
189
|
+
path_cache: Mutex<VecDeque<CachedPath>>,
|
|
190
|
+
ip_version: u16,
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl Reader {
|
|
194
|
+
fn new(args: &[Value]) -> Result<Self, Error> {
|
|
195
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
196
|
+
|
|
197
|
+
let args = scan_args::<(Value,), (), (), (), _, ()>(args)?;
|
|
198
|
+
let (database,) = args.required;
|
|
199
|
+
let kw = get_kwargs::<_, (), (Option<Symbol>,), ()>(args.keywords, &[], &["mode"])?;
|
|
200
|
+
let (mode,) = kw.optional;
|
|
201
|
+
|
|
202
|
+
// Parse mode from options hash
|
|
203
|
+
let mode: Symbol = mode.unwrap_or_else(|| ruby.to_symbol("MODE_AUTO"));
|
|
204
|
+
|
|
205
|
+
let open_mode = OpenMode::from_symbol(mode, &ruby)?;
|
|
206
|
+
|
|
207
|
+
// Open database with appropriate mode
|
|
208
|
+
match open_mode {
|
|
209
|
+
OpenMode::Mmap => open_database_mmap(&database_path(database)?),
|
|
210
|
+
OpenMode::Memory => open_database_memory(&database_path(database)?),
|
|
211
|
+
OpenMode::Buffer => open_database_buffer(database_buffer(database)?),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[inline]
|
|
216
|
+
fn get(&self, ip_address: Value) -> Result<Value, Error> {
|
|
217
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
218
|
+
|
|
219
|
+
let guard = self.get_reader(&ruby)?;
|
|
220
|
+
let reader_option = guard.as_ref();
|
|
221
|
+
let reader = reader_option.as_ref().unwrap();
|
|
222
|
+
|
|
223
|
+
let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
|
|
224
|
+
|
|
225
|
+
lookup_result_to_value(&ruby, reader.lookup(parsed_ip), "Database lookup failed")
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#[inline]
|
|
229
|
+
fn get_path(&self, ip_address: Value, path: Value) -> Result<Value, Error> {
|
|
230
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
231
|
+
|
|
232
|
+
let guard = self.get_reader(&ruby)?;
|
|
233
|
+
let reader_option = guard.as_ref();
|
|
234
|
+
let reader = reader_option.as_ref().unwrap();
|
|
235
|
+
|
|
236
|
+
let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
|
|
237
|
+
let owned_path = self.parse_path(path, &ruby)?;
|
|
238
|
+
let path_elements = path_elements_from_owned_path(owned_path.as_ref());
|
|
239
|
+
|
|
240
|
+
lookup_result_to_value(
|
|
241
|
+
&ruby,
|
|
242
|
+
reader.lookup_path(parsed_ip, &path_elements),
|
|
243
|
+
"Database lookup failed",
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[inline]
|
|
248
|
+
fn get_with_prefix_length(&self, ip_address: Value) -> Result<RArray, Error> {
|
|
249
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
250
|
+
|
|
251
|
+
let guard = self.get_reader(&ruby)?;
|
|
252
|
+
let reader_option = guard.as_ref();
|
|
253
|
+
let reader = reader_option.as_ref().unwrap();
|
|
254
|
+
|
|
255
|
+
let parsed_ip = self.parse_lookup_ip(ip_address, &ruby)?;
|
|
256
|
+
|
|
257
|
+
// Perform lookup with prefix
|
|
258
|
+
lookup_prefix_result_to_array(
|
|
259
|
+
&ruby,
|
|
260
|
+
reader.lookup_prefix(parsed_ip),
|
|
261
|
+
"Database lookup failed",
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn get_many(&self, ips: Value) -> Result<RArray, Error> {
|
|
266
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
267
|
+
|
|
268
|
+
let guard = self.get_reader(&ruby)?;
|
|
269
|
+
let reader_option = guard.as_ref();
|
|
270
|
+
let reader = reader_option.as_ref().unwrap();
|
|
271
|
+
|
|
272
|
+
if let Ok(ip_array) = RArray::try_convert(ips) {
|
|
273
|
+
return ruby.ary_try_from_iter((0..ip_array.len()).map(|index| {
|
|
274
|
+
let ip = ip_array.entry::<Value>(index as isize)?;
|
|
275
|
+
self.lookup_ip_value(&ruby, reader, ip)
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
ensure_enumerable(ips, &ruby, "ips must be an Array or Enumerable")?;
|
|
280
|
+
ruby.ary_try_from_iter(
|
|
281
|
+
ips.enumeratorize("each", ())
|
|
282
|
+
.map(|ip| self.lookup_ip_value(&ruby, reader, ip?)),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fn get_many_path(&self, ips: Value, path: Value) -> Result<RArray, Error> {
|
|
287
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
288
|
+
|
|
289
|
+
let guard = self.get_reader(&ruby)?;
|
|
290
|
+
let reader_option = guard.as_ref();
|
|
291
|
+
let reader = reader_option.as_ref().unwrap();
|
|
292
|
+
|
|
293
|
+
let owned_path = self.parse_path(path, &ruby)?;
|
|
294
|
+
let path_elements = path_elements_from_owned_path(owned_path.as_ref());
|
|
295
|
+
|
|
296
|
+
if let Ok(ip_array) = RArray::try_convert(ips) {
|
|
297
|
+
return ruby.ary_try_from_iter((0..ip_array.len()).map(|index| {
|
|
298
|
+
let ip = ip_array.entry::<Value>(index as isize)?;
|
|
299
|
+
self.lookup_ip_path_value(&ruby, reader, ip, &path_elements)
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
ensure_enumerable(ips, &ruby, "ips must be an Array or Enumerable")?;
|
|
304
|
+
ruby.ary_try_from_iter(
|
|
305
|
+
ips.enumeratorize("each", ())
|
|
306
|
+
.map(|ip| self.lookup_ip_path_value(&ruby, reader, ip?, &path_elements)),
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn metadata(&self) -> Result<Metadata, Error> {
|
|
311
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
312
|
+
|
|
313
|
+
let guard = self.get_reader(&ruby)?;
|
|
314
|
+
let reader_option = guard.as_ref();
|
|
315
|
+
let reader = reader_option.as_ref().unwrap();
|
|
316
|
+
let meta = reader.metadata();
|
|
317
|
+
|
|
318
|
+
Ok(Metadata::from_maxmind(meta))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fn verify(&self) -> Result<bool, Error> {
|
|
322
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
323
|
+
let guard = self.get_reader(&ruby)?;
|
|
324
|
+
let reader_option = guard.as_ref();
|
|
325
|
+
let reader = reader_option.as_ref().unwrap();
|
|
326
|
+
|
|
327
|
+
reader.verify().map_err(|err| {
|
|
328
|
+
invalid_database_exception(&format!("Database verification failed: {err}"))
|
|
329
|
+
})?;
|
|
330
|
+
Ok(true)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
fn close(&self) {
|
|
334
|
+
self.reader.store(None);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn closed(&self) -> bool {
|
|
338
|
+
self.reader.load().is_none()
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn inspect(&self) -> String {
|
|
342
|
+
format!(
|
|
343
|
+
"#<MaxMind::DB::Rust::Reader:0x{:x} @closed={} @ip_version={}>",
|
|
344
|
+
self as *const Self as usize,
|
|
345
|
+
self.closed(),
|
|
346
|
+
self.ip_version,
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
fn each(ruby: &magnus::Ruby, rb_self: Obj<Self>, args: &[Value]) -> Result<Value, Error> {
|
|
351
|
+
let reader_self = &*rb_self;
|
|
352
|
+
|
|
353
|
+
let guard = reader_self.get_reader(ruby)?;
|
|
354
|
+
let reader_option = guard.as_ref();
|
|
355
|
+
let reader = reader_option.as_ref().unwrap();
|
|
356
|
+
|
|
357
|
+
// If no block given, return enumerator
|
|
358
|
+
if !ruby.block_given() {
|
|
359
|
+
return Ok(rb_self.enumeratorize("each", args).as_value());
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
let ip_version = reader.metadata().ip_version;
|
|
363
|
+
|
|
364
|
+
// Determine the network to iterate over
|
|
365
|
+
let network_str = if args.is_empty() {
|
|
366
|
+
// No argument: use default (full database)
|
|
367
|
+
if ip_version == 4 {
|
|
368
|
+
"0.0.0.0/0".to_string()
|
|
369
|
+
} else {
|
|
370
|
+
"::/0".to_string()
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
// Argument provided: extract network CIDR string
|
|
374
|
+
let network_arg = args[0];
|
|
375
|
+
|
|
376
|
+
// Try to get string representation
|
|
377
|
+
// Accept both String and IPAddr objects
|
|
378
|
+
let network_str_val = if let Ok(s) = RString::try_convert(network_arg) {
|
|
379
|
+
// It's already a string
|
|
380
|
+
s.to_string()?
|
|
381
|
+
} else {
|
|
382
|
+
// Check if it's an IPAddr object
|
|
383
|
+
let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
|
|
384
|
+
if network_arg.is_kind_of(ipaddr_class) {
|
|
385
|
+
// It's an IPAddr - need to get both address and prefix
|
|
386
|
+
let ip_str: String = network_arg.funcall("to_s", ())?;
|
|
387
|
+
|
|
388
|
+
// Get the prefix length from IPAddr
|
|
389
|
+
// IPAddr stores prefix as a netmask, need to convert
|
|
390
|
+
let prefix_len: u8 = network_arg.funcall("prefix", ())?;
|
|
391
|
+
|
|
392
|
+
// Construct CIDR notation
|
|
393
|
+
format!("{}/{}", ip_str, prefix_len)
|
|
394
|
+
} else {
|
|
395
|
+
// Try to call to_s on it (works for other objects)
|
|
396
|
+
let to_s_result: Value = network_arg.funcall("to_s", ())?;
|
|
397
|
+
RString::try_convert(to_s_result)
|
|
398
|
+
.map_err(|_| {
|
|
399
|
+
Error::new(
|
|
400
|
+
ruby.exception_arg_error(),
|
|
401
|
+
"Network parameter must be a String or IPAddr",
|
|
402
|
+
)
|
|
403
|
+
})?
|
|
404
|
+
.to_string()?
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
network_str_val
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
let network = IpNetwork::from_str(&network_str).map_err(|e| {
|
|
412
|
+
Error::new(
|
|
413
|
+
ruby.exception_arg_error(),
|
|
414
|
+
format!("Invalid network CIDR '{}': {}", network_str, e),
|
|
415
|
+
)
|
|
416
|
+
})?;
|
|
417
|
+
|
|
418
|
+
// Validate network matches database IP version
|
|
419
|
+
// IPv4 in IPv6 DB is OK (IPv4-mapped), IPv6 in IPv6 DB is OK
|
|
420
|
+
if let (4, IpNetwork::V6(_)) = (ip_version, network) {
|
|
421
|
+
return Err(Error::new(
|
|
422
|
+
ruby.exception_arg_error(),
|
|
423
|
+
format!(
|
|
424
|
+
"Cannot search for IPv6 network '{}' in an IPv4-only database",
|
|
425
|
+
network_str
|
|
426
|
+
),
|
|
427
|
+
));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let mut iter = reader
|
|
431
|
+
.within(network)
|
|
432
|
+
.map_err(|e| invalid_database_exception(&format!("Failed to iterate: {}", e)))?;
|
|
433
|
+
// Get IPAddr class
|
|
434
|
+
let ipaddr_class = ruby.class_object().const_get::<_, RClass>("IPAddr")?;
|
|
435
|
+
|
|
436
|
+
// Iterate over all networks
|
|
437
|
+
while let Some(result) = iter.next() {
|
|
438
|
+
match result {
|
|
439
|
+
Ok((network, data)) => {
|
|
440
|
+
// Convert IpNetwork to IPAddr
|
|
441
|
+
let ip_str = network.to_string();
|
|
442
|
+
let ipaddr = ipaddr_class.funcall::<_, _, Value>("new", (ip_str,))?;
|
|
443
|
+
|
|
444
|
+
// Yield [network, data] to block
|
|
445
|
+
let values = (ipaddr, data.into_value());
|
|
446
|
+
ruby.yield_values::<(Value, Value), Value>(values)?;
|
|
447
|
+
}
|
|
448
|
+
Err(MaxMindDbError::InvalidDatabase { .. })
|
|
449
|
+
| Err(MaxMindDbError::Decoding { .. })
|
|
450
|
+
| Err(MaxMindDbError::Io(_)) => {
|
|
451
|
+
return Err(invalid_database_exception(ERR_BAD_DATA));
|
|
452
|
+
}
|
|
453
|
+
Err(e) => {
|
|
454
|
+
return Err(Error::new(
|
|
455
|
+
ruby.exception_runtime_error(),
|
|
456
|
+
format!("Database iteration failed: {}", e),
|
|
457
|
+
));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
Ok(ruby.qnil().as_value())
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/// Helper method to get the reader from the ArcSwapOption
|
|
466
|
+
fn get_reader(&self, ruby: &magnus::Ruby) -> Result<Guard<Option<Arc<ReaderSource>>>, Error> {
|
|
467
|
+
let guard = self.reader.load();
|
|
468
|
+
if guard.is_none() {
|
|
469
|
+
return Err(Error::new(ruby.exception_runtime_error(), ERR_CLOSED_DB));
|
|
470
|
+
}
|
|
471
|
+
Ok(guard)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
#[inline]
|
|
475
|
+
fn parse_lookup_ip(&self, ip_address: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
476
|
+
let parsed_ip = parse_ip_address_fast(ip_address, ruby)?;
|
|
477
|
+
self.validate_lookup_ip(parsed_ip, ruby)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
#[inline]
|
|
481
|
+
fn validate_lookup_ip(&self, parsed_ip: IpAddr, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
482
|
+
if self.ip_version == 4 && matches!(parsed_ip, IpAddr::V6(_)) {
|
|
483
|
+
Err(Error::new(
|
|
484
|
+
ruby.exception_arg_error(),
|
|
485
|
+
ipv6_in_ipv4_error(&parsed_ip),
|
|
486
|
+
))
|
|
487
|
+
} else {
|
|
488
|
+
Ok(parsed_ip)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
#[inline]
|
|
493
|
+
fn lookup_ip_value(
|
|
494
|
+
&self,
|
|
495
|
+
ruby: &magnus::Ruby,
|
|
496
|
+
reader: &ReaderSource,
|
|
497
|
+
ip: Value,
|
|
498
|
+
) -> Result<Value, Error> {
|
|
499
|
+
let parsed_ip = self.parse_lookup_ip(ip, ruby)?;
|
|
500
|
+
lookup_result_to_value(ruby, reader.lookup(parsed_ip), "Database lookup failed")
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
#[inline]
|
|
504
|
+
fn lookup_ip_path_value(
|
|
505
|
+
&self,
|
|
506
|
+
ruby: &magnus::Ruby,
|
|
507
|
+
reader: &ReaderSource,
|
|
508
|
+
ip: Value,
|
|
509
|
+
path_elements: &[PathElement<'_>],
|
|
510
|
+
) -> Result<Value, Error> {
|
|
511
|
+
let parsed_ip = self.parse_lookup_ip(ip, ruby)?;
|
|
512
|
+
lookup_result_to_value(
|
|
513
|
+
ruby,
|
|
514
|
+
reader.lookup_path(parsed_ip, path_elements),
|
|
515
|
+
"Database lookup failed",
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fn parse_path(
|
|
520
|
+
&self,
|
|
521
|
+
path: Value,
|
|
522
|
+
ruby: &magnus::Ruby,
|
|
523
|
+
) -> Result<Arc<[OwnedPathElement]>, Error> {
|
|
524
|
+
let path = path_array(path, ruby)?;
|
|
525
|
+
let hash = path_cache_hash(path, ruby)?;
|
|
526
|
+
|
|
527
|
+
if let Some(cached) = self.cached_path(path, hash)? {
|
|
528
|
+
return Ok(cached);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
let parsed_path: Arc<[OwnedPathElement]> = parse_path_array(path, ruby)?.into();
|
|
532
|
+
self.store_cached_path(hash, parsed_path.clone());
|
|
533
|
+
Ok(parsed_path)
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fn cached_path(
|
|
537
|
+
&self,
|
|
538
|
+
path: RArray,
|
|
539
|
+
hash: u64,
|
|
540
|
+
) -> Result<Option<Arc<[OwnedPathElement]>>, Error> {
|
|
541
|
+
let cache = match self.path_cache.lock() {
|
|
542
|
+
Ok(cache) => cache,
|
|
543
|
+
Err(_) => return Ok(None),
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
for entry in cache
|
|
547
|
+
.iter()
|
|
548
|
+
.filter(|entry| entry.hash == hash && entry.elements.len() == path.len())
|
|
549
|
+
{
|
|
550
|
+
if path_matches_cached(path, entry.elements.as_ref())? {
|
|
551
|
+
return Ok(Some(entry.elements.clone()));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
Ok(None)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
fn store_cached_path(&self, hash: u64, elements: Arc<[OwnedPathElement]>) {
|
|
559
|
+
if let Ok(mut cache) = self.path_cache.lock() {
|
|
560
|
+
if cache
|
|
561
|
+
.iter()
|
|
562
|
+
.any(|entry| entry.hash == hash && entry.elements.as_ref() == elements.as_ref())
|
|
563
|
+
{
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
cache.push_back(CachedPath { hash, elements });
|
|
568
|
+
while cache.len() > PATH_CACHE_MAX_ENTRIES {
|
|
569
|
+
cache.pop_front();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// SAFETY: Reader does not store Ruby VALUE handles. The database source is
|
|
576
|
+
// owned by ReaderSource and is read-only after construction; close atomically
|
|
577
|
+
// swaps the shared source to None. The path cache contains only Rust-owned path
|
|
578
|
+
// elements behind a Mutex. All Ruby object access happens inside method calls
|
|
579
|
+
// while the Ruby VM is active.
|
|
580
|
+
unsafe impl Send for Reader {}
|
|
581
|
+
|
|
582
|
+
/// Helper function to create a Reader from a ReaderSource
|
|
583
|
+
fn create_reader(source: ReaderSource) -> Reader {
|
|
584
|
+
let ip_version = source.metadata().ip_version;
|
|
585
|
+
let source = Arc::new(source);
|
|
586
|
+
Reader {
|
|
587
|
+
reader: ArcSwapOption::from(Some(source)),
|
|
588
|
+
path_cache: Mutex::new(VecDeque::with_capacity(PATH_CACHE_MAX_ENTRIES)),
|
|
589
|
+
ip_version,
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
fn ensure_enumerable(value: Value, ruby: &magnus::Ruby, error_message: &str) -> Result<(), Error> {
|
|
594
|
+
if value.respond_to("each", false)? {
|
|
595
|
+
Ok(())
|
|
596
|
+
} else {
|
|
597
|
+
Err(Error::new(
|
|
598
|
+
ruby.exception_arg_error(),
|
|
599
|
+
error_message.to_owned(),
|
|
600
|
+
))
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/// Parse IP address from Ruby value (String or IPAddr) - optimized version
|
|
605
|
+
#[inline(always)]
|
|
606
|
+
fn parse_ip_address_fast(value: Value, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
607
|
+
// Fast path: Try as RString first (most common case) - zero-copy
|
|
608
|
+
if let Some(rstring) = RString::from_value(value) {
|
|
609
|
+
// SAFETY: as_str() returns a &str that's valid as long as the Ruby string isn't modified
|
|
610
|
+
// We use it immediately for parsing, so this is safe
|
|
611
|
+
let ip_str = unsafe { rstring.as_str() }.map_err(|e| {
|
|
612
|
+
Error::new(
|
|
613
|
+
ruby.exception_arg_error(),
|
|
614
|
+
format!("Invalid UTF-8 in IP address string: {}", e),
|
|
615
|
+
)
|
|
616
|
+
})?;
|
|
617
|
+
|
|
618
|
+
return parse_ip_string(ip_str, ruby);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Slow path: Try as IPAddr object
|
|
622
|
+
if let Ok(ipaddr_class) = ruby.class_object().const_get::<_, RClass>("IPAddr") {
|
|
623
|
+
if value.is_kind_of(ipaddr_class) {
|
|
624
|
+
let address: u128 = value.funcall("to_i", ())?;
|
|
625
|
+
let is_ipv4: bool = value.funcall("ipv4?", ())?;
|
|
626
|
+
if is_ipv4 {
|
|
627
|
+
let address = u32::try_from(address).map_err(|_| {
|
|
628
|
+
Error::new(
|
|
629
|
+
ruby.exception_arg_error(),
|
|
630
|
+
format!("'{}' does not appear to be an IPv4 address", value),
|
|
631
|
+
)
|
|
632
|
+
})?;
|
|
633
|
+
return Ok(IpAddr::V4(Ipv4Addr::from(address)));
|
|
634
|
+
}
|
|
635
|
+
return Ok(IpAddr::V6(Ipv6Addr::from(address)));
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if let Ok(ipaddr_obj) = value.funcall::<_, _, String>("to_s", ()) {
|
|
640
|
+
return parse_ip_string(&ipaddr_obj, ruby);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
Err(Error::new(
|
|
644
|
+
ruby.exception_arg_error(),
|
|
645
|
+
format!("'{}' does not appear to be an IPv4 or IPv6 address", value),
|
|
646
|
+
))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#[inline(always)]
|
|
650
|
+
fn parse_ip_string(s: &str, ruby: &magnus::Ruby) -> Result<IpAddr, Error> {
|
|
651
|
+
if let Some(ip) = parse_ipv4_string(s.as_bytes()) {
|
|
652
|
+
return Ok(IpAddr::V4(ip));
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
IpAddr::from_str(s).map_err(|_| {
|
|
656
|
+
Error::new(
|
|
657
|
+
ruby.exception_arg_error(),
|
|
658
|
+
format!("'{}' does not appear to be an IPv4 or IPv6 address", s),
|
|
659
|
+
)
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
#[inline(always)]
|
|
664
|
+
fn parse_ipv4_string(bytes: &[u8]) -> Option<Ipv4Addr> {
|
|
665
|
+
let mut octets = [0u8; 4];
|
|
666
|
+
let mut octet_index = 0;
|
|
667
|
+
let mut value: u16 = 0;
|
|
668
|
+
let mut digits = 0;
|
|
669
|
+
|
|
670
|
+
for &byte in bytes {
|
|
671
|
+
if byte == b'.' {
|
|
672
|
+
if digits == 0 || octet_index == 3 {
|
|
673
|
+
return None;
|
|
674
|
+
}
|
|
675
|
+
octets[octet_index] = value as u8;
|
|
676
|
+
octet_index += 1;
|
|
677
|
+
value = 0;
|
|
678
|
+
digits = 0;
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if !byte.is_ascii_digit() {
|
|
683
|
+
return None;
|
|
684
|
+
}
|
|
685
|
+
if digits == 1 && value == 0 {
|
|
686
|
+
return None;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
digits += 1;
|
|
690
|
+
if digits > 3 {
|
|
691
|
+
return None;
|
|
692
|
+
}
|
|
693
|
+
value = value * 10 + u16::from(byte - b'0');
|
|
694
|
+
if value > u16::from(u8::MAX) {
|
|
695
|
+
return None;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if octet_index != 3 || digits == 0 {
|
|
700
|
+
return None;
|
|
701
|
+
}
|
|
702
|
+
octets[octet_index] = value as u8;
|
|
703
|
+
|
|
704
|
+
Some(Ipv4Addr::from(octets))
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
#[inline]
|
|
708
|
+
fn lookup_result_to_value(
|
|
709
|
+
ruby: &magnus::Ruby,
|
|
710
|
+
result: Result<Option<RubyDecodedValue>, MaxMindDbError>,
|
|
711
|
+
error_context: &str,
|
|
712
|
+
) -> Result<Value, Error> {
|
|
713
|
+
match result {
|
|
714
|
+
Ok(Some(data)) => Ok(data.into_value()),
|
|
715
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
716
|
+
Err(err) => Err(lookup_error(ruby, err, error_context)),
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
#[inline]
|
|
721
|
+
fn lookup_prefix_result_to_array(
|
|
722
|
+
ruby: &magnus::Ruby,
|
|
723
|
+
result: Result<(Option<RubyDecodedValue>, usize), MaxMindDbError>,
|
|
724
|
+
error_context: &str,
|
|
725
|
+
) -> Result<RArray, Error> {
|
|
726
|
+
match result {
|
|
727
|
+
Ok((data, prefix)) => {
|
|
728
|
+
let arr = ruby.ary_new();
|
|
729
|
+
arr.push(data.map_or_else(|| ruby.qnil().as_value(), RubyDecodedValue::into_value))?;
|
|
730
|
+
arr.push(prefix.into_value_with(ruby))?;
|
|
731
|
+
Ok(arr)
|
|
732
|
+
}
|
|
733
|
+
Err(err) => Err(lookup_error(ruby, err, error_context)),
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
#[inline]
|
|
738
|
+
fn lookup_error(ruby: &magnus::Ruby, err: MaxMindDbError, context: &str) -> Error {
|
|
739
|
+
match err {
|
|
740
|
+
MaxMindDbError::InvalidDatabase { .. }
|
|
741
|
+
| MaxMindDbError::Decoding { .. }
|
|
742
|
+
| MaxMindDbError::Io(_) => invalid_database_exception(ERR_BAD_DATA),
|
|
743
|
+
other => Error::new(
|
|
744
|
+
ruby.exception_runtime_error(),
|
|
745
|
+
format!("{}: {}", context, other),
|
|
746
|
+
),
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/// Generate error message for IPv6 in IPv4-only database
|
|
751
|
+
fn ipv6_in_ipv4_error(ip: &IpAddr) -> String {
|
|
752
|
+
format!(
|
|
753
|
+
"Error looking up {}. You attempted to look up an IPv6 address in an IPv4-only database.",
|
|
754
|
+
ip
|
|
755
|
+
)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
fn database_path(database: Value) -> Result<String, Error> {
|
|
759
|
+
RString::try_convert(database)?.to_string()
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
fn database_buffer(database: Value) -> Result<Vec<u8>, Error> {
|
|
763
|
+
let string = RString::try_convert(database)?;
|
|
764
|
+
// SAFETY: the slice is copied into an owned Vec before Ruby can mutate or
|
|
765
|
+
// free the string, and the reader only ever sees the owned bytes.
|
|
766
|
+
Ok(unsafe { string.as_slice() }.to_vec())
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/// Open a MaxMind DB using memory-mapped I/O (MODE_MMAP)
|
|
770
|
+
fn open_database_mmap(path: &str) -> Result<Reader, Error> {
|
|
771
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
772
|
+
let file = open_database_file(path, &ruby)?;
|
|
773
|
+
|
|
774
|
+
// SAFETY: the mapping is read-only, and MODE_MMAP's documented contract
|
|
775
|
+
// requires callers not to modify or truncate the mapped file while this
|
|
776
|
+
// reader is alive. Database updates should atomically replace the path.
|
|
777
|
+
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| {
|
|
778
|
+
Error::new(
|
|
779
|
+
ruby.exception_io_error(),
|
|
780
|
+
format!("Failed to memory-map database file: {}", e),
|
|
781
|
+
)
|
|
782
|
+
})?;
|
|
783
|
+
let reader = MaxMindReader::from_source(mmap).map_err(|_| {
|
|
784
|
+
invalid_database_exception(&format!(
|
|
785
|
+
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
786
|
+
path
|
|
787
|
+
))
|
|
788
|
+
})?;
|
|
789
|
+
|
|
790
|
+
Ok(create_reader(ReaderSource::Mmap(reader)))
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/// Open a MaxMind DB by loading entire file into memory (MODE_MEMORY)
|
|
794
|
+
fn open_database_memory(path: &str) -> Result<Reader, Error> {
|
|
795
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
796
|
+
let mut file = open_database_file(path, &ruby)?;
|
|
797
|
+
|
|
798
|
+
let mut buffer = Vec::new();
|
|
799
|
+
file.read_to_end(&mut buffer).map_err(|e| {
|
|
800
|
+
Error::new(
|
|
801
|
+
ruby.exception_io_error(),
|
|
802
|
+
format!("Failed to read database file: {}", e),
|
|
803
|
+
)
|
|
804
|
+
})?;
|
|
805
|
+
|
|
806
|
+
reader_from_buffer(
|
|
807
|
+
buffer,
|
|
808
|
+
format!(
|
|
809
|
+
"Error opening database file ({}). Is this a valid MaxMind DB file?",
|
|
810
|
+
path
|
|
811
|
+
),
|
|
812
|
+
)
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
fn open_database_buffer(buffer: Vec<u8>) -> Result<Reader, Error> {
|
|
816
|
+
reader_from_buffer(
|
|
817
|
+
buffer,
|
|
818
|
+
"Error opening database from buffer. Is this a valid MaxMind DB file?".to_owned(),
|
|
819
|
+
)
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
fn reader_from_buffer(buffer: Vec<u8>, invalid_message: String) -> Result<Reader, Error> {
|
|
823
|
+
let reader = MaxMindReader::from_source(buffer)
|
|
824
|
+
.map_err(|_| invalid_database_exception(invalid_message.as_str()))?;
|
|
825
|
+
|
|
826
|
+
Ok(create_reader(ReaderSource::Memory(reader)))
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
fn open_database_file(path: &str, ruby: &magnus::Ruby) -> Result<File, Error> {
|
|
830
|
+
File::open(Path::new(path)).map_err(|e| {
|
|
831
|
+
if e.kind() == std::io::ErrorKind::NotFound {
|
|
832
|
+
open_not_found_error(ruby, e)
|
|
833
|
+
} else {
|
|
834
|
+
Error::new(ruby.exception_io_error(), e.to_string())
|
|
835
|
+
}
|
|
836
|
+
})
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
fn open_not_found_error(ruby: &magnus::Ruby, err: std::io::Error) -> Error {
|
|
840
|
+
let errno = ruby
|
|
841
|
+
.class_object()
|
|
842
|
+
.const_get::<_, RModule>("Errno")
|
|
843
|
+
.expect("Errno module should exist");
|
|
844
|
+
let enoent = errno
|
|
845
|
+
.const_get::<_, RClass>("ENOENT")
|
|
846
|
+
.expect("Errno::ENOENT should exist");
|
|
847
|
+
Error::new(
|
|
848
|
+
ExceptionClass::from_value(enoent.as_value())
|
|
849
|
+
.expect("ENOENT should convert to ExceptionClass"),
|
|
850
|
+
err.to_string(),
|
|
851
|
+
)
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/// Get the InvalidDatabaseError class
|
|
855
|
+
fn invalid_database_error() -> RClass {
|
|
856
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby context");
|
|
857
|
+
let rust = rust_module(&ruby);
|
|
858
|
+
rust.const_get::<_, RClass>("InvalidDatabaseError")
|
|
859
|
+
.expect("InvalidDatabaseError class should exist")
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
fn invalid_database_exception(message: &str) -> Error {
|
|
863
|
+
Error::new(
|
|
864
|
+
ExceptionClass::from_value(invalid_database_error().as_value())
|
|
865
|
+
.expect("InvalidDatabaseError should convert to ExceptionClass"),
|
|
866
|
+
message.to_owned(),
|
|
867
|
+
)
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
pub(crate) fn define(ruby: &magnus::Ruby, rust: RModule) -> Result<(), Error> {
|
|
871
|
+
let runtime_error = ruby.exception_runtime_error();
|
|
872
|
+
rust.define_error("InvalidDatabaseError", runtime_error)?;
|
|
873
|
+
|
|
874
|
+
let reader_class = rust.define_class("Reader", ruby.class_object())?;
|
|
875
|
+
reader_class.define_singleton_method("new", magnus::function!(Reader::new, -1))?;
|
|
876
|
+
reader_class.define_method("get", magnus::method!(Reader::get, 1))?;
|
|
877
|
+
reader_class.define_method("get_path", magnus::method!(Reader::get_path, 2))?;
|
|
878
|
+
reader_class.define_method(
|
|
879
|
+
"get_with_prefix_length",
|
|
880
|
+
magnus::method!(Reader::get_with_prefix_length, 1),
|
|
881
|
+
)?;
|
|
882
|
+
reader_class.define_method("get_many", magnus::method!(Reader::get_many, 1))?;
|
|
883
|
+
reader_class.define_method("get_many_path", magnus::method!(Reader::get_many_path, 2))?;
|
|
884
|
+
reader_class.define_method("metadata", magnus::method!(Reader::metadata, 0))?;
|
|
885
|
+
reader_class.define_method("verify", magnus::method!(Reader::verify, 0))?;
|
|
886
|
+
reader_class.define_method("close", magnus::method!(Reader::close, 0))?;
|
|
887
|
+
reader_class.define_method("closed", magnus::method!(Reader::closed, 0))?;
|
|
888
|
+
reader_class.define_method("inspect", magnus::method!(Reader::inspect, 0))?;
|
|
889
|
+
reader_class.define_method("each", magnus::method!(Reader::each, -1))?;
|
|
890
|
+
|
|
891
|
+
let enumerable = ruby.class_object().const_get::<_, RModule>("Enumerable")?;
|
|
892
|
+
reader_class.include_module(enumerable)?;
|
|
893
|
+
|
|
894
|
+
rust.const_set("MODE_AUTO", ruby.to_symbol("MODE_AUTO"))?;
|
|
895
|
+
rust.const_set("MODE_FILE", ruby.to_symbol("MODE_FILE"))?;
|
|
896
|
+
rust.const_set("MODE_MEMORY", ruby.to_symbol("MODE_MEMORY"))?;
|
|
897
|
+
rust.const_set("MODE_MMAP", ruby.to_symbol("MODE_MMAP"))?;
|
|
898
|
+
rust.const_set(
|
|
899
|
+
"MODE_PARAM_IS_BUFFER",
|
|
900
|
+
ruby.to_symbol("MODE_PARAM_IS_BUFFER"),
|
|
901
|
+
)?;
|
|
902
|
+
|
|
903
|
+
Ok(())
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
#[cfg(test)]
|
|
907
|
+
mod tests {
|
|
908
|
+
use super::parse_ipv4_string;
|
|
909
|
+
use std::net::Ipv4Addr;
|
|
910
|
+
|
|
911
|
+
#[test]
|
|
912
|
+
fn parses_strict_ipv4_strings() {
|
|
913
|
+
assert_eq!(
|
|
914
|
+
parse_ipv4_string(b"0.1.2.255"),
|
|
915
|
+
Some(Ipv4Addr::new(0, 1, 2, 255))
|
|
916
|
+
);
|
|
917
|
+
assert_eq!(
|
|
918
|
+
parse_ipv4_string(b"192.0.2.1"),
|
|
919
|
+
Some(Ipv4Addr::new(192, 0, 2, 1))
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
#[test]
|
|
924
|
+
fn rejects_ipv4_strings_that_std_parser_rejects() {
|
|
925
|
+
for value in [
|
|
926
|
+
b"01.2.3.4".as_slice(),
|
|
927
|
+
b"1.02.3.4".as_slice(),
|
|
928
|
+
b"1.2.3.04".as_slice(),
|
|
929
|
+
b"1.2.3".as_slice(),
|
|
930
|
+
b"1.2.3.4.5".as_slice(),
|
|
931
|
+
b"1..2.3".as_slice(),
|
|
932
|
+
b"256.1.1.1".as_slice(),
|
|
933
|
+
b"1.2.3.4 ".as_slice(),
|
|
934
|
+
b" 1.2.3.4".as_slice(),
|
|
935
|
+
b"2001:db8::1".as_slice(),
|
|
936
|
+
] {
|
|
937
|
+
assert_eq!(parse_ipv4_string(value), None);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|