schemurai 2.2.3 → 3.0.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/README.md +81 -7
- data/ext/schemurai_native/Cargo.lock +99 -0
- data/ext/schemurai_native/Cargo.toml +21 -0
- data/ext/schemurai_native/build.rs +35 -0
- data/ext/schemurai_native/extconf.rb +63 -0
- data/ext/schemurai_native/glue.c +780 -0
- data/ext/schemurai_native/protocol.def +63 -0
- data/ext/schemurai_native/protocol.h +10 -0
- data/ext/schemurai_native/src/budget.rs +101 -0
- data/ext/schemurai_native/src/content.rs +574 -0
- data/ext/schemurai_native/src/diagnostics.rs +1135 -0
- 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 +357 -0
- data/ext/schemurai_native/src/graph.rs +50 -0
- data/ext/schemurai_native/src/instance_graph.rs +84 -0
- data/ext/schemurai_native/src/lib.rs +3139 -0
- 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 +1054 -0
- data/ext/schemurai_native/src/pattern.rs +431 -0
- data/ext/schemurai_native/src/scope.rs +420 -0
- data/ext/schemurai_native/src/structural.rs +86 -0
- data/ext/schemurai_native/src/tracking.rs +687 -0
- data/lib/schemurai/backend.rb +3 -1
- 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/diagnostics.rb +52 -0
- data/lib/schemurai/native/evaluator.rb +59 -0
- 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 +81 -33
- data/sig/schemurai.rbs +21 -13
- metadata +36 -6
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
//! Crate-backed matching over borrowed Ruby string bytes.
|
|
2
|
+
//! The compatibility adapter translates VM shorthands and absolute anchors;
|
|
3
|
+
//! regex-syntax parses syntax, and regex-automata performs every search.
|
|
4
|
+
use super::*;
|
|
5
|
+
use regex_automata::{
|
|
6
|
+
Anchored, Input, MatchKind,
|
|
7
|
+
dfa::{Automaton, OverlappingState, StartKind, dense},
|
|
8
|
+
meta::{Cache, Regex},
|
|
9
|
+
nfa::thompson,
|
|
10
|
+
};
|
|
11
|
+
use regex_syntax::ast::{Ast, RepetitionKind, RepetitionRange};
|
|
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
|
+
|
|
57
|
+
#[derive(Debug)]
|
|
58
|
+
pub(super) enum Pattern {
|
|
59
|
+
Unsupported,
|
|
60
|
+
Dfa(Box<dense::DFA<Vec<u32>>>),
|
|
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 encoding != ENCODING_SEVEN_BIT
|
|
113
|
+
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
114
|
+
|| std::str::from_utf8(bytes).is_err())
|
|
115
|
+
{
|
|
116
|
+
return None;
|
|
117
|
+
}
|
|
118
|
+
let input = Input::new(bytes).anchored(Anchored::Yes);
|
|
119
|
+
let mut state = OverlappingState::start();
|
|
120
|
+
let mut matches = 0;
|
|
121
|
+
loop {
|
|
122
|
+
self.dfa
|
|
123
|
+
.try_search_overlapping_fwd(&input, &mut state)
|
|
124
|
+
.ok()?;
|
|
125
|
+
let Some(found) = state.get_match() else {
|
|
126
|
+
break;
|
|
127
|
+
};
|
|
128
|
+
matches |= 1_u64 << found.pattern().as_usize();
|
|
129
|
+
if matches == self.all {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
Some(matches)
|
|
134
|
+
}
|
|
135
|
+
pub(super) fn memory_usage(&self) -> usize {
|
|
136
|
+
self.dfa.memory_usage()
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl Pattern {
|
|
141
|
+
// Select the crate matcher once for a homogeneous array. The borrowed
|
|
142
|
+
// Ruby slices are used synchronously; classification and encoding are
|
|
143
|
+
// still checked on every element, including non-string instances.
|
|
144
|
+
pub(super) fn eval_items(&self, caches: &Caches, required: i32, values: &[Value]) -> Verdict {
|
|
145
|
+
self.visit_invalid_items(caches, required, values, |_, _| Invalid)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pub(super) fn visit_invalid_items(
|
|
149
|
+
&self,
|
|
150
|
+
caches: &Caches,
|
|
151
|
+
required: i32,
|
|
152
|
+
values: &[Value],
|
|
153
|
+
invalid: impl FnMut(usize, Value) -> Verdict,
|
|
154
|
+
) -> Verdict {
|
|
155
|
+
match self {
|
|
156
|
+
Self::Dfa(dfa) => scan_items(required, values, invalid, |bytes| {
|
|
157
|
+
match dfa.try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
|
|
158
|
+
{
|
|
159
|
+
Ok(matched) => verdict(matched.is_some()),
|
|
160
|
+
Err(_) => Unsupported,
|
|
161
|
+
}
|
|
162
|
+
}),
|
|
163
|
+
Self::Compiled { regex, index } => scan_items(required, values, invalid, |bytes| {
|
|
164
|
+
verdict(caches.is_match(*index, regex, bytes))
|
|
165
|
+
}),
|
|
166
|
+
Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pub(super) fn memory_usage(&self) -> usize {
|
|
171
|
+
match self {
|
|
172
|
+
Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
|
|
173
|
+
Self::Compiled { regex, .. } => regex.memory_usage(),
|
|
174
|
+
Self::Unsupported => 0,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pub(super) fn compile(value: Value, cache_count: &mut usize) -> Self {
|
|
179
|
+
if unsafe { sn_kind(value) } != TYPE_STRING {
|
|
180
|
+
return Self::Unsupported;
|
|
181
|
+
}
|
|
182
|
+
let mut data = std::ptr::null();
|
|
183
|
+
let mut len = 0;
|
|
184
|
+
let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
|
|
185
|
+
if encoding == ENCODING_OTHER {
|
|
186
|
+
return Self::Unsupported;
|
|
187
|
+
}
|
|
188
|
+
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
189
|
+
if encoding == ENCODING_ASCII && !bytes.is_ascii() {
|
|
190
|
+
return Self::Unsupported;
|
|
191
|
+
}
|
|
192
|
+
let Some(source) = std::str::from_utf8(bytes).ok().and_then(translate) else {
|
|
193
|
+
return Self::Unsupported;
|
|
194
|
+
};
|
|
195
|
+
let Ok(ast) = regex_syntax::ast::parse::Parser::new().parse(&source) else {
|
|
196
|
+
return Self::Unsupported;
|
|
197
|
+
};
|
|
198
|
+
if !compatible(&ast) {
|
|
199
|
+
return Self::Unsupported;
|
|
200
|
+
}
|
|
201
|
+
// Use a single immutable DFA for fully start-anchored validity. The
|
|
202
|
+
// meta engine retains its literal prefilters for unanchored searches.
|
|
203
|
+
// Bound both the DFA table and auxiliary determinization memory.
|
|
204
|
+
if let Ok(hir) = regex_syntax::hir::translate::Translator::new().translate(&source, &ast)
|
|
205
|
+
&& hir
|
|
206
|
+
.properties()
|
|
207
|
+
.look_set_prefix()
|
|
208
|
+
.contains(regex_syntax::hir::Look::Start)
|
|
209
|
+
&& let Ok(dfa) = dense::Builder::new()
|
|
210
|
+
.configure(
|
|
211
|
+
dense::Config::new()
|
|
212
|
+
.match_kind(MatchKind::LeftmostFirst)
|
|
213
|
+
.start_kind(StartKind::Anchored)
|
|
214
|
+
.dfa_size_limit(Some(1 << 16))
|
|
215
|
+
.determinize_size_limit(Some(1 << 18)),
|
|
216
|
+
)
|
|
217
|
+
.thompson(thompson::Config::new().nfa_size_limit(Some(1 << 20)))
|
|
218
|
+
.build(&source)
|
|
219
|
+
{
|
|
220
|
+
return Self::Dfa(Box::new(dfa));
|
|
221
|
+
}
|
|
222
|
+
Regex::builder()
|
|
223
|
+
.configure(
|
|
224
|
+
Regex::config()
|
|
225
|
+
.nfa_size_limit(Some(1 << 20))
|
|
226
|
+
.hybrid_cache_capacity(1 << 20),
|
|
227
|
+
)
|
|
228
|
+
.build(&source)
|
|
229
|
+
.map(|regex| {
|
|
230
|
+
let index = *cache_count;
|
|
231
|
+
*cache_count += 1;
|
|
232
|
+
Self::Compiled { regex, index }
|
|
233
|
+
})
|
|
234
|
+
.unwrap_or(Self::Unsupported)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
pub(super) fn eval(&self, caches: &Caches, value: Value) -> Verdict {
|
|
238
|
+
if matches!(self, Self::Unsupported) {
|
|
239
|
+
return Unsupported;
|
|
240
|
+
}
|
|
241
|
+
let mut data = std::ptr::null();
|
|
242
|
+
let mut len = 0;
|
|
243
|
+
let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
|
|
244
|
+
if encoding == ENCODING_OTHER {
|
|
245
|
+
return Unsupported;
|
|
246
|
+
}
|
|
247
|
+
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
248
|
+
self.eval_bytes(caches, bytes, encoding)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
pub(super) fn eval_bytes(&self, caches: &Caches, bytes: &[u8], encoding: i32) -> Verdict {
|
|
252
|
+
if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
|
|
253
|
+
return Unsupported;
|
|
254
|
+
}
|
|
255
|
+
if encoding != ENCODING_SEVEN_BIT
|
|
256
|
+
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
257
|
+
|| std::str::from_utf8(bytes).is_err())
|
|
258
|
+
{
|
|
259
|
+
return Unsupported;
|
|
260
|
+
}
|
|
261
|
+
let matched = match self {
|
|
262
|
+
Self::Dfa(dfa) => match dfa
|
|
263
|
+
.try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
|
|
264
|
+
{
|
|
265
|
+
Ok(matched) => matched.is_some(),
|
|
266
|
+
Err(_) => return Unsupported,
|
|
267
|
+
},
|
|
268
|
+
Self::Compiled { regex, index } => caches.is_match(*index, regex, bytes),
|
|
269
|
+
Self::Unsupported => return Unsupported,
|
|
270
|
+
};
|
|
271
|
+
if matched { Valid } else { Invalid }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
fn scan_items(
|
|
276
|
+
required: i32,
|
|
277
|
+
values: &[Value],
|
|
278
|
+
mut invalid: impl FnMut(usize, Value) -> Verdict,
|
|
279
|
+
mut matches: impl FnMut(&[u8]) -> Verdict,
|
|
280
|
+
) -> Verdict {
|
|
281
|
+
for (index, &value) in values.iter().enumerate() {
|
|
282
|
+
let mut data = std::ptr::null();
|
|
283
|
+
let mut len = 0;
|
|
284
|
+
let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
|
|
285
|
+
if encoding == -TYPE_UNSUPPORTED {
|
|
286
|
+
return Unsupported;
|
|
287
|
+
}
|
|
288
|
+
if encoding < 0 {
|
|
289
|
+
if required != 0 {
|
|
290
|
+
let result = invalid(index, value);
|
|
291
|
+
if result != Valid {
|
|
292
|
+
return result;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if encoding == ENCODING_OTHER {
|
|
298
|
+
return Unsupported;
|
|
299
|
+
}
|
|
300
|
+
let bytes = unsafe { std::slice::from_raw_parts(data, len) };
|
|
301
|
+
if encoding != ENCODING_SEVEN_BIT
|
|
302
|
+
&& ((encoding == ENCODING_ASCII && !bytes.is_ascii())
|
|
303
|
+
|| std::str::from_utf8(bytes).is_err())
|
|
304
|
+
{
|
|
305
|
+
return Unsupported;
|
|
306
|
+
}
|
|
307
|
+
let result = matches(bytes);
|
|
308
|
+
match result {
|
|
309
|
+
Unsupported => return Unsupported,
|
|
310
|
+
Invalid => {
|
|
311
|
+
let result = invalid(index, value);
|
|
312
|
+
if result != Valid {
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
Valid => {}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
Valid
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// This is a VM compatibility translation, not a regex parser. Unsupported
|
|
323
|
+
// escapes, class set operations and special groups retain Ruby's behavior.
|
|
324
|
+
fn translate(source: &str) -> Option<String> {
|
|
325
|
+
let mut output = String::with_capacity(source.len());
|
|
326
|
+
let mut characters = source.chars().peekable();
|
|
327
|
+
let mut in_class = false;
|
|
328
|
+
while let Some(character) = characters.next() {
|
|
329
|
+
match character {
|
|
330
|
+
'\\' => {
|
|
331
|
+
let escaped = characters.next()?;
|
|
332
|
+
let class = match escaped {
|
|
333
|
+
'd' => Some("0-9"),
|
|
334
|
+
'D' => Some("^0-9"),
|
|
335
|
+
'w' => Some("A-Za-z0-9_"),
|
|
336
|
+
'W' => Some("^A-Za-z0-9_"),
|
|
337
|
+
's' => Some(
|
|
338
|
+
r"\x{0009}-\x{000D}\x{0020}\x{00A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}",
|
|
339
|
+
),
|
|
340
|
+
'S' => Some(
|
|
341
|
+
r"^\x{0009}-\x{000D}\x{0020}\x{00A0}\x{1680}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}\x{FEFF}",
|
|
342
|
+
),
|
|
343
|
+
_ => None,
|
|
344
|
+
};
|
|
345
|
+
if let Some(class) = class {
|
|
346
|
+
if !in_class {
|
|
347
|
+
output.push('[');
|
|
348
|
+
}
|
|
349
|
+
output.push_str(class);
|
|
350
|
+
if !in_class {
|
|
351
|
+
output.push(']');
|
|
352
|
+
}
|
|
353
|
+
} else if ".^$?+*(){}[]|\\-afnrtv".contains(escaped)
|
|
354
|
+
|| (!in_class && matches!(escaped, 'A' | 'z'))
|
|
355
|
+
{
|
|
356
|
+
output.push('\\');
|
|
357
|
+
output.push(escaped);
|
|
358
|
+
} else {
|
|
359
|
+
return None;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
'[' => {
|
|
363
|
+
if in_class {
|
|
364
|
+
return None;
|
|
365
|
+
}
|
|
366
|
+
in_class = true;
|
|
367
|
+
output.push(character);
|
|
368
|
+
}
|
|
369
|
+
']' => {
|
|
370
|
+
in_class = false;
|
|
371
|
+
output.push(character);
|
|
372
|
+
}
|
|
373
|
+
'&' if in_class => return None,
|
|
374
|
+
'-' | '~' if in_class && characters.peek() == Some(&character) => return None,
|
|
375
|
+
'(' if !in_class && characters.peek() == Some(&'?') => {
|
|
376
|
+
characters.next();
|
|
377
|
+
if characters.next() != Some(':') {
|
|
378
|
+
return None;
|
|
379
|
+
}
|
|
380
|
+
output.push_str("(?:");
|
|
381
|
+
}
|
|
382
|
+
'^' if !in_class => output.push_str(r"\A"),
|
|
383
|
+
'$' if !in_class => output.push_str(r"\z"),
|
|
384
|
+
_ => output.push(character),
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
Some(output)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fn compatible(ast: &Ast) -> bool {
|
|
391
|
+
match ast {
|
|
392
|
+
Ast::Repetition(repetition) => {
|
|
393
|
+
// Ruby treats adjacent repetition operators as possessive or
|
|
394
|
+
// rejects them, while regex-syntax can parse nested repetitions.
|
|
395
|
+
if matches!(*repetition.ast, Ast::Repetition(_)) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
let bounded = match repetition.op.kind {
|
|
399
|
+
RepetitionKind::Range(
|
|
400
|
+
RepetitionRange::Exactly(n) | RepetitionRange::AtLeast(n),
|
|
401
|
+
) => n <= 4096,
|
|
402
|
+
RepetitionKind::Range(RepetitionRange::Bounded(_, max)) => max <= 4096,
|
|
403
|
+
_ => true,
|
|
404
|
+
};
|
|
405
|
+
bounded && compatible(&repetition.ast)
|
|
406
|
+
}
|
|
407
|
+
Ast::Group(group) => compatible(&group.ast),
|
|
408
|
+
Ast::Alternation(alternation) => alternation.asts.iter().all(compatible),
|
|
409
|
+
Ast::Concat(concat) => concat.asts.iter().all(compatible),
|
|
410
|
+
Ast::Flags(_) | Ast::ClassUnicode(_) | Ast::ClassPerl(_) => false,
|
|
411
|
+
_ => true,
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[cfg(test)]
|
|
416
|
+
mod tests {
|
|
417
|
+
use super::*;
|
|
418
|
+
|
|
419
|
+
#[test]
|
|
420
|
+
fn reports_memory_when_a_search_cache_is_borrowed() {
|
|
421
|
+
let caches = Caches::default();
|
|
422
|
+
let regex = Regex::new("value[0-9]+").unwrap();
|
|
423
|
+
assert!(caches.is_match(0, ®ex, b"value1"));
|
|
424
|
+
assert!(unsafe { sn_caches_size(&caches) } > std::mem::size_of::<Caches>());
|
|
425
|
+
let _search = caches.0.borrow_mut();
|
|
426
|
+
assert_eq!(
|
|
427
|
+
unsafe { sn_caches_size(&caches) },
|
|
428
|
+
std::mem::size_of::<Caches>()
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
}
|