schemurai 2.3.0 → 3.0.1
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/README.md +67 -4
- data/ext/schemurai_native/Cargo.lock +8 -1
- data/ext/schemurai_native/Cargo.toml +2 -1
- data/ext/schemurai_native/glue.c +155 -14
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/content.rs +18 -0
- data/ext/schemurai_native/src/diagnostics.rs +395 -225
- data/ext/schemurai_native/src/enumeration.rs +154 -0
- data/ext/schemurai_native/src/evaluation.rs +219 -0
- data/ext/schemurai_native/src/format.rs +118 -8
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/integer_scan.rs +132 -0
- data/ext/schemurai_native/src/lib.rs +954 -185
- data/ext/schemurai_native/src/memo.rs +1149 -0
- data/ext/schemurai_native/src/memo_scope.rs +240 -0
- data/ext/schemurai_native/src/number.rs +7 -8
- data/ext/schemurai_native/src/pattern.rs +191 -22
- data/ext/schemurai_native/src/scope.rs +243 -33
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +245 -90
- data/lib/schemurai/backend.rb +1 -0
- data/lib/schemurai/evaluator.rb +45 -3
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +100 -0
- data/lib/schemurai/native/evaluator.rb +32 -5
- data/lib/schemurai/schema_domain.rb +12 -7
- data/lib/schemurai/schema_graph.rb +10 -1
- data/lib/schemurai/unique_items.rb +2 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +7 -3
- data/lib/schemurai/vm/evaluator.rb +50 -3
- data/lib/schemurai/vm/limited_evaluator.rb +97 -0
- data/lib/schemurai.rb +74 -27
- data/sig/schemurai.rbs +20 -12
- metadata +17 -4
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
//! Exact reachable scope dependencies, shared across schema components.
|
|
2
|
+
use super::*;
|
|
3
|
+
use std::borrow::Cow;
|
|
4
|
+
use std::collections::HashSet;
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
|
|
7
|
+
const MAX_NAMES: usize = 65536;
|
|
8
|
+
|
|
9
|
+
#[derive(Default)]
|
|
10
|
+
pub(super) struct Projection {
|
|
11
|
+
names: Vec<Option<Arc<[usize]>>>,
|
|
12
|
+
// If materialized closures exceed the budget, retain the linear dependency
|
|
13
|
+
// graph and compute those exact projections on demand, never all names.
|
|
14
|
+
graph: Option<Vec<Vec<usize>>>,
|
|
15
|
+
}
|
|
16
|
+
impl Projection {
|
|
17
|
+
pub(super) fn compile(edges: &[Vec<usize>], parents: &[Vec<usize>], count: usize) -> Self {
|
|
18
|
+
Self::bounded(edges, parents, count, MAX_NAMES)
|
|
19
|
+
}
|
|
20
|
+
fn bounded(edges: &[Vec<usize>], parents: &[Vec<usize>], count: usize, limit: usize) -> Self {
|
|
21
|
+
let mut seen = vec![false; edges.len()];
|
|
22
|
+
let order = graph::postorder(edges, &mut seen);
|
|
23
|
+
let mut component = vec![usize::MAX; edges.len()];
|
|
24
|
+
let mut members = vec![];
|
|
25
|
+
for root in order.into_iter().rev() {
|
|
26
|
+
if component[root] != usize::MAX {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
let id = members.len();
|
|
30
|
+
let mut nodes = vec![];
|
|
31
|
+
let mut stack = vec![root];
|
|
32
|
+
component[root] = id;
|
|
33
|
+
while let Some(node) = stack.pop() {
|
|
34
|
+
nodes.push(node);
|
|
35
|
+
for &parent in &parents[node] {
|
|
36
|
+
if component[parent] == usize::MAX {
|
|
37
|
+
component[parent] = id;
|
|
38
|
+
stack.push(parent);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
members.push(nodes);
|
|
43
|
+
}
|
|
44
|
+
// Kosaraju components are ordered from sources to sinks. Every member
|
|
45
|
+
// of a cycle has the same reachable reference names; solve it once.
|
|
46
|
+
let mut closures: Vec<Option<Arc<[usize]>>> = vec![None; members.len()];
|
|
47
|
+
let mut interned: HashSet<Arc<[usize]>> = HashSet::new();
|
|
48
|
+
let mut slots = 0;
|
|
49
|
+
for (id, nodes) in members.iter().enumerate().rev() {
|
|
50
|
+
let mut names = vec![];
|
|
51
|
+
let mut children = HashSet::new();
|
|
52
|
+
let mut dependencies = HashSet::new();
|
|
53
|
+
let mut complete = true;
|
|
54
|
+
for &node in nodes {
|
|
55
|
+
if node >= count {
|
|
56
|
+
names.push(node - count);
|
|
57
|
+
}
|
|
58
|
+
for &child in &edges[node] {
|
|
59
|
+
let child = component[child];
|
|
60
|
+
if child != id && children.insert(child) {
|
|
61
|
+
match &closures[child] {
|
|
62
|
+
Some(dependency) => {
|
|
63
|
+
if dependencies.insert(dependency.as_ptr()) {
|
|
64
|
+
names.extend(dependency.iter().copied());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
None => complete = false,
|
|
68
|
+
}
|
|
69
|
+
if names.len() > limit {
|
|
70
|
+
names.sort_unstable();
|
|
71
|
+
names.dedup();
|
|
72
|
+
if names.len() > limit {
|
|
73
|
+
complete = false;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// Bound temporary concatenation too. Deduplicate overlapping
|
|
80
|
+
// dependencies before choosing the on-demand representation.
|
|
81
|
+
if names.len() > limit {
|
|
82
|
+
names.sort_unstable();
|
|
83
|
+
names.dedup();
|
|
84
|
+
if names.len() > limit {
|
|
85
|
+
complete = false;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if !complete {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
names.sort_unstable();
|
|
94
|
+
names.dedup();
|
|
95
|
+
if let Some(existing) = interned.get(names.as_slice()) {
|
|
96
|
+
closures[id] = Some(existing.clone());
|
|
97
|
+
} else if names.len() <= limit - slots {
|
|
98
|
+
slots += names.len();
|
|
99
|
+
let names: Arc<[usize]> = names.into();
|
|
100
|
+
interned.insert(names.clone());
|
|
101
|
+
closures[id] = Some(names);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
let names: Vec<_> = component[..count]
|
|
105
|
+
.iter()
|
|
106
|
+
.map(|&id| closures[id].clone())
|
|
107
|
+
.collect();
|
|
108
|
+
let graph = names.iter().any(Option::is_none).then(|| edges.to_vec());
|
|
109
|
+
Self { names, graph }
|
|
110
|
+
}
|
|
111
|
+
pub(super) fn names(
|
|
112
|
+
&self,
|
|
113
|
+
node: usize,
|
|
114
|
+
count: usize,
|
|
115
|
+
anchors: usize,
|
|
116
|
+
guarded: bool,
|
|
117
|
+
budget: &budget::Budget,
|
|
118
|
+
) -> Option<Cow<'_, [usize]>> {
|
|
119
|
+
if self.names.is_empty() {
|
|
120
|
+
// Hand-built engines in key tests have no compiled dependency graph.
|
|
121
|
+
return Some(Cow::Owned((0..anchors + usize::from(guarded)).collect()));
|
|
122
|
+
}
|
|
123
|
+
if let Some(names) = &self.names[node] {
|
|
124
|
+
return Some(Cow::Borrowed(names));
|
|
125
|
+
}
|
|
126
|
+
let graph = self.graph.as_ref()?;
|
|
127
|
+
let mut seen = vec![false; graph.len()];
|
|
128
|
+
let mut stack = vec![node];
|
|
129
|
+
let mut names = vec![];
|
|
130
|
+
seen[node] = true;
|
|
131
|
+
while let Some(node) = stack.pop() {
|
|
132
|
+
if !budget.checkpoint() {
|
|
133
|
+
return None;
|
|
134
|
+
}
|
|
135
|
+
if node >= count {
|
|
136
|
+
names.push(node - count);
|
|
137
|
+
}
|
|
138
|
+
for &child in &graph[node] {
|
|
139
|
+
if !seen[child] {
|
|
140
|
+
seen[child] = true;
|
|
141
|
+
stack.push(child);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
names.sort_unstable();
|
|
146
|
+
Some(Cow::Owned(names))
|
|
147
|
+
}
|
|
148
|
+
pub(super) fn memory_usage(&self) -> usize {
|
|
149
|
+
let mut seen = HashSet::new();
|
|
150
|
+
self.names.capacity() * std::mem::size_of::<Option<Arc<[usize]>>>()
|
|
151
|
+
+ self
|
|
152
|
+
.names
|
|
153
|
+
.iter()
|
|
154
|
+
.flatten()
|
|
155
|
+
.filter(|names| seen.insert(names.as_ptr()))
|
|
156
|
+
.map(|names| (names.len() + 2) * std::mem::size_of::<usize>())
|
|
157
|
+
.sum::<usize>()
|
|
158
|
+
+ self.graph.as_ref().map_or(0, |graph| {
|
|
159
|
+
graph.capacity() * std::mem::size_of::<Vec<usize>>()
|
|
160
|
+
+ graph
|
|
161
|
+
.iter()
|
|
162
|
+
.map(|row| row.capacity() * std::mem::size_of::<usize>())
|
|
163
|
+
.sum::<usize>()
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
#[cfg(test)]
|
|
169
|
+
mod tests {
|
|
170
|
+
use super::*;
|
|
171
|
+
|
|
172
|
+
fn parents(edges: &[Vec<usize>]) -> Vec<Vec<usize>> {
|
|
173
|
+
let mut parents = vec![vec![]; edges.len()];
|
|
174
|
+
for (node, children) in edges.iter().enumerate() {
|
|
175
|
+
for &child in children {
|
|
176
|
+
parents[child].push(node);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
parents
|
|
180
|
+
}
|
|
181
|
+
#[test]
|
|
182
|
+
fn projects_cycles_and_transitive_dynamic_dispatch_targets() {
|
|
183
|
+
// Four nodes, two dynamic names and the recursive dispatch vertex.
|
|
184
|
+
let edges = vec![
|
|
185
|
+
vec![1],
|
|
186
|
+
vec![0, 4],
|
|
187
|
+
vec![5],
|
|
188
|
+
vec![],
|
|
189
|
+
vec![2],
|
|
190
|
+
vec![3],
|
|
191
|
+
vec![0],
|
|
192
|
+
];
|
|
193
|
+
for limit in [0, 1, MAX_NAMES] {
|
|
194
|
+
let projection = Projection::bounded(&edges, &parents(&edges), 4, limit);
|
|
195
|
+
let names = |node| {
|
|
196
|
+
projection
|
|
197
|
+
.names(node, 4, 2, true, &budget::Budget::default())
|
|
198
|
+
.unwrap()
|
|
199
|
+
.into_owned()
|
|
200
|
+
};
|
|
201
|
+
assert_eq!(names(0), [0, 1]);
|
|
202
|
+
assert_eq!(names(1), [0, 1]);
|
|
203
|
+
assert_eq!(names(2), [1]);
|
|
204
|
+
assert!(names(3).is_empty());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
#[test]
|
|
208
|
+
fn retains_recursive_dispatch_only_where_reachable() {
|
|
209
|
+
let edges = vec![vec![1], vec![4], vec![3], vec![], vec![0]];
|
|
210
|
+
let projection = Projection::compile(&edges, &parents(&edges), 3);
|
|
211
|
+
let names = |node| {
|
|
212
|
+
projection
|
|
213
|
+
.names(node, 3, 1, true, &budget::Budget::default())
|
|
214
|
+
.unwrap()
|
|
215
|
+
.into_owned()
|
|
216
|
+
};
|
|
217
|
+
assert_eq!(names(0), [1]);
|
|
218
|
+
assert_eq!(names(1), [1]);
|
|
219
|
+
assert_eq!(names(2), [0]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn shares_equal_closures_and_retains_exact_projection_after_budget_exhaustion() {
|
|
224
|
+
let edges = vec![vec![1, 2], vec![3], vec![3], vec![4], vec![], vec![]];
|
|
225
|
+
let projection = Projection::bounded(&edges, &parents(&edges), 4, MAX_NAMES);
|
|
226
|
+
assert!(projection.graph.is_none());
|
|
227
|
+
assert!(Arc::ptr_eq(
|
|
228
|
+
projection.names[0].as_ref().unwrap(),
|
|
229
|
+
projection.names[3].as_ref().unwrap()
|
|
230
|
+
));
|
|
231
|
+
let projection = Projection::bounded(&edges, &parents(&edges), 4, 0);
|
|
232
|
+
assert!(projection.graph.is_some());
|
|
233
|
+
assert_eq!(
|
|
234
|
+
&*projection
|
|
235
|
+
.names(0, 4, 1, false, &budget::Budget::default())
|
|
236
|
+
.unwrap(),
|
|
237
|
+
&[0]
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -96,14 +96,13 @@ pub(super) fn integer_fingerprint(integer: Value) -> u64 {
|
|
|
96
96
|
let mut words = [0_u64; 16];
|
|
97
97
|
let sign = unsafe { sn_integer1024(integer, words.as_mut_ptr()) };
|
|
98
98
|
if sign.abs() > 1 {
|
|
99
|
-
// No finite Float can equal this magnitude
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return integer_bucket(&words[..len], sign, unsafe { sn_integer_bytes(integer) });
|
|
99
|
+
// No finite Float can equal this magnitude, so the full native digit
|
|
100
|
+
// hash needs no Float normalization. Sampling only low digits makes
|
|
101
|
+
// mixed/nested uniqueItems quadratic for differences above bit 1024
|
|
102
|
+
// and collapses large enum indexes into a single candidate bucket.
|
|
103
|
+
// The adapter hashes the original Bignum without Ruby dispatch or
|
|
104
|
+
// allocation; exact equality still decides every hash collision.
|
|
105
|
+
return mix(big_only_fingerprint(integer).unwrap() ^ TYPE_NUMBER as u64);
|
|
107
106
|
}
|
|
108
107
|
exact_integer_fingerprint(&words, sign)
|
|
109
108
|
}
|
|
@@ -4,29 +4,147 @@
|
|
|
4
4
|
use super::*;
|
|
5
5
|
use regex_automata::{
|
|
6
6
|
Anchored, Input, MatchKind,
|
|
7
|
-
dfa::{Automaton, StartKind, dense},
|
|
8
|
-
meta::Regex,
|
|
7
|
+
dfa::{Automaton, OverlappingState, StartKind, dense},
|
|
8
|
+
meta::{Cache, Regex},
|
|
9
9
|
nfa::thompson,
|
|
10
10
|
};
|
|
11
11
|
use regex_syntax::ast::{Ast, RepetitionKind, RepetitionRange};
|
|
12
12
|
|
|
13
|
+
// Each validator owns its search caches. Compiled regex/DFA data stays shared.
|
|
14
|
+
#[derive(Default)]
|
|
15
|
+
pub struct Caches(std::cell::RefCell<Vec<Option<Box<Cache>>>>);
|
|
16
|
+
impl Caches {
|
|
17
|
+
fn is_match(&self, index: usize, regex: &Regex, bytes: &[u8]) -> bool {
|
|
18
|
+
// Each runner is tied to one engine, whose regex indices are fixed at
|
|
19
|
+
// compilation. Allocate only the caches this validator actually uses.
|
|
20
|
+
let mut caches = self.0.borrow_mut();
|
|
21
|
+
if caches.len() <= index {
|
|
22
|
+
caches.resize_with(index + 1, || None);
|
|
23
|
+
}
|
|
24
|
+
let cache = caches[index].get_or_insert_with(|| Box::new(regex.create_cache()));
|
|
25
|
+
regex
|
|
26
|
+
.search_half_with(cache, &Input::new(bytes).earliest(true))
|
|
27
|
+
.is_some()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
#[unsafe(no_mangle)]
|
|
31
|
+
pub extern "C" fn sn_caches_new() -> *mut Caches {
|
|
32
|
+
Box::into_raw(Box::new(Caches::default()))
|
|
33
|
+
}
|
|
34
|
+
/// # Safety
|
|
35
|
+
/// Caches must be owned, returned by sn_caches_new, and freed exactly once.
|
|
36
|
+
#[unsafe(no_mangle)]
|
|
37
|
+
pub unsafe extern "C" fn sn_caches_free(caches: *mut Caches) {
|
|
38
|
+
drop(unsafe { Box::from_raw(caches) });
|
|
39
|
+
}
|
|
40
|
+
/// # Safety
|
|
41
|
+
/// Caches must be live; call under the GVL or while all Ractors are stopped.
|
|
42
|
+
#[unsafe(no_mangle)]
|
|
43
|
+
pub unsafe extern "C" fn sn_caches_size(caches: *const Caches) -> usize {
|
|
44
|
+
// GC may inspect a stopped runner while its search has borrowed a cache.
|
|
45
|
+
let Ok(caches) = (unsafe { &*caches }).0.try_borrow() else {
|
|
46
|
+
return std::mem::size_of::<Caches>();
|
|
47
|
+
};
|
|
48
|
+
std::mem::size_of::<Caches>()
|
|
49
|
+
+ caches.capacity() * std::mem::size_of::<Option<Box<Cache>>>()
|
|
50
|
+
+ caches
|
|
51
|
+
.iter()
|
|
52
|
+
.flatten()
|
|
53
|
+
.map(|cache| std::mem::size_of::<Cache>() + cache.memory_usage())
|
|
54
|
+
.sum::<usize>()
|
|
55
|
+
}
|
|
56
|
+
|
|
13
57
|
#[derive(Debug)]
|
|
14
58
|
pub(super) enum Pattern {
|
|
15
59
|
Unsupported,
|
|
16
60
|
Dfa(Box<dense::DFA<Vec<u32>>>),
|
|
17
|
-
Compiled
|
|
61
|
+
Compiled { regex: Regex, index: usize },
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Preserve every overlapping match while scanning a key with one automaton.
|
|
65
|
+
// Keep individual matchers for unsupported compilation/search configurations.
|
|
66
|
+
#[derive(Debug)]
|
|
67
|
+
pub(super) struct PatternGroup {
|
|
68
|
+
dfa: dense::DFA<Vec<u32>>,
|
|
69
|
+
all: u64,
|
|
70
|
+
}
|
|
71
|
+
impl PatternGroup {
|
|
72
|
+
pub(super) fn compile(names: &[Value], patterns: &[(Pattern, usize)]) -> Option<Self> {
|
|
73
|
+
if !(2..=64).contains(&patterns.len())
|
|
74
|
+
|| !patterns
|
|
75
|
+
.iter()
|
|
76
|
+
.all(|(pattern, _)| matches!(pattern, Pattern::Dfa(_)))
|
|
77
|
+
{
|
|
78
|
+
return None;
|
|
79
|
+
}
|
|
80
|
+
// Existing DFA compilation already proved syntax, encoding, compatibility
|
|
81
|
+
// and full start anchoring. Reuse exactly the same translation.
|
|
82
|
+
let sources: Option<Vec<_>> = names
|
|
83
|
+
.iter()
|
|
84
|
+
.map(|&name| {
|
|
85
|
+
std::str::from_utf8(unsafe { string_bytes(name) })
|
|
86
|
+
.ok()
|
|
87
|
+
.and_then(translate)
|
|
88
|
+
})
|
|
89
|
+
.collect();
|
|
90
|
+
let dfa = dense::Builder::new()
|
|
91
|
+
.configure(
|
|
92
|
+
dense::Config::new()
|
|
93
|
+
.match_kind(MatchKind::All)
|
|
94
|
+
.start_kind(StartKind::Anchored)
|
|
95
|
+
.dfa_size_limit(Some(1 << 20))
|
|
96
|
+
.determinize_size_limit(Some(1 << 22)),
|
|
97
|
+
)
|
|
98
|
+
.thompson(thompson::Config::new().nfa_size_limit(Some(1 << 20)))
|
|
99
|
+
.build_many(&sources?)
|
|
100
|
+
.ok()?;
|
|
101
|
+
let all = u64::MAX >> (64 - patterns.len());
|
|
102
|
+
Some(Self { dfa, all })
|
|
103
|
+
}
|
|
104
|
+
pub(super) fn matches(&self, value: Value) -> Option<u64> {
|
|
105
|
+
let mut data = std::ptr::null();
|
|
106
|
+
let mut len = 0;
|
|
107
|
+
let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
|
|
108
|
+
if encoding == ENCODING_OTHER {
|
|
109
|
+
return None;
|
|
110
|
+
}
|
|
111
|
+
let bytes = unsafe { byte_view(data, len) };
|
|
112
|
+
if !valid_encoding(bytes, encoding) {
|
|
113
|
+
return None;
|
|
114
|
+
}
|
|
115
|
+
let input = Input::new(bytes).anchored(Anchored::Yes);
|
|
116
|
+
let mut state = OverlappingState::start();
|
|
117
|
+
let mut matches = 0;
|
|
118
|
+
loop {
|
|
119
|
+
self.dfa
|
|
120
|
+
.try_search_overlapping_fwd(&input, &mut state)
|
|
121
|
+
.ok()?;
|
|
122
|
+
let Some(found) = state.get_match() else {
|
|
123
|
+
break;
|
|
124
|
+
};
|
|
125
|
+
matches |= 1_u64 << found.pattern().as_usize();
|
|
126
|
+
if matches == self.all {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
Some(matches)
|
|
131
|
+
}
|
|
132
|
+
pub(super) fn memory_usage(&self) -> usize {
|
|
133
|
+
self.dfa.memory_usage()
|
|
134
|
+
}
|
|
18
135
|
}
|
|
19
136
|
|
|
20
137
|
impl Pattern {
|
|
21
138
|
// Select the crate matcher once for a homogeneous array. The borrowed
|
|
22
139
|
// Ruby slices are used synchronously; classification and encoding are
|
|
23
140
|
// still checked on every element, including non-string instances.
|
|
24
|
-
pub(super) fn eval_items(&self, required: i32, values: &[Value]) -> Verdict {
|
|
25
|
-
self.visit_invalid_items(required, values, |_, _| Invalid)
|
|
141
|
+
pub(super) fn eval_items(&self, caches: &Caches, required: i32, values: &[Value]) -> Verdict {
|
|
142
|
+
self.visit_invalid_items(caches, required, values, |_, _| Invalid)
|
|
26
143
|
}
|
|
27
144
|
|
|
28
145
|
pub(super) fn visit_invalid_items(
|
|
29
146
|
&self,
|
|
147
|
+
caches: &Caches,
|
|
30
148
|
required: i32,
|
|
31
149
|
values: &[Value],
|
|
32
150
|
invalid: impl FnMut(usize, Value) -> Verdict,
|
|
@@ -39,8 +157,8 @@ impl Pattern {
|
|
|
39
157
|
Err(_) => Unsupported,
|
|
40
158
|
}
|
|
41
159
|
}),
|
|
42
|
-
Self::Compiled
|
|
43
|
-
verdict(
|
|
160
|
+
Self::Compiled { regex, index } => scan_items(required, values, invalid, |bytes| {
|
|
161
|
+
verdict(caches.is_match(*index, regex, bytes))
|
|
44
162
|
}),
|
|
45
163
|
Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
|
|
46
164
|
}
|
|
@@ -49,12 +167,12 @@ impl Pattern {
|
|
|
49
167
|
pub(super) fn memory_usage(&self) -> usize {
|
|
50
168
|
match self {
|
|
51
169
|
Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
|
|
52
|
-
Self::Compiled
|
|
170
|
+
Self::Compiled { regex, .. } => regex.memory_usage(),
|
|
53
171
|
Self::Unsupported => 0,
|
|
54
172
|
}
|
|
55
173
|
}
|
|
56
174
|
|
|
57
|
-
pub(super) fn compile(value: Value) -> Self {
|
|
175
|
+
pub(super) fn compile(value: Value, cache_count: &mut usize) -> Self {
|
|
58
176
|
if unsafe { sn_kind(value) } != TYPE_STRING {
|
|
59
177
|
return Self::Unsupported;
|
|
60
178
|
}
|
|
@@ -105,11 +223,15 @@ impl Pattern {
|
|
|
105
223
|
.hybrid_cache_capacity(1 << 20),
|
|
106
224
|
)
|
|
107
225
|
.build(&source)
|
|
108
|
-
.map(
|
|
226
|
+
.map(|regex| {
|
|
227
|
+
let index = *cache_count;
|
|
228
|
+
*cache_count += 1;
|
|
229
|
+
Self::Compiled { regex, index }
|
|
230
|
+
})
|
|
109
231
|
.unwrap_or(Self::Unsupported)
|
|
110
232
|
}
|
|
111
233
|
|
|
112
|
-
pub(super) fn eval(&self, value: Value) -> Verdict {
|
|
234
|
+
pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
|
|
113
235
|
if matches!(self, Self::Unsupported) {
|
|
114
236
|
return Unsupported;
|
|
115
237
|
}
|
|
@@ -120,17 +242,14 @@ impl Pattern {
|
|
|
120
242
|
return Unsupported;
|
|
121
243
|
}
|
|
122
244
|
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
123
|
-
self.eval_bytes(bytes, encoding)
|
|
245
|
+
self.eval_bytes(caches, bytes, encoding)
|
|
124
246
|
}
|
|
125
247
|
|
|
126
|
-
pub(super) fn eval_bytes(&self, bytes: &[u8], encoding: i32) -> Verdict {
|
|
248
|
+
pub(super) fn eval_bytes(&self, caches: &Caches, bytes: &[u8], encoding: i32) -> Verdict {
|
|
127
249
|
if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
|
|
128
250
|
return Unsupported;
|
|
129
251
|
}
|
|
130
|
-
if encoding
|
|
131
|
-
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
132
|
-
|| std::str::from_utf8(bytes).is_err())
|
|
133
|
-
{
|
|
252
|
+
if !valid_encoding(bytes, encoding) {
|
|
134
253
|
return Unsupported;
|
|
135
254
|
}
|
|
136
255
|
let matched = match self {
|
|
@@ -140,13 +259,27 @@ impl Pattern {
|
|
|
140
259
|
Ok(matched) => matched.is_some(),
|
|
141
260
|
Err(_) => return Unsupported,
|
|
142
261
|
},
|
|
143
|
-
Self::Compiled
|
|
262
|
+
Self::Compiled { regex, index } => caches.is_match(*index, regex, bytes),
|
|
144
263
|
Self::Unsupported => return Unsupported,
|
|
145
264
|
};
|
|
146
265
|
if matched { Valid } else { Invalid }
|
|
147
266
|
}
|
|
148
267
|
}
|
|
149
268
|
|
|
269
|
+
// Keep short checks in std and skip scanning when Ruby already proved ASCII.
|
|
270
|
+
// The compatibility SIMD API retains early rejection of malformed UTF-8.
|
|
271
|
+
fn valid_encoding(bytes: &[u8], encoding: i32) -> bool {
|
|
272
|
+
if encoding == ENCODING_SEVEN_BIT {
|
|
273
|
+
true
|
|
274
|
+
} else if encoding == ENCODING_ASCII {
|
|
275
|
+
bytes.is_ascii()
|
|
276
|
+
} else if bytes.len() < 64 {
|
|
277
|
+
std::str::from_utf8(bytes).is_ok()
|
|
278
|
+
} else {
|
|
279
|
+
simdutf8::compat::from_utf8(bytes).is_ok()
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
150
283
|
fn scan_items(
|
|
151
284
|
required: i32,
|
|
152
285
|
values: &[Value],
|
|
@@ -173,10 +306,7 @@ fn scan_items(
|
|
|
173
306
|
return Unsupported;
|
|
174
307
|
}
|
|
175
308
|
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
176
|
-
if encoding
|
|
177
|
-
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
178
|
-
|| std::str::from_utf8(bytes).is_err())
|
|
179
|
-
{
|
|
309
|
+
if !valid_encoding(bytes, encoding) {
|
|
180
310
|
return Unsupported;
|
|
181
311
|
}
|
|
182
312
|
let result = matches(bytes);
|
|
@@ -286,3 +416,42 @@ fn compatible(ast: &Ast) -> bool {
|
|
|
286
416
|
_ => true,
|
|
287
417
|
}
|
|
288
418
|
}
|
|
419
|
+
|
|
420
|
+
#[cfg(test)]
|
|
421
|
+
mod tests {
|
|
422
|
+
use super::*;
|
|
423
|
+
|
|
424
|
+
#[test]
|
|
425
|
+
fn encoding_validation_matches_std_across_simd_blocks_and_tails() {
|
|
426
|
+
let original = "日𐀀é".repeat(32).into_bytes();
|
|
427
|
+
for length in [63, 64, 65, 127, 128, 129, original.len()] {
|
|
428
|
+
for at in 0..length {
|
|
429
|
+
for byte in 0..=255 {
|
|
430
|
+
let mut input = original[..length].to_vec();
|
|
431
|
+
input[at] = byte;
|
|
432
|
+
assert_eq!(
|
|
433
|
+
valid_encoding(&input, ENCODING_UTF8),
|
|
434
|
+
std::str::from_utf8(&input).is_ok(),
|
|
435
|
+
"length {length}, position {at}, byte {byte}"
|
|
436
|
+
);
|
|
437
|
+
assert_eq!(valid_encoding(&input, ENCODING_ASCII), input.is_ascii());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
assert!(valid_encoding(&original, ENCODING_UTF8));
|
|
442
|
+
assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn reports_memory_when_a_search_cache_is_borrowed() {
|
|
447
|
+
let caches = Caches::default();
|
|
448
|
+
let regex = Regex::new("value[0-9]+").unwrap();
|
|
449
|
+
assert!(caches.is_match(0, ®ex, b"value1"));
|
|
450
|
+
assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
|
|
451
|
+
let _search = caches.0.borrow_mut();
|
|
452
|
+
assert_eq!(
|
|
453
|
+
unsafe { sn_caches_size(&caches) },
|
|
454
|
+
std::mem::size_of::<Caches>()
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
}
|