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.
@@ -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
+ }