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,123 @@
|
|
|
1
|
+
use magnus::{error::Error, prelude::*, RHash, RModule};
|
|
2
|
+
use std::collections::BTreeMap;
|
|
3
|
+
|
|
4
|
+
/// Metadata about the MaxMind DB database.
|
|
5
|
+
#[derive(Clone)]
|
|
6
|
+
#[magnus::wrap(class = "MaxMind::DB::Rust::Metadata")]
|
|
7
|
+
pub(crate) struct Metadata {
|
|
8
|
+
/// The major version number of the binary format used when creating the database.
|
|
9
|
+
binary_format_major_version: u16,
|
|
10
|
+
/// The minor version number of the binary format used when creating the database.
|
|
11
|
+
binary_format_minor_version: u16,
|
|
12
|
+
/// The Unix epoch timestamp for when the database was built.
|
|
13
|
+
build_epoch: u64,
|
|
14
|
+
/// A string identifying the database type (e.g., 'GeoIP2-City', 'GeoLite2-Country').
|
|
15
|
+
database_type: String,
|
|
16
|
+
description_map: BTreeMap<String, String>,
|
|
17
|
+
/// The IP version of the data in a database. A value of 4 means IPv4 only; 6 supports both IPv4 and IPv6.
|
|
18
|
+
ip_version: u16,
|
|
19
|
+
languages_list: Vec<String>,
|
|
20
|
+
/// The number of nodes in the search tree.
|
|
21
|
+
node_count: u32,
|
|
22
|
+
/// The record size in bits (24, 28, or 32).
|
|
23
|
+
record_size: u16,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl Metadata {
|
|
27
|
+
pub(crate) fn from_maxmind(meta: &maxminddb::Metadata) -> Self {
|
|
28
|
+
Self {
|
|
29
|
+
binary_format_major_version: meta.binary_format_major_version,
|
|
30
|
+
binary_format_minor_version: meta.binary_format_minor_version,
|
|
31
|
+
build_epoch: meta.build_epoch,
|
|
32
|
+
database_type: meta.database_type.clone(),
|
|
33
|
+
description_map: meta.description.clone(),
|
|
34
|
+
ip_version: meta.ip_version,
|
|
35
|
+
languages_list: meta.languages.clone(),
|
|
36
|
+
node_count: meta.node_count,
|
|
37
|
+
record_size: meta.record_size,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn binary_format_major_version(&self) -> u16 {
|
|
42
|
+
self.binary_format_major_version
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn binary_format_minor_version(&self) -> u16 {
|
|
46
|
+
self.binary_format_minor_version
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn build_epoch(&self) -> u64 {
|
|
50
|
+
self.build_epoch
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn database_type(&self) -> String {
|
|
54
|
+
self.database_type.clone()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn description(&self) -> Result<RHash, Error> {
|
|
58
|
+
let ruby = magnus::Ruby::get().expect("Ruby VM should be available in Ruby method");
|
|
59
|
+
let hash = ruby.hash_new();
|
|
60
|
+
for (k, v) in &self.description_map {
|
|
61
|
+
hash.aset(k.as_str(), v.as_str())?;
|
|
62
|
+
}
|
|
63
|
+
Ok(hash)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn ip_version(&self) -> u16 {
|
|
67
|
+
self.ip_version
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn languages(&self) -> Vec<String> {
|
|
71
|
+
self.languages_list.clone()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn node_count(&self) -> u32 {
|
|
75
|
+
self.node_count
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn record_size(&self) -> u16 {
|
|
79
|
+
self.record_size
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn node_byte_size(&self) -> u16 {
|
|
83
|
+
self.record_size / 4
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn search_tree_size(&self) -> u32 {
|
|
87
|
+
self.node_count * (self.record_size as u32 / 4)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// SAFETY: Metadata stores only owned Rust values copied out of the database
|
|
92
|
+
// metadata. It contains no Ruby VALUE handles or borrowed database/source data,
|
|
93
|
+
// so moving it between Ruby-managed threads cannot invalidate GC or lifetime
|
|
94
|
+
// assumptions.
|
|
95
|
+
unsafe impl Send for Metadata {}
|
|
96
|
+
|
|
97
|
+
pub(crate) fn define(ruby: &magnus::Ruby, rust: RModule) -> Result<(), Error> {
|
|
98
|
+
let metadata_class = rust.define_class("Metadata", ruby.class_object())?;
|
|
99
|
+
metadata_class.define_method(
|
|
100
|
+
"binary_format_major_version",
|
|
101
|
+
magnus::method!(Metadata::binary_format_major_version, 0),
|
|
102
|
+
)?;
|
|
103
|
+
metadata_class.define_method(
|
|
104
|
+
"binary_format_minor_version",
|
|
105
|
+
magnus::method!(Metadata::binary_format_minor_version, 0),
|
|
106
|
+
)?;
|
|
107
|
+
metadata_class.define_method("build_epoch", magnus::method!(Metadata::build_epoch, 0))?;
|
|
108
|
+
metadata_class.define_method("database_type", magnus::method!(Metadata::database_type, 0))?;
|
|
109
|
+
metadata_class.define_method("description", magnus::method!(Metadata::description, 0))?;
|
|
110
|
+
metadata_class.define_method("ip_version", magnus::method!(Metadata::ip_version, 0))?;
|
|
111
|
+
metadata_class.define_method("languages", magnus::method!(Metadata::languages, 0))?;
|
|
112
|
+
metadata_class.define_method("node_count", magnus::method!(Metadata::node_count, 0))?;
|
|
113
|
+
metadata_class.define_method("record_size", magnus::method!(Metadata::record_size, 0))?;
|
|
114
|
+
metadata_class.define_method(
|
|
115
|
+
"node_byte_size",
|
|
116
|
+
magnus::method!(Metadata::node_byte_size, 0),
|
|
117
|
+
)?;
|
|
118
|
+
metadata_class.define_method(
|
|
119
|
+
"search_tree_size",
|
|
120
|
+
magnus::method!(Metadata::search_tree_size, 0),
|
|
121
|
+
)?;
|
|
122
|
+
Ok(())
|
|
123
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
use magnus::{error::Error, prelude::*, RArray, RString, Value};
|
|
2
|
+
use maxminddb::PathElement;
|
|
3
|
+
use rustc_hash::FxHasher;
|
|
4
|
+
use std::{
|
|
5
|
+
hash::{Hash, Hasher},
|
|
6
|
+
sync::Arc,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
pub(crate) const PATH_CACHE_MAX_ENTRIES: usize = 64;
|
|
10
|
+
|
|
11
|
+
#[derive(PartialEq, Eq)]
|
|
12
|
+
pub(crate) enum OwnedPathElement {
|
|
13
|
+
Key(String),
|
|
14
|
+
Index(usize),
|
|
15
|
+
IndexFromEnd(usize),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub(crate) struct CachedPath {
|
|
19
|
+
pub(crate) hash: u64,
|
|
20
|
+
pub(crate) elements: Arc<[OwnedPathElement]>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
pub(crate) fn path_array(path: Value, ruby: &magnus::Ruby) -> Result<RArray, Error> {
|
|
24
|
+
RArray::try_convert(path).map_err(|_| {
|
|
25
|
+
Error::new(
|
|
26
|
+
ruby.exception_arg_error(),
|
|
27
|
+
"Path must be an Array of String and Integer elements",
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub(crate) fn parse_path_array(
|
|
33
|
+
path: RArray,
|
|
34
|
+
ruby: &magnus::Ruby,
|
|
35
|
+
) -> Result<Vec<OwnedPathElement>, Error> {
|
|
36
|
+
let mut elements = Vec::with_capacity(path.len());
|
|
37
|
+
for index in 0..path.len() {
|
|
38
|
+
let item = path.entry::<Value>(index as isize)?;
|
|
39
|
+
if let Ok(key) = RString::try_convert(item) {
|
|
40
|
+
elements.push(OwnedPathElement::Key(key.to_string()?));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if let Ok(index) = isize::try_convert(item) {
|
|
44
|
+
elements.push(signed_index_to_owned_path_element(index));
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
return Err(Error::new(
|
|
48
|
+
ruby.exception_arg_error(),
|
|
49
|
+
"Path elements must be Strings or Integers",
|
|
50
|
+
));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Ok(elements)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#[inline]
|
|
57
|
+
fn signed_index_to_owned_path_element(index: isize) -> OwnedPathElement {
|
|
58
|
+
if index >= 0 {
|
|
59
|
+
OwnedPathElement::Index(index as usize)
|
|
60
|
+
} else {
|
|
61
|
+
let index_from_end = index
|
|
62
|
+
.checked_neg()
|
|
63
|
+
.and_then(|index| index.checked_sub(1))
|
|
64
|
+
.map(|index| index as usize)
|
|
65
|
+
.unwrap_or(usize::MAX);
|
|
66
|
+
OwnedPathElement::IndexFromEnd(index_from_end)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
pub(crate) fn path_cache_hash(path: RArray, ruby: &magnus::Ruby) -> Result<u64, Error> {
|
|
71
|
+
let mut hasher = FxHasher::default();
|
|
72
|
+
path.len().hash(&mut hasher);
|
|
73
|
+
|
|
74
|
+
for index in 0..path.len() {
|
|
75
|
+
let item = path.entry::<Value>(index as isize)?;
|
|
76
|
+
if let Ok(key) = RString::try_convert(item) {
|
|
77
|
+
0_u8.hash(&mut hasher);
|
|
78
|
+
hash_path_key(key, &mut hasher)?;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if let Ok(index) = isize::try_convert(item) {
|
|
82
|
+
1_u8.hash(&mut hasher);
|
|
83
|
+
index.hash(&mut hasher);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
return Err(Error::new(
|
|
87
|
+
ruby.exception_arg_error(),
|
|
88
|
+
"Path elements must be Strings or Integers",
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
Ok(hasher.finish())
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn hash_path_key(key: RString, hasher: &mut FxHasher) -> Result<(), Error> {
|
|
96
|
+
// SAFETY: the borrowed str is used only for immediate hashing and is not
|
|
97
|
+
// stored across any call that could mutate or free the Ruby string.
|
|
98
|
+
if let Some(key_str) = unsafe { key.test_as_str() } {
|
|
99
|
+
key_str.hash(hasher);
|
|
100
|
+
} else {
|
|
101
|
+
key.to_string()?.hash(hasher);
|
|
102
|
+
}
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub(crate) fn path_matches_cached(
|
|
107
|
+
path: RArray,
|
|
108
|
+
cached: &[OwnedPathElement],
|
|
109
|
+
) -> Result<bool, Error> {
|
|
110
|
+
if path.len() != cached.len() {
|
|
111
|
+
return Ok(false);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
for (index, cached_element) in cached.iter().enumerate() {
|
|
115
|
+
let item = path.entry::<Value>(index as isize)?;
|
|
116
|
+
match cached_element {
|
|
117
|
+
OwnedPathElement::Key(expected) => {
|
|
118
|
+
let Ok(key) = RString::try_convert(item) else {
|
|
119
|
+
return Ok(false);
|
|
120
|
+
};
|
|
121
|
+
if !path_key_matches(key, expected)? {
|
|
122
|
+
return Ok(false);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
OwnedPathElement::Index(_) | OwnedPathElement::IndexFromEnd(_) => {
|
|
126
|
+
let Ok(index) = isize::try_convert(item) else {
|
|
127
|
+
return Ok(false);
|
|
128
|
+
};
|
|
129
|
+
if signed_index_to_owned_path_element(index) != *cached_element {
|
|
130
|
+
return Ok(false);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Ok(true)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn path_key_matches(key: RString, expected: &str) -> Result<bool, Error> {
|
|
140
|
+
// SAFETY: the borrowed str is used only for immediate comparison and is not
|
|
141
|
+
// stored across any call that could mutate or free the Ruby string.
|
|
142
|
+
if let Some(key_str) = unsafe { key.test_as_str() } {
|
|
143
|
+
Ok(key_str == expected)
|
|
144
|
+
} else {
|
|
145
|
+
Ok(key.to_string()? == expected)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub(crate) fn path_elements_from_owned_path(path: &[OwnedPathElement]) -> Vec<PathElement<'_>> {
|
|
150
|
+
path.iter()
|
|
151
|
+
.map(|element| match element {
|
|
152
|
+
OwnedPathElement::Key(key) => PathElement::Key(key.as_str()),
|
|
153
|
+
OwnedPathElement::Index(index) => PathElement::Index(*index),
|
|
154
|
+
OwnedPathElement::IndexFromEnd(index) => PathElement::IndexFromEnd(*index),
|
|
155
|
+
})
|
|
156
|
+
.collect()
|
|
157
|
+
}
|