corvus_json_schema 0.1.3 → 0.1.4

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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +2 -12
  3. data/README.md +10 -1
  4. data/THIRD-PARTY-NOTICES.txt +95 -0
  5. data/VERSIONHISTORY.md +24 -0
  6. data/ext/corvus_json_schema/Cargo.toml +2 -2
  7. data/ext/corvus_json_schema/clippy.toml +3 -4
  8. data/ext/corvus_json_schema/src/lib.rs +42 -19
  9. data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +9 -4
  10. data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +87 -4
  11. data/ext/corvus_json_schema/vendor/corvus-json-schema/THIRD-PARTY-NOTICES.txt +93 -0
  12. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +88 -96
  13. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +2 -2
  14. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/charset.rs +271 -0
  15. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/compile.rs +941 -0
  16. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/dfa.rs +366 -0
  17. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/fold.rs +138 -0
  18. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/mod.rs +329 -0
  19. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/parse.rs +1128 -0
  20. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/re2.rs +129 -0
  21. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/tables_gen.rs +6685 -0
  22. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/tests.rs +1672 -0
  23. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/ucd.rs +309 -0
  24. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/units.rs +165 -0
  25. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/vm.rs +981 -0
  26. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +13 -9
  27. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +12 -11
  28. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +105 -73
  29. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +95 -1
  30. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +165 -30
  31. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +19 -21
  32. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +1 -1
  33. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +199 -4
  34. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +758 -79
  35. data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +4 -4
  36. data/lib/corvus_json_schema/version.rb +1 -1
  37. data/lib/corvus_json_schema.rb +13 -3
  38. metadata +16 -2
@@ -0,0 +1,329 @@
1
+ //! Matches ECMA-262 regular expressions against UTF-8 text, with the semantics JSON Schema requires of the
2
+ //! `pattern` and `patternProperties` keywords and of the `regex` format.
3
+ //!
4
+ //! This is a port of the `ecmaregex` package of the Go module (`src-go/corvus-json-schema/internal/ecmaregex`),
5
+ //! which the Object Pascal and OCaml ports of the evaluator also carry. The files and the names follow the Go
6
+ //! source, so that a fix made in one port can be carried to the others: `parse.rs` is `parse.go`, `compile.rs` is
7
+ //! `compile.go`, `vm.rs` is `vm.go`, `dfa.rs` is `dfa.go`, `charset.rs`, `fold.rs` and `units.rs` are the files of
8
+ //! the same names, and `ucd.rs` with `tables_gen.rs` is the `ucd` package.
9
+ //!
10
+ //! # Unicode mode
11
+ //!
12
+ //! A pattern is read first with the grammar of the ECMA-262 u flag, which is what JSON Schema specifies. A pattern
13
+ //! that is not valid under that grammar is then read with the Annex B grammar of a pattern with no flag, which
14
+ //! accepts what many schemas in the wild contain (identity escapes such as `\&`, a lone brace, legacy octal
15
+ //! escapes). Only a pattern that neither grammar accepts is an error. [`valid`], which backs the `regex` format,
16
+ //! accepts only the u flag grammar.
17
+ //!
18
+ //! A pattern the u flag grammar accepts is matched by Unicode code point over the UTF-8 text. The dot, a class and a
19
+ //! quantifier each see a character beyond the Basic Multilingual Plane as one character, and a surrogate pair
20
+ //! written as two `\u` escapes names one code point.
21
+ //!
22
+ //! A pattern that only the Annex B grammar accepts is matched by UTF-16 code unit, as ECMA-262 matches a pattern
23
+ //! with no u flag. A character beyond the Basic Multilingual Plane is then two characters, a high surrogate and a
24
+ //! low one, to the dot, a class, a quantifier, a group and a backreference. A match can start or end between the
25
+ //! two. Such a character in the pattern is two atoms, and a class range between two of them is an error. Case is
26
+ //! folded by the Canonicalize of a pattern with no u flag. The text stays UTF-8 and no UTF-16 copy of it is made
27
+ //! (see `units.rs`). Only a text with a character beyond the Basic Multilingual Plane is read differently by the
28
+ //! two.
29
+ //!
30
+ //! The text is a `&str`, so it is UTF-8 and cannot hold half of a surrogate pair. A pattern that names one, such
31
+ //! as `\ud800`, matches it only as half of a character and only when the pattern has no u flag.
32
+ //!
33
+ //! JSON Schema gives a pattern no flags, so matching is case sensitive, `^` and `$` match only at the ends of the
34
+ //! text, and the dot excludes the four line terminators. A pattern can change that for part of itself with a
35
+ //! modifier group of ECMAScript 2025, such as `(?i:...)`, `(?m:...)` or `(?s:...)`.
36
+ //!
37
+ //! # The matcher
38
+ //!
39
+ //! Every pattern is compiled for a backtracking matcher. A pattern with no lookaround, no backreference, no
40
+ //! multiline anchor and no case-insensitive word boundary also gets a deterministic automaton over ASCII text when
41
+ //! it is small enough and has no word boundary (see `dfa.rs`), which decides an ASCII text with one table read per
42
+ //! byte. (The Go engine gives such a pattern to the standard library's regexp package as well. In this crate the
43
+ //! `regex` crate has that part, and `pattern.rs` decides which patterns it gets before the engine is asked.)
44
+ //!
45
+ //! The backtracking matcher keeps its choices on an explicit stack that the thread reuses, so a long text cannot
46
+ //! overflow the stack of the thread and a match does not allocate once the stack has grown. Four things keep it
47
+ //! from the time a backtracking matcher can take. A repetition whose body cannot consume a character is run once,
48
+ //! or not at all (see `simplify`), as V8 runs it. A counted loop gives up at a position from which the rest of the
49
+ //! pattern, with the iterations the loop still owes, needs more text than is left (see `set_needs`). A way the next
50
+ //! character of the text rules out is not taken (see `set_quick`), as in V8. And where a pattern has no
51
+ //! backreference the matcher remembers the places from which every way has failed (see `memo` in `vm.rs`), so a
52
+ //! repetition inside a repetition takes time proportional to the pattern times the text. What is left, a group read
53
+ //! back inside a repetition above all, can still take time that doubles with each character, and is ended by the
54
+ //! limit of the match.
55
+ //!
56
+ //! # Limits
57
+ //!
58
+ //! [`Regexp::match_limit`] gives a match a limit of time, and with it a cap on the memory of its backtracking.
59
+ //! [`compile`] refuses a pattern over a limit of size ([`MAX_PATTERN_LENGTH`], [`MAX_NESTING`], [`MAX_SIZE`]) with
60
+ //! a [`TooLargeError`], so that reading and compiling a pattern take bounded time and memory whatever the pattern
61
+ //! holds.
62
+ //!
63
+ //! # Coverage
64
+ //!
65
+ //! The grammar is that of ECMAScript 2025. It includes lookbehind, named groups, group names shared between
66
+ //! separate alternatives, modifier groups, and Unicode property escapes for every property, value and alias
67
+ //! ECMA-262 lists, with Unicode 17 data. The properties and the case folding come from the tables of `ucd.rs` and
68
+ //! never from the standard library or from a dependency, so a pattern matches the same texts whatever built the
69
+ //! program. The v flag (set notation and properties of strings) does not apply, because a JSON Schema pattern has
70
+ //! no flags.
71
+
72
+ mod charset;
73
+ mod compile;
74
+ mod dfa;
75
+ mod fold;
76
+ mod parse;
77
+ mod re2;
78
+ // Generated: it has names of scripts and properties that only the tests read.
79
+ #[allow(dead_code)]
80
+ #[rustfmt::skip]
81
+ mod tables_gen;
82
+ #[cfg(test)]
83
+ mod tests;
84
+ mod ucd;
85
+ mod units;
86
+ mod vm;
87
+
88
+ use std::fmt;
89
+ use std::sync::OnceLock;
90
+ use std::time::Duration;
91
+
92
+ pub(crate) use charset::{CharSet, property_set};
93
+ use parse::Limit;
94
+ pub(crate) use parse::{MAX_NESTING, TooLargeError};
95
+ pub(crate) use re2::MAX_RE2_COST;
96
+ pub(crate) use units::has_supplementary;
97
+ pub(crate) use vm::MatchError;
98
+
99
+ /// A code point, or a UTF-16 code unit of a pattern read without the u flag. It is signed, as the `rune` of the Go
100
+ /// source is, and -1 stands for "none".
101
+ pub(crate) type Rune = i32;
102
+
103
+ /// Why a pattern does not compile.
104
+ #[derive(Clone, Debug, PartialEq, Eq)]
105
+ pub(crate) enum Error {
106
+ /// The pattern is over a limit of size. It was not read to its end, so nothing is known of whether it is valid.
107
+ TooLarge(TooLargeError),
108
+ /// The pattern is not valid in either grammar. The message is that of the u flag grammar.
109
+ Invalid(String),
110
+ }
111
+
112
+ impl fmt::Display for Error {
113
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114
+ match self {
115
+ Error::TooLarge(e) => e.fmt(f),
116
+ Error::Invalid(message) => f.write_str(message),
117
+ }
118
+ }
119
+ }
120
+
121
+ impl std::error::Error for Error {}
122
+
123
+ /// What reading a pattern tells of it, without compiling it.
124
+ #[derive(Clone, Copy, Debug)]
125
+ pub(crate) struct Facts {
126
+ /// The pattern is valid with the u flag. When it is not, it was read with the Annex B grammar.
127
+ pub(crate) unicode: bool,
128
+ /// The pattern has a construct only the backtracking matcher can run.
129
+ pub(crate) needs_backtracking: bool,
130
+ /// The size the parser counted for the pattern (see [`MAX_SIZE`]).
131
+ pub(crate) size: usize,
132
+ /// The number of instructions estimated for the program a linear matcher compiles for the pattern, which stops
133
+ /// counting above [`MAX_RE2_COST`].
134
+ pub(crate) re2_cost: i64,
135
+ /// The number of instructions estimated for the program a linear matcher runs for the pattern (see
136
+ /// `re2::linear_cost`), which is `re2_cost` or less.
137
+ pub(crate) linear_cost: i64,
138
+ /// The memory estimated for the program the `regex` crate compiles for the pattern, in bytes (see
139
+ /// `re2::linear_size`).
140
+ pub(crate) linear_size: i64,
141
+ }
142
+
143
+ fn invalid(pattern: &str, msg: &str, pos: usize) -> Error {
144
+ Error::Invalid(format!("ecmaregex: {msg} at offset {pos} in {pattern:?}"))
145
+ }
146
+
147
+ /// Reads a pattern with the u flag grammar and, when that refuses it, with the Annex B grammar.
148
+ fn read(pattern: &str) -> Result<(parse::Node, Facts), Error> {
149
+ let (root, ps, unicode) = match parse::parse(pattern, true) {
150
+ Ok((root, ps)) => (root, ps, true),
151
+ Err(parse::ParseError::TooLarge(large)) => return Err(Error::TooLarge(large)),
152
+ Err(parse::ParseError::Syntax(msg, pos)) => match parse::parse(pattern, false) {
153
+ Ok((root, ps)) => (root, ps, false),
154
+ Err(parse::ParseError::TooLarge(large)) => return Err(Error::TooLarge(large)),
155
+ Err(parse::ParseError::Syntax(..)) => return Err(invalid(pattern, msg, pos)),
156
+ },
157
+ };
158
+ let facts = Facts {
159
+ unicode,
160
+ needs_backtracking: ps.needs_backtracking,
161
+ size: ps.size,
162
+ re2_cost: re2::re2_cost(&root),
163
+ linear_cost: re2::linear_cost(&root),
164
+ linear_size: re2::linear_size(&root),
165
+ };
166
+ Ok((root, facts))
167
+ }
168
+
169
+ /// The number of groups a pattern may open and still be read on the stack of the thread that asks.
170
+ const SHALLOW: usize = 32;
171
+
172
+ /// The stack of the thread that reads a pattern with more groups than that. The parser, the compiler and the
173
+ /// dropping of the syntax tree go one level deeper for each level of groups, and [`MAX_NESTING`] allows a thousand
174
+ /// levels, which is more than the stack of a thread can be relied on to hold: a thread of a language runtime that
175
+ /// embeds the crate may have a few hundred kilobytes. Only the pages the work touches are used.
176
+ const DEEP_STACK: usize = 64 << 20;
177
+
178
+ /// Runs the reading of a pattern, with everything that walks its syntax tree, where the depth of the tree cannot
179
+ /// overflow the stack. A pattern that opens few groups is shallow, and is read where it is. Any other is read on a
180
+ /// thread of its own with a stack for the deepest nesting the limits allow. (Go grows the stack of a goroutine as
181
+ /// it is needed, so the Go engine has no such step.) `None` when that thread could not be started.
182
+ fn on_a_deep_stack<T: Send>(pattern: &str, work: impl FnOnce() -> T + Send) -> Option<T> {
183
+ if shallow(pattern) {
184
+ return Some(work());
185
+ }
186
+ deep(work)
187
+ }
188
+
189
+ /// Runs work that goes a level deeper for each level of groups of a pattern on a thread with a stack for the
190
+ /// deepest nesting the limits allow. `None` when the thread could not be started.
191
+ pub(crate) fn deep<T: Send>(work: impl FnOnce() -> T + Send) -> Option<T> {
192
+ std::thread::scope(|scope| {
193
+ let thread = std::thread::Builder::new().stack_size(DEEP_STACK).spawn_scoped(scope, work).ok()?;
194
+ match thread.join() {
195
+ Ok(result) => Some(result),
196
+ Err(panic) => std::panic::resume_unwind(panic),
197
+ }
198
+ })
199
+ }
200
+
201
+ /// Whether a pattern opens few enough groups to be read on the stack of the thread that asks.
202
+ pub(crate) fn shallow(pattern: &str) -> bool {
203
+ pattern.bytes().filter(|&b| b == b'(').count() <= SHALLOW
204
+ }
205
+
206
+ /// The error of a pattern that nests deeply when no thread could be started to read it: the pattern is refused as
207
+ /// one over the limit of nesting, which is the limit the thread is there to serve.
208
+ const NO_DEEP_STACK: Error = Error::TooLarge(TooLargeError { limit: Limit::Nesting, value: MAX_NESTING });
209
+
210
+ /// A compiled ECMA-262 pattern. It is immutable once compiled, and safe to share between threads.
211
+ pub(crate) struct Regexp {
212
+ pattern: String,
213
+ prog: compile::Program,
214
+ /// The pattern was read with the u flag grammar.
215
+ unicode: bool,
216
+ /// The pattern needs no backtracking and is small enough to be tried for an automaton.
217
+ regular: bool,
218
+ /// The automaton of the pattern over ASCII text (see `dfa.rs`), built on the first match. `None` inside when
219
+ /// the pattern has none. The syntax tree is let go of when the pattern is compiled, so the pattern is read
220
+ /// again to build it, which cannot fail.
221
+ dfa: OnceLock<Option<dfa::Dfa>>,
222
+ /// The size the parser counted for the pattern.
223
+ #[cfg(test)]
224
+ size: usize,
225
+ }
226
+
227
+ impl fmt::Debug for Regexp {
228
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229
+ write!(f, "Regexp({:?})", self.pattern)
230
+ }
231
+ }
232
+
233
+ /// Compiles an ECMA-262 pattern with the semantics JSON Schema requires. It returns an error for a pattern that is
234
+ /// not valid, and a [`TooLargeError`] for a pattern over a limit of size (see [`MAX_SIZE`]), whether or not it is
235
+ /// valid.
236
+ pub(crate) fn compile(pattern: &str) -> Result<Regexp, Error> {
237
+ on_a_deep_stack(pattern, || {
238
+ let (root, facts) = read(pattern)?;
239
+ let regular = !facts.needs_backtracking && facts.re2_cost <= MAX_RE2_COST;
240
+ let dfa = OnceLock::new();
241
+ if regular && !shallow(pattern) {
242
+ // The automaton is built from the syntax tree, so that of a pattern that nests deeply is built here
243
+ // and not on the stack of the thread that first matches.
244
+ let _ = dfa.set(dfa::build_dfa(&root));
245
+ }
246
+ Ok(Regexp {
247
+ pattern: pattern.to_string(),
248
+ prog: compile::compile_program(root, facts.unicode),
249
+ unicode: facts.unicode,
250
+ regular,
251
+ dfa,
252
+ #[cfg(test)]
253
+ size: facts.size,
254
+ })
255
+ })
256
+ .unwrap_or(Err(NO_DEEP_STACK))
257
+ }
258
+
259
+ impl Regexp {
260
+ /// Reports whether the pattern matches anywhere in the text (an unanchored search), with no limit on the time
261
+ /// or the memory the match takes. It allocates nothing in the steady state.
262
+ #[cfg(test)]
263
+ pub(crate) fn is_match(&self, text: &str) -> bool {
264
+ self.match_limit(text, None).unwrap_or(false)
265
+ }
266
+
267
+ /// [`Regexp::is_match`] with a limit on the time the match may take, when `limit` is `Some` and not zero. A
268
+ /// match that reaches the limit is abandoned and the error is [`MatchError::Time`]. So is a match whose
269
+ /// backtracking stack reaches [`MAX_STACK`] entries, and the error is [`MatchError::Memory`]. The result is
270
+ /// then not an answer. The clock is read every 4,096 steps of the backtracking matcher, so a match can go past
271
+ /// the limit by the time those take, which is some tens of microseconds. The automaton takes time proportional
272
+ /// to the text and is not stopped.
273
+ #[inline]
274
+ pub(crate) fn match_limit(&self, text: &str, limit: Option<Duration>) -> Result<bool, MatchError> {
275
+ if self.regular
276
+ && let Some(dfa) = self.dfa.get_or_init(|| self.build_dfa())
277
+ && let Some(matched) = dfa.is_match(text.as_bytes())
278
+ {
279
+ return Ok(matched);
280
+ }
281
+ self.prog.is_match(text, limit)
282
+ }
283
+
284
+ /// The backtracking matcher alone, which the tests use to compare it with the automaton.
285
+ #[cfg(test)]
286
+ pub(crate) fn match_backtrack(&self, text: &str, limit: Option<Duration>) -> Result<bool, MatchError> {
287
+ self.prog.is_match(text, limit)
288
+ }
289
+
290
+ /// Builds the automaton of the pattern, which opens few groups (see `compile`).
291
+ #[cold]
292
+ fn build_dfa(&self) -> Option<dfa::Dfa> {
293
+ let (root, _) = parse::parse(&self.pattern, self.unicode).ok()?;
294
+ dfa::build_dfa(&root)
295
+ }
296
+
297
+ /// Whether the pattern is valid with the ECMA-262 u flag. When it is not, the pattern was read with the Annex B
298
+ /// grammar.
299
+ #[cfg(test)]
300
+ pub(crate) fn unicode(&self) -> bool {
301
+ self.unicode
302
+ }
303
+
304
+ /// The size the parser counted for the pattern, which the tree, the program and the time to make both are
305
+ /// proportional to (see [`MAX_SIZE`]).
306
+ #[cfg(test)]
307
+ pub(crate) fn size(&self) -> usize {
308
+ self.size
309
+ }
310
+
311
+ /// The source of the pattern.
312
+ #[cfg(test)]
313
+ pub(crate) fn as_str(&self) -> &str {
314
+ &self.pattern
315
+ }
316
+ }
317
+
318
+ /// Reports whether the pattern is a valid ECMA-262 pattern (for `format: regex`). It applies the grammar of the u
319
+ /// flag. A pattern over a limit of size is not read to its end and is reported as not valid.
320
+ pub(crate) fn valid(pattern: &str) -> bool {
321
+ on_a_deep_stack(pattern, || parse::parse(pattern, true).is_ok()).unwrap_or(false)
322
+ }
323
+
324
+ /// Reads the pattern without compiling it. It returns what the parser learnt of it, or the error [`compile`]
325
+ /// would: a [`TooLargeError`] for a pattern over a limit of size, and any other error for a pattern that is not
326
+ /// valid.
327
+ pub(crate) fn check(pattern: &str) -> Result<Facts, Error> {
328
+ on_a_deep_stack(pattern, || read(pattern).map(|(_, facts)| facts)).unwrap_or(Err(NO_DEEP_STACK))
329
+ }