schemurai 2.2.2 → 2.3.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,288 @@
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, StartKind, dense},
8
+ meta::Regex,
9
+ nfa::thompson,
10
+ };
11
+ use regex_syntax::ast::{Ast, RepetitionKind, RepetitionRange};
12
+
13
+ #[derive(Debug)]
14
+ pub(super) enum Pattern {
15
+ Unsupported,
16
+ Dfa(Box<dense::DFA<Vec<u32>>>),
17
+ Compiled(Regex),
18
+ }
19
+
20
+ impl Pattern {
21
+ // Select the crate matcher once for a homogeneous array. The borrowed
22
+ // Ruby slices are used synchronously; classification and encoding are
23
+ // 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)
26
+ }
27
+
28
+ pub(super) fn visit_invalid_items(
29
+ &self,
30
+ required: i32,
31
+ values: &[Value],
32
+ invalid: impl FnMut(usize, Value) -> Verdict,
33
+ ) -> Verdict {
34
+ match self {
35
+ Self::Dfa(dfa) => scan_items(required, values, invalid, |bytes| {
36
+ match dfa.try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
37
+ {
38
+ Ok(matched) => verdict(matched.is_some()),
39
+ Err(_) => Unsupported,
40
+ }
41
+ }),
42
+ Self::Compiled(regex) => scan_items(required, values, invalid, |bytes| {
43
+ verdict(regex.is_match(bytes))
44
+ }),
45
+ Self::Unsupported => scan_items(required, values, invalid, |_| Unsupported),
46
+ }
47
+ }
48
+
49
+ pub(super) fn memory_usage(&self) -> usize {
50
+ match self {
51
+ Self::Dfa(dfa) => std::mem::size_of_val(&**dfa) + dfa.memory_usage(),
52
+ Self::Compiled(regex) => regex.memory_usage(),
53
+ Self::Unsupported => 0,
54
+ }
55
+ }
56
+
57
+ pub(super) fn compile(value: Value) -> Self {
58
+ if unsafe { sn_kind(value) } != TYPE_STRING {
59
+ return Self::Unsupported;
60
+ }
61
+ let mut data = std::ptr::null();
62
+ let mut len = 0;
63
+ let encoding = unsafe { sn_string_view(value, &mut data, &mut len) };
64
+ if encoding == ENCODING_OTHER {
65
+ return Self::Unsupported;
66
+ }
67
+ let bytes = unsafe { std::slice::from_raw_parts(data, len) };
68
+ if encoding == ENCODING_ASCII && !bytes.is_ascii() {
69
+ return Self::Unsupported;
70
+ }
71
+ let Some(source) = std::str::from_utf8(bytes).ok().and_then(translate) else {
72
+ return Self::Unsupported;
73
+ };
74
+ let Ok(ast) = regex_syntax::ast::parse::Parser::new().parse(&source) else {
75
+ return Self::Unsupported;
76
+ };
77
+ if !compatible(&ast) {
78
+ return Self::Unsupported;
79
+ }
80
+ // Use a single immutable DFA for fully start-anchored validity. The
81
+ // meta engine retains its literal prefilters for unanchored searches.
82
+ // Bound both the DFA table and auxiliary determinization memory.
83
+ if let Ok(hir) = regex_syntax::hir::translate::Translator::new().translate(&source, &ast)
84
+ && hir
85
+ .properties()
86
+ .look_set_prefix()
87
+ .contains(regex_syntax::hir::Look::Start)
88
+ && let Ok(dfa) = dense::Builder::new()
89
+ .configure(
90
+ dense::Config::new()
91
+ .match_kind(MatchKind::LeftmostFirst)
92
+ .start_kind(StartKind::Anchored)
93
+ .dfa_size_limit(Some(1 << 16))
94
+ .determinize_size_limit(Some(1 << 18)),
95
+ )
96
+ .thompson(thompson::Config::new().nfa_size_limit(Some(1 << 20)))
97
+ .build(&source)
98
+ {
99
+ return Self::Dfa(Box::new(dfa));
100
+ }
101
+ Regex::builder()
102
+ .configure(
103
+ Regex::config()
104
+ .nfa_size_limit(Some(1 << 20))
105
+ .hybrid_cache_capacity(1 << 20),
106
+ )
107
+ .build(&source)
108
+ .map(Self::Compiled)
109
+ .unwrap_or(Self::Unsupported)
110
+ }
111
+
112
+ pub(super) fn eval(&self, value: Value) -> Verdict {
113
+ if matches!(self, Self::Unsupported) {
114
+ return Unsupported;
115
+ }
116
+ let mut data = std::ptr::null();
117
+ let mut len = 0;
118
+ let encoding = unsafe { sn_pattern_view(value, &mut data, &mut len) };
119
+ if encoding == ENCODING_OTHER {
120
+ return Unsupported;
121
+ }
122
+ let bytes = unsafe { std::slice::from_raw_parts(data, len) };
123
+ self.eval_bytes(bytes, encoding)
124
+ }
125
+
126
+ pub(super) fn eval_bytes(&self, bytes: &[u8], encoding: i32) -> Verdict {
127
+ if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
128
+ return Unsupported;
129
+ }
130
+ if encoding != ENCODING_SEVEN_BIT
131
+ && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
132
+ || std::str::from_utf8(bytes).is_err())
133
+ {
134
+ return Unsupported;
135
+ }
136
+ let matched = match self {
137
+ Self::Dfa(dfa) => match dfa
138
+ .try_search_fwd(&Input::new(bytes).anchored(Anchored::Yes).earliest(true))
139
+ {
140
+ Ok(matched) => matched.is_some(),
141
+ Err(_) => return Unsupported,
142
+ },
143
+ Self::Compiled(regex) => regex.is_match(bytes),
144
+ Self::Unsupported => return Unsupported,
145
+ };
146
+ if matched { Valid } else { Invalid }
147
+ }
148
+ }
149
+
150
+ fn scan_items(
151
+ required: i32,
152
+ values: &[Value],
153
+ mut invalid: impl FnMut(usize, Value) -> Verdict,
154
+ mut matches: impl FnMut(&[u8]) -> Verdict,
155
+ ) -> Verdict {
156
+ for (index, &value) in values.iter().enumerate() {
157
+ let mut data = std::ptr::null();
158
+ let mut len = 0;
159
+ let encoding = unsafe { sn_pattern_item_view(value, &mut data, &mut len) };
160
+ if encoding == -TYPE_UNSUPPORTED {
161
+ return Unsupported;
162
+ }
163
+ if encoding < 0 {
164
+ if required != 0 {
165
+ let result = invalid(index, value);
166
+ if result != Valid {
167
+ return result;
168
+ }
169
+ }
170
+ continue;
171
+ }
172
+ if encoding == ENCODING_OTHER {
173
+ return Unsupported;
174
+ }
175
+ let bytes = unsafe { std::slice::from_raw_parts(data, len) };
176
+ if encoding != ENCODING_SEVEN_BIT
177
+ && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
178
+ || std::str::from_utf8(bytes).is_err())
179
+ {
180
+ return Unsupported;
181
+ }
182
+ let result = matches(bytes);
183
+ match result {
184
+ Unsupported => return Unsupported,
185
+ Invalid => {
186
+ let result = invalid(index, value);
187
+ if result != Valid {
188
+ return result;
189
+ }
190
+ }
191
+ Valid => {}
192
+ }
193
+ }
194
+ Valid
195
+ }
196
+
197
+ // This is a VM compatibility translation, not a regex parser. Unsupported
198
+ // escapes, class set operations and special groups retain Ruby's behavior.
199
+ fn translate(source: &str) -> Option<String> {
200
+ let mut output = String::with_capacity(source.len());
201
+ let mut characters = source.chars().peekable();
202
+ let mut in_class = false;
203
+ while let Some(character) = characters.next() {
204
+ match character {
205
+ '\\' => {
206
+ let escaped = characters.next()?;
207
+ let class = match escaped {
208
+ 'd' => Some("0-9"),
209
+ 'D' => Some("^0-9"),
210
+ 'w' => Some("A-Za-z0-9_"),
211
+ 'W' => Some("^A-Za-z0-9_"),
212
+ 's' => Some(
213
+ 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}",
214
+ ),
215
+ 'S' => Some(
216
+ 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}",
217
+ ),
218
+ _ => None,
219
+ };
220
+ if let Some(class) = class {
221
+ if !in_class {
222
+ output.push('[');
223
+ }
224
+ output.push_str(class);
225
+ if !in_class {
226
+ output.push(']');
227
+ }
228
+ } else if ".^$?+*(){}[]|\\-afnrtv".contains(escaped)
229
+ || (!in_class && matches!(escaped, 'A' | 'z'))
230
+ {
231
+ output.push('\\');
232
+ output.push(escaped);
233
+ } else {
234
+ return None;
235
+ }
236
+ }
237
+ '[' => {
238
+ if in_class {
239
+ return None;
240
+ }
241
+ in_class = true;
242
+ output.push(character);
243
+ }
244
+ ']' => {
245
+ in_class = false;
246
+ output.push(character);
247
+ }
248
+ '&' if in_class => return None,
249
+ '-' | '~' if in_class && characters.peek() == Some(&character) => return None,
250
+ '(' if !in_class && characters.peek() == Some(&'?') => {
251
+ characters.next();
252
+ if characters.next() != Some(':') {
253
+ return None;
254
+ }
255
+ output.push_str("(?:");
256
+ }
257
+ '^' if !in_class => output.push_str(r"\A"),
258
+ '$' if !in_class => output.push_str(r"\z"),
259
+ _ => output.push(character),
260
+ }
261
+ }
262
+ Some(output)
263
+ }
264
+
265
+ fn compatible(ast: &Ast) -> bool {
266
+ match ast {
267
+ Ast::Repetition(repetition) => {
268
+ // Ruby treats adjacent repetition operators as possessive or
269
+ // rejects them, while regex-syntax can parse nested repetitions.
270
+ if matches!(*repetition.ast, Ast::Repetition(_)) {
271
+ return false;
272
+ }
273
+ let bounded = match repetition.op.kind {
274
+ RepetitionKind::Range(
275
+ RepetitionRange::Exactly(n) | RepetitionRange::AtLeast(n),
276
+ ) => n <= 4096,
277
+ RepetitionKind::Range(RepetitionRange::Bounded(_, max)) => max <= 4096,
278
+ _ => true,
279
+ };
280
+ bounded && compatible(&repetition.ast)
281
+ }
282
+ Ast::Group(group) => compatible(&group.ast),
283
+ Ast::Alternation(alternation) => alternation.asts.iter().all(compatible),
284
+ Ast::Concat(concat) => concat.asts.iter().all(compatible),
285
+ Ast::Flags(_) | Ast::ClassUnicode(_) | Ast::ClassPerl(_) => false,
286
+ _ => true,
287
+ }
288
+ }
@@ -0,0 +1,210 @@
1
+ //! Resource/anchor lookup is compiled once. Evaluation only follows native
2
+ //! tables and stack frames, including when speculative branches collect annotations.
3
+ use super::*;
4
+ use tracking::JsonKey;
5
+ #[derive(Clone, Copy, Default)]
6
+ pub(super) struct Metadata {
7
+ pub resource: Value,
8
+ pub enters_scope: bool,
9
+ pub recursive_anchor: bool,
10
+ pub dynamic_anchor: Option<Value>,
11
+ pub primary_binding: Option<usize>,
12
+ }
13
+ #[derive(Clone, Copy, Debug)]
14
+ enum ReferenceKind {
15
+ Static,
16
+ Recursive,
17
+ Dynamic(usize),
18
+ }
19
+ #[derive(Clone, Copy, Debug)]
20
+ pub(super) struct Reference {
21
+ target: usize,
22
+ kind: ReferenceKind,
23
+ }
24
+ #[derive(Clone, Copy, Default)]
25
+ pub(super) struct Context<'a> {
26
+ pub active: Option<&'a RefFrame<'a>>,
27
+ pub scope: Option<&'a ScopeFrame<'a>>,
28
+ }
29
+ pub(super) struct ScopeFrame<'a> {
30
+ pub resource: Value,
31
+ pub parent: Option<&'a ScopeFrame<'a>>,
32
+ pub primary_binding: Option<usize>,
33
+ }
34
+ impl Engine {
35
+ pub(super) fn compile_scope(
36
+ &mut self,
37
+ program: Value,
38
+ index: usize,
39
+ flags: i64,
40
+ ) -> Result<(), Value> {
41
+ let node = self.field(program, b"node\0")?;
42
+ let resource = self.field(node, b"resource\0")?;
43
+ let recursive = self.field(program, b"recursive_anchor?\0")?;
44
+ let dynamic = self.field(program, b"dynamic_anchor\0")?;
45
+ self.metadata[index] = Metadata {
46
+ resource,
47
+ enters_scope: flags & i64::from(TRACKS_DYNAMIC_SCOPE) != 0,
48
+ recursive_anchor: unsafe { sn_true(recursive) } != 0,
49
+ primary_binding: None,
50
+ dynamic_anchor: (unsafe { sn_same(dynamic, sn_nil()) } == 0).then_some(dynamic),
51
+ };
52
+ if !self.resources.contains_key(&resource) {
53
+ // Reserve the resource before compiling its root: roots can reach
54
+ // the current child through a recursive or dynamic reference.
55
+ self.resources.insert(resource, usize::MAX);
56
+ let mut root = 0;
57
+ if unsafe { sn_resource_program(self.compiler, resource, &mut root) } != 0 {
58
+ return Err(root);
59
+ }
60
+ self.retain(root);
61
+ let root = self.compile(root)?;
62
+ self.resources.insert(resource, root);
63
+ }
64
+ Ok(())
65
+ }
66
+ pub(super) fn compile_reference(
67
+ &mut self,
68
+ operand: Value,
69
+ opcode: i32,
70
+ target: usize,
71
+ ) -> Result<Op, Value> {
72
+ let fragment = self.field(operand, b"fragment\0")?;
73
+ let kind = if opcode == OP_RECURSIVE_REF
74
+ && unsafe { sn_same(fragment, sn_nil()) } == 0
75
+ && unsafe { sn_string_bytes(fragment) } == 0
76
+ && self.metadata[target].recursive_anchor
77
+ {
78
+ ReferenceKind::Recursive
79
+ } else if opcode == OP_DYNAMIC_REF {
80
+ let anchor = self.metadata[target].dynamic_anchor;
81
+ match anchor {
82
+ Some(anchor)
83
+ if unsafe { sn_same(fragment, sn_nil()) } == 0
84
+ && unsafe { sn_string_bytes(fragment) } != 0
85
+ && unsafe { *sn_string_data(fragment) } != b'/'
86
+ && unsafe { sn_string_equal(fragment, anchor) } != 0 =>
87
+ {
88
+ let name = JsonKey::new(fragment);
89
+ let index = self
90
+ .anchor_names
91
+ .iter()
92
+ .position(|&candidate| candidate == name)
93
+ .unwrap_or_else(|| {
94
+ let index = self.anchor_names.len();
95
+ self.anchor_names.push(name);
96
+ index
97
+ });
98
+ ReferenceKind::Dynamic(index)
99
+ }
100
+ _ => ReferenceKind::Static,
101
+ }
102
+ } else {
103
+ ReferenceKind::Static
104
+ };
105
+ Ok(Op::Ref(Reference { target, kind }))
106
+ }
107
+ pub(super) fn bind_dynamic_anchors(&mut self) -> Result<(), Value> {
108
+ loop {
109
+ let pending: Vec<_> = self
110
+ .resources
111
+ .keys()
112
+ .flat_map(|&resource| {
113
+ (0..self.anchor_names.len()).map(move |name| (resource, name))
114
+ })
115
+ .filter(|key| !self.dynamic_bindings.contains_key(key))
116
+ .collect();
117
+ if pending.is_empty() {
118
+ self.anchor_tables = vec![vec![]; self.anchor_names.len()];
119
+ for (&(resource, name), &target) in &self.dynamic_bindings {
120
+ if let Some(target) = target {
121
+ self.anchor_tables[name].push((resource, target));
122
+ }
123
+ }
124
+ if self.anchor_tables.len() == 1 {
125
+ for metadata in &mut self.metadata {
126
+ metadata.primary_binding =
127
+ self.anchor_tables[0]
128
+ .iter()
129
+ .find_map(|&(resource, target)| {
130
+ (resource == metadata.resource).then_some(target)
131
+ });
132
+ }
133
+ }
134
+ return Ok(());
135
+ }
136
+ for (resource, name) in pending {
137
+ let mut target = 0;
138
+ if unsafe {
139
+ sn_anchor_program(
140
+ self.compiler,
141
+ resource,
142
+ self.anchor_names[name].0,
143
+ &mut target,
144
+ )
145
+ } != 0
146
+ {
147
+ return Err(target);
148
+ }
149
+ self.retain(target);
150
+ let target = if unsafe { sn_same(target, sn_nil()) } != 0 {
151
+ None
152
+ } else {
153
+ Some(self.compile(target)?)
154
+ };
155
+ self.dynamic_bindings.insert((resource, name), target);
156
+ }
157
+ }
158
+ }
159
+ pub(super) fn enter_scope<'a>(
160
+ &self,
161
+ node: usize,
162
+ active: Context<'a>,
163
+ frame: &'a ScopeFrame<'a>,
164
+ ) -> Context<'a> {
165
+ if self.metadata[node].enters_scope
166
+ && active
167
+ .scope
168
+ .is_none_or(|scope| scope.resource != frame.resource)
169
+ {
170
+ Context {
171
+ scope: Some(frame),
172
+ ..active
173
+ }
174
+ } else {
175
+ active
176
+ }
177
+ }
178
+ pub(super) fn reference_target(&self, reference: Reference, context: Context<'_>) -> usize {
179
+ if matches!(reference.kind, ReferenceKind::Dynamic(0)) && self.anchor_tables.len() == 1 {
180
+ return context
181
+ .scope
182
+ .and_then(|frame| frame.primary_binding)
183
+ .unwrap_or(reference.target);
184
+ }
185
+ let mut target = reference.target;
186
+ let mut current = context.scope;
187
+ // Linked frames walk inner to outer. Keep replacing a match so the
188
+ // outermost matching resource wins, just as the VM's scope.each does.
189
+ while let Some(frame) = current {
190
+ match reference.kind {
191
+ ReferenceKind::Static => break,
192
+ ReferenceKind::Recursive => {
193
+ if let Some(&root) = self.resources.get(&frame.resource)
194
+ && self.metadata[root].recursive_anchor
195
+ {
196
+ target = root;
197
+ }
198
+ }
199
+ ReferenceKind::Dynamic(name) => {
200
+ if let Some(Some(binding)) = self.dynamic_bindings.get(&(frame.resource, name))
201
+ {
202
+ target = *binding;
203
+ }
204
+ }
205
+ }
206
+ current = frame.parent;
207
+ }
208
+ target
209
+ }
210
+ }