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
@@ -18,10 +18,52 @@
18
18
  //! assert!(validator.is_valid(&json!({ "id": 3 })));
19
19
  //! assert!(!validator.is_valid(&json!({ "id": 0 })));
20
20
  //! ```
21
+ //!
22
+ //! # Regular expressions and their limits
23
+ //!
24
+ //! `pattern`, `patternProperties` and the `regex` format follow ECMA-262. Simple patterns are matched by matchers of
25
+ //! the crate's own and by the `regex` crate, and everything else (lookarounds, backreferences, modifier groups) by
26
+ //! the crate's own ECMA-262 engine, as each pattern is written.
27
+ //!
28
+ //! A schema and an instance may both come from outside, so a pattern is bounded in what it costs. One match against
29
+ //! one string may take [`CompileOptions::pattern_match_limit`], one second unless the options say otherwise. A match
30
+ //! that reaches it is abandoned and the validation has no answer: [`Validator::try_validate`] and the other `try_`
31
+ //! functions return [`SchemaEvaluationError::PatternMatchLimit`]. A pattern longer than 1,048,576 bytes, nested more
32
+ //! than 1,000 groups deep or of more than about 16,000 terms does not compile, and
33
+ //! [`SchemaCompilationError::kind`] is then [`SchemaCompilationErrorKind::PatternTooLarge`], which is not the kind
34
+ //! of a pattern that is not valid.
35
+ //!
36
+ //! ```
37
+ //! use std::time::Duration;
38
+ //!
39
+ //! use corvus_json_schema::{CompileOptions, SchemaCompilationErrorKind, SchemaEvaluationError, compile, compile_with};
40
+ //! use serde_json::json;
41
+ //!
42
+ //! // A group read back inside a repetition: on this string the match would run for longer than anyone waits.
43
+ //! let options = CompileOptions { pattern_match_limit: Some(Duration::from_millis(50)), ..CompileOptions::default() };
44
+ //! let validator = compile_with(&json!({ "pattern": r"^(?:(a+)\1*)+$" }), &options).unwrap();
45
+ //! let instance = json!("a".repeat(60) + "b");
46
+ //! match validator.try_validate(&instance) {
47
+ //! Err(SchemaEvaluationError::PatternMatchLimit(e)) => {
48
+ //! assert_eq!(e.pattern(), r"^(?:(a+)\1*)+$");
49
+ //! assert_eq!(e.limit(), Duration::from_millis(50));
50
+ //! }
51
+ //! other => panic!("{other:?}"),
52
+ //! }
53
+ //! // is_valid has no way to say so, and answers false.
54
+ //! assert!(!validator.is_valid(&instance));
55
+ //!
56
+ //! // A pattern over a limit of size is not a pattern that is not valid.
57
+ //! let error = compile(&json!({ "pattern": "a".repeat(20000) })).unwrap_err();
58
+ //! assert!(matches!(error.kind(), SchemaCompilationErrorKind::PatternTooLarge(large) if large.limit() == "size"));
59
+ //! let error = compile(&json!({ "pattern": "a**" })).unwrap_err();
60
+ //! assert_eq!(error.kind(), &SchemaCompilationErrorKind::InvalidPattern);
61
+ //! ```
21
62
 
22
63
  mod compiler;
23
64
  mod dialect;
24
65
  mod document;
66
+ mod ecmaregex;
25
67
  mod eval;
26
68
  mod formats;
27
69
  mod instance;
@@ -43,7 +85,9 @@ pub use dialect::Dialect;
43
85
  pub use document::{JsonDocument, JsonDocumentArray, JsonDocumentObject, JsonDocumentValue, JsonParseError};
44
86
  pub use instance::{ArrayView, Instance, Kind, ObjectView, View};
45
87
  pub use options::{
46
- CompileOptions, DocumentResolver, FormatValidator, SchemaCompilationError, SchemaEvaluationDepthError,
88
+ CompileOptions, DEFAULT_PATTERN_MATCH_LIMIT, DocumentResolver, FormatValidator, PatternMatchLimitError,
89
+ PatternTooLargeError, SchemaCompilationError, SchemaCompilationErrorKind, SchemaEvaluationDepthError,
90
+ SchemaEvaluationError,
47
91
  };
48
92
  pub use results::{
49
93
  Annotation, JsonSchemaResultsCollector, ResultsLevel, SchemaResult, collect_annotations, encode_pointer_segment,
@@ -53,12 +97,16 @@ pub use results::{
53
97
  /// The version of this crate.
54
98
  pub const VERSION: &str = env!("CARGO_PKG_VERSION");
55
99
 
56
- /// Why JSON text could not be validated.
100
+ /// Why JSON text could not be validated, as [`Validator::validate_json`] and [`Validator::evaluate_json`] report it.
101
+ /// [`Validator::try_validate_json`] and [`Validator::try_evaluate_json`] report a [`SchemaEvaluationError`], which
102
+ /// also tells a match of a regular expression that reached its limit from the maximum depth.
57
103
  #[derive(Clone, Debug, PartialEq, Eq)]
58
104
  pub enum JsonValidationError {
59
105
  /// The text is not valid JSON.
60
106
  InvalidJson(JsonParseError),
61
- /// Evaluation recursed in place beyond the maximum depth.
107
+ /// The validation was abandoned at a limit: it recursed in place beyond the maximum depth, or a match of a
108
+ /// regular expression reached its limit. The name is from when the depth was the one limit, and the message
109
+ /// names both (see [`without_the_limit`] for why the two are one here).
62
110
  DepthExceeded(SchemaEvaluationDepthError),
63
111
  }
64
112
 
@@ -80,6 +128,24 @@ impl std::error::Error for JsonValidationError {
80
128
  }
81
129
  }
82
130
 
131
+ /// What the functions whose error type was fixed before the limit of a match existed report for an evaluation that
132
+ /// was abandoned. Their type, [`SchemaEvaluationDepthError`], has one value, so a match that reached its limit is
133
+ /// reported as that value too: an error, and so not an answer, though not one that says which limit it was. The
134
+ /// `try_` functions say which. This is the one place that decides it.
135
+ fn without_the_limit(e: SchemaEvaluationError) -> SchemaEvaluationDepthError {
136
+ match e {
137
+ SchemaEvaluationError::DepthExceeded(e) => e,
138
+ _ => SchemaEvaluationDepthError,
139
+ }
140
+ }
141
+
142
+ fn json_error(e: SchemaEvaluationError) -> JsonValidationError {
143
+ match e {
144
+ SchemaEvaluationError::InvalidJson(e) => JsonValidationError::InvalidJson(e),
145
+ other => JsonValidationError::DepthExceeded(without_the_limit(other)),
146
+ }
147
+ }
148
+
83
149
  /// A compiled schema. Cheap to clone and safe to share between threads.
84
150
  #[derive(Clone)]
85
151
  pub struct Validator {
@@ -101,18 +167,21 @@ pub fn compile(schema: &Value) -> Result<Validator, SchemaCompilationError> {
101
167
  /// Compiles a schema.
102
168
  pub fn compile_with(schema: &Value, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
103
169
  let compiled = compiler::SchemaCompiler::compile(schema.clone(), options)?;
104
- Ok(Validator::from_compiled(compiled, options))
170
+ Validator::from_compiled(compiled, options)
105
171
  }
106
172
 
107
173
  /// Compiles the schema document at `uri`, fetched through `options.resolve_document` (or one of the standard
108
174
  /// metaschemas).
109
175
  pub fn compile_from_uri(uri: &str, options: &CompileOptions) -> Result<Validator, SchemaCompilationError> {
110
176
  let compiled = compiler::SchemaCompiler::compile_from_uri(uri, options)?;
111
- Ok(Validator::from_compiled(compiled, options))
177
+ Validator::from_compiled(compiled, options)
112
178
  }
113
179
 
114
180
  impl Validator {
115
- fn from_compiled(c: compiler::CompiledSchema, options: &CompileOptions) -> Validator {
181
+ fn from_compiled(
182
+ c: compiler::CompiledSchema,
183
+ options: &CompileOptions,
184
+ ) -> Result<Validator, SchemaCompilationError> {
116
185
  let program = eval::Program::new(
117
186
  c.nodes,
118
187
  c.root,
@@ -122,68 +191,134 @@ impl Validator {
122
191
  c.documents,
123
192
  c.annotation_sources,
124
193
  options.assert_format.is_some(),
194
+ pattern::PatternLimit::new(options.pattern_match_limit),
125
195
  );
126
- Validator { program: Arc::new(program) }
196
+ // Compiling matches each pattern of patternProperties against the property names the schema declares.
197
+ let failure = program.compile_budget.lock().unwrap_or_else(|e| e.into_inner()).take_failure();
198
+ if let Some(e) = failure {
199
+ return Err(SchemaCompilationError::of_kind(
200
+ e.to_string(),
201
+ SchemaCompilationErrorKind::PatternMatchLimit(e),
202
+ ));
203
+ }
204
+ Ok(Validator { program: Arc::new(program) })
127
205
  }
128
206
 
129
- /// Whether the instance is valid. A schema that recursed in place beyond the maximum depth is reported as
130
- /// invalid; use [`Validator::validate`] to tell the two apart.
207
+ /// Whether the instance is valid. It has no way to report an error, so it fails closed: it reports false both
208
+ /// for a schema that recursed in place beyond the maximum depth and for a match of a regular expression that
209
+ /// reached its limit (see [`CompileOptions::pattern_match_limit`]). A limit is never reported as valid. In
210
+ /// neither case is the instance known to be not valid. Use [`Validator::try_validate`] to tell them from an
211
+ /// instance that is not valid.
131
212
  #[inline]
132
213
  pub fn is_valid(&self, instance: &Value) -> bool {
133
214
  eval::Evaluator::new(&self.program, None).validate(instance)
134
215
  }
135
216
 
136
- /// Whether the instance is valid, or an error if evaluation recursed in place beyond the maximum depth.
137
- pub fn validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationDepthError> {
217
+ /// Whether the instance is valid, or why the evaluation gave no answer: it recursed in place beyond the maximum
218
+ /// depth, or a match of a regular expression reached its limit.
219
+ pub fn try_validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationError> {
220
+ self.try_validate_instance(instance)
221
+ }
222
+
223
+ /// [`Validator::try_validate`] for any [`Instance`]: a value read in place, such as a host language's own
224
+ /// objects, with no conversion to `serde_json::Value`.
225
+ pub fn try_validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationError> {
138
226
  let mut e = eval::Evaluator::new(&self.program, None);
139
227
  let ok = e.validate(instance);
140
- if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
228
+ e.outcome(ok)
229
+ }
230
+
231
+ /// Whether the JSON text is valid, or why there is no answer. The text is parsed into the thread's reused
232
+ /// buffers and evaluated in place: in the steady state nothing is allocated. Use [`JsonDocument`] to validate
233
+ /// the same text more than once.
234
+ pub fn try_validate_json(&self, json: &str) -> Result<bool, SchemaEvaluationError> {
235
+ JsonDocument::with_document(json, |d| self.try_validate_instance(d.root()))
236
+ .map_err(SchemaEvaluationError::InvalidJson)?
237
+ }
238
+
239
+ /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid, or why the
240
+ /// evaluation gave no answer. With an error the collector's results are not complete.
241
+ pub fn try_evaluate(
242
+ &self,
243
+ instance: &Value,
244
+ collector: &mut JsonSchemaResultsCollector,
245
+ ) -> Result<bool, SchemaEvaluationError> {
246
+ self.try_evaluate_instance(instance, collector)
247
+ }
248
+
249
+ /// [`Validator::try_evaluate`] for any [`Instance`].
250
+ pub fn try_evaluate_instance<'a, I: Instance<'a>>(
251
+ &self,
252
+ instance: I,
253
+ collector: &mut JsonSchemaResultsCollector,
254
+ ) -> Result<bool, SchemaEvaluationError> {
255
+ let mut e = eval::Evaluator::new(&self.program, Some(collector));
256
+ let ok = e.evaluate(instance);
257
+ e.outcome(ok)
141
258
  }
142
259
 
143
- /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid.
260
+ /// [`Validator::try_evaluate`] for JSON text, parsed as [`Validator::try_validate_json`] parses it.
261
+ pub fn try_evaluate_json(
262
+ &self,
263
+ json: &str,
264
+ collector: &mut JsonSchemaResultsCollector,
265
+ ) -> Result<bool, SchemaEvaluationError> {
266
+ JsonDocument::with_document(json, |d| self.try_evaluate_instance(d.root(), collector))
267
+ .map_err(SchemaEvaluationError::InvalidJson)?
268
+ }
269
+
270
+ /// Whether the instance is valid, or an error if the validation was abandoned at a limit. The error type has
271
+ /// one value, whose message names both limits, so it does not say whether the evaluation recursed in place
272
+ /// beyond the maximum depth or a match of a regular expression reached its limit: [`Validator::try_validate`]
273
+ /// does.
274
+ pub fn validate(&self, instance: &Value) -> Result<bool, SchemaEvaluationDepthError> {
275
+ self.try_validate(instance).map_err(without_the_limit)
276
+ }
277
+
278
+ /// Evaluates the instance exhaustively, reporting to the collector. Returns whether it is valid. The error is
279
+ /// that of [`Validator::validate`], for a validation abandoned at either limit: [`Validator::try_evaluate`]
280
+ /// says which.
144
281
  pub fn evaluate(
145
282
  &self,
146
283
  instance: &Value,
147
284
  collector: &mut JsonSchemaResultsCollector,
148
285
  ) -> Result<bool, SchemaEvaluationDepthError> {
149
- self.evaluate_instance(instance, collector)
286
+ self.try_evaluate(instance, collector).map_err(without_the_limit)
150
287
  }
151
288
 
152
289
  /// [`Validator::validate`] for any [`Instance`]: a value read in place, such as a host language's own objects,
153
- /// with no conversion to `serde_json::Value`.
290
+ /// with no conversion to `serde_json::Value`. The error is that of [`Validator::validate`], for a validation
291
+ /// abandoned at either limit: [`Validator::try_validate_instance`] says which.
154
292
  pub fn validate_instance<'a, I: Instance<'a>>(&self, instance: I) -> Result<bool, SchemaEvaluationDepthError> {
155
- let mut e = eval::Evaluator::new(&self.program, None);
156
- let ok = e.validate(instance);
157
- if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
293
+ self.try_validate_instance(instance).map_err(without_the_limit)
158
294
  }
159
295
 
160
296
  /// Whether the JSON text is valid. The text is parsed into the thread's reused buffers and evaluated in place: in
161
- /// the steady state nothing is allocated. Use [`JsonDocument`] to validate the same text more than once.
297
+ /// the steady state nothing is allocated. Use [`JsonDocument`] to validate the same text more than once. An
298
+ /// evaluation that was abandoned is reported as [`JsonValidationError::DepthExceeded`], whichever limit it
299
+ /// reached: [`Validator::try_validate_json`] says which.
162
300
  pub fn validate_json(&self, json: &str) -> Result<bool, JsonValidationError> {
163
- JsonDocument::with_document(json, |d| self.validate_instance(d.root()))
164
- .map_err(JsonValidationError::InvalidJson)?
165
- .map_err(JsonValidationError::DepthExceeded)
301
+ self.try_validate_json(json).map_err(json_error)
166
302
  }
167
303
 
168
- /// [`Validator::evaluate`] for JSON text, parsed as [`Validator::validate_json`] parses it.
304
+ /// [`Validator::evaluate`] for JSON text, parsed as [`Validator::validate_json`] parses it. An evaluation that
305
+ /// was abandoned is [`JsonValidationError::DepthExceeded`], whichever limit it reached:
306
+ /// [`Validator::try_evaluate_json`] says which.
169
307
  pub fn evaluate_json(
170
308
  &self,
171
309
  json: &str,
172
310
  collector: &mut JsonSchemaResultsCollector,
173
311
  ) -> Result<bool, JsonValidationError> {
174
- JsonDocument::with_document(json, |d| self.evaluate_instance(d.root(), collector))
175
- .map_err(JsonValidationError::InvalidJson)?
176
- .map_err(JsonValidationError::DepthExceeded)
312
+ self.try_evaluate_json(json, collector).map_err(json_error)
177
313
  }
178
314
 
179
- /// [`Validator::evaluate`] for any [`Instance`].
315
+ /// [`Validator::evaluate`] for any [`Instance`]. The error is that of [`Validator::validate`], for a validation
316
+ /// abandoned at either limit: [`Validator::try_evaluate_instance`] says which.
180
317
  pub fn evaluate_instance<'a, I: Instance<'a>>(
181
318
  &self,
182
319
  instance: I,
183
320
  collector: &mut JsonSchemaResultsCollector,
184
321
  ) -> Result<bool, SchemaEvaluationDepthError> {
185
- let mut e = eval::Evaluator::new(&self.program, Some(collector));
186
- let ok = e.evaluate(instance);
187
- if e.depth_exceeded { Err(SchemaEvaluationDepthError) } else { Ok(ok) }
322
+ self.try_evaluate_instance(instance, collector).map_err(without_the_limit)
188
323
  }
189
324
  }
@@ -231,11 +231,11 @@ impl<'o> SchemaLoader<'o> {
231
231
  if self.documents_by_uri.contains_key(absolute_uri) {
232
232
  return Ok(true);
233
233
  }
234
- if let Some(resolver) = &self.options.resolve_document {
235
- if let Some(doc) = resolver(absolute_uri) {
236
- self.add_document(absolute_uri.to_string(), doc)?;
237
- return Ok(true);
238
- }
234
+ if let Some(resolver) = &self.options.resolve_document
235
+ && let Some(doc) = resolver(absolute_uri)
236
+ {
237
+ self.add_document(absolute_uri.to_string(), doc)?;
238
+ return Ok(true);
239
239
  }
240
240
  if let Some(text) = metaschema(absolute_uri) {
241
241
  let doc: Value = serde_json::from_str(text).expect("embedded metaschemas are valid JSON");
@@ -304,10 +304,8 @@ impl<'o> SchemaLoader<'o> {
304
304
  let r = &self.resources[resource as usize];
305
305
  (r.dialect, r.vocabularies)
306
306
  };
307
- if !is_resource_root {
308
- if let Some(Value::String(s)) = map.get("$schema") {
309
- (dialect, vocabularies) = self.get_dialect_info(s)?;
310
- }
307
+ if !is_resource_root && let Some(Value::String(s)) = map.get("$schema") {
308
+ (dialect, vocabularies) = self.get_dialect_info(s)?;
311
309
  }
312
310
 
313
311
  let legacy_ref_overrides_siblings = dialect.is_legacy() && matches!(map.get("$ref"), Some(Value::String(_)));
@@ -335,19 +333,19 @@ impl<'o> SchemaLoader<'o> {
335
333
  }
336
334
  }
337
335
  }
338
- if dialect >= Dialect::Draft201909 {
339
- if let Some(Value::String(a)) = map.get("$anchor") {
340
- self.add_anchor(resource, a, &pointer);
341
- }
336
+ if dialect >= Dialect::Draft201909
337
+ && let Some(Value::String(a)) = map.get("$anchor")
338
+ {
339
+ self.add_anchor(resource, a, &pointer);
342
340
  }
343
- if dialect >= Dialect::Draft202012 {
344
- if let Some(Value::String(name)) = map.get("$dynamicAnchor") {
345
- self.resources[resource as usize]
346
- .dynamic_anchors
347
- .entry(name.clone())
348
- .or_insert_with(|| pointer.clone());
349
- self.add_anchor(resource, name, &pointer);
350
- }
341
+ if dialect >= Dialect::Draft202012
342
+ && let Some(Value::String(name)) = map.get("$dynamicAnchor")
343
+ {
344
+ self.resources[resource as usize]
345
+ .dynamic_anchors
346
+ .entry(name.clone())
347
+ .or_insert_with(|| pointer.clone());
348
+ self.add_anchor(resource, name, &pointer);
351
349
  }
352
350
  if dialect == Dialect::Draft201909
353
351
  && is_resource_root
@@ -140,7 +140,7 @@ fn divides_scaled(m: u128, shift: u32, d: u128) -> bool {
140
140
  let mut rest = d / gcd(m, d);
141
141
  for f in [2u128, 5] {
142
142
  let mut count = 0u32;
143
- while rest % f == 0 {
143
+ while rest.is_multiple_of(f) {
144
144
  rest /= f;
145
145
  count += 1;
146
146
  }
@@ -3,6 +3,7 @@
3
3
  use std::collections::HashMap;
4
4
  use std::fmt;
5
5
  use std::sync::Arc;
6
+ use std::time::Duration;
6
7
 
7
8
  use serde_json::Value;
8
9
 
@@ -36,8 +37,23 @@ pub struct CompileOptions {
36
37
  pub entry_point: Option<String>,
37
38
  /// Maximum depth of in-place recursion on a cycle before evaluation is abandoned. Defaults to 128.
38
39
  pub max_depth: u32,
40
+ /// The time one match of a `pattern` or `patternProperties` regular expression against one string may take.
41
+ /// Defaults to [`DEFAULT_PATTERN_MATCH_LIMIT`], one second. A match that takes longer is abandoned, and the
42
+ /// validation it belongs to reports a [`PatternMatchLimitError`]. `None`, or a limit of zero, turns the limit
43
+ /// off, for a caller who trusts both the schema and the instances. Without the limit a match also has no limit
44
+ /// on its memory.
45
+ ///
46
+ /// The limit is on each match, not on the validation. Only the backtracking matcher can take long, and it
47
+ /// reads the clock every 4,096 steps, so a match is abandoned within tens of microseconds of the limit. The
48
+ /// matchers that take time proportional to the string are not stopped: a string is given to one only when it
49
+ /// can answer within the limit.
50
+ pub pattern_match_limit: Option<Duration>,
39
51
  }
40
52
 
53
+ /// The time one match of a `pattern` or `patternProperties` regular expression against one string may take, unless
54
+ /// [`CompileOptions::pattern_match_limit`] says otherwise.
55
+ pub const DEFAULT_PATTERN_MATCH_LIMIT: Duration = Duration::from_secs(1);
56
+
41
57
  impl Default for CompileOptions {
42
58
  fn default() -> Self {
43
59
  CompileOptions {
@@ -50,6 +66,7 @@ impl Default for CompileOptions {
50
66
  base_uri: None,
51
67
  entry_point: None,
52
68
  max_depth: 128,
69
+ pattern_match_limit: Some(DEFAULT_PATTERN_MATCH_LIMIT),
53
70
  }
54
71
  }
55
72
  }
@@ -66,25 +83,53 @@ impl fmt::Debug for CompileOptions {
66
83
  .field("base_uri", &self.base_uri)
67
84
  .field("entry_point", &self.entry_point)
68
85
  .field("max_depth", &self.max_depth)
86
+ .field("pattern_match_limit", &self.pattern_match_limit)
69
87
  .finish()
70
88
  }
71
89
  }
72
90
 
73
- /// A schema could not be compiled (an unresolvable reference, an invalid pattern).
91
+ /// A schema could not be compiled (an unresolvable reference, an invalid pattern, a pattern over a limit of size).
92
+ /// [`SchemaCompilationError::kind`] tells the errors of a regular expression apart.
74
93
  #[derive(Clone, Debug, PartialEq, Eq)]
75
94
  pub struct SchemaCompilationError {
76
95
  message: String,
96
+ kind: SchemaCompilationErrorKind,
97
+ }
98
+
99
+ /// What kind of error a [`SchemaCompilationError`] is.
100
+ #[derive(Clone, Debug, PartialEq, Eq)]
101
+ #[non_exhaustive]
102
+ pub enum SchemaCompilationErrorKind {
103
+ /// Any error that is not about a regular expression: an unresolvable reference, for one.
104
+ Other,
105
+ /// A `pattern`, or a key of `patternProperties`, is not a valid ECMA-262 regular expression.
106
+ InvalidPattern,
107
+ /// A regular expression is over a limit of size. It is not the error of one that is not valid: the pattern was
108
+ /// not read to its end, so nothing is known of whether it is valid.
109
+ PatternTooLarge(PatternTooLargeError),
110
+ /// Matching a regular expression against a property name the schema declares, which is done once when the
111
+ /// schema is compiled, reached the limit of a match (see [`CompileOptions::pattern_match_limit`]).
112
+ PatternMatchLimit(PatternMatchLimitError),
77
113
  }
78
114
 
79
115
  impl SchemaCompilationError {
80
116
  pub(crate) fn new(message: impl Into<String>) -> Self {
81
- SchemaCompilationError { message: message.into() }
117
+ SchemaCompilationError { message: message.into(), kind: SchemaCompilationErrorKind::Other }
118
+ }
119
+
120
+ pub(crate) fn of_kind(message: impl Into<String>, kind: SchemaCompilationErrorKind) -> Self {
121
+ SchemaCompilationError { message: message.into(), kind }
82
122
  }
83
123
 
84
124
  /// The reason compilation failed.
85
125
  pub fn message(&self) -> &str {
86
126
  &self.message
87
127
  }
128
+
129
+ /// The kind of error, which tells a regular expression that is too large from one that is not valid.
130
+ pub fn kind(&self) -> &SchemaCompilationErrorKind {
131
+ &self.kind
132
+ }
88
133
  }
89
134
 
90
135
  impl fmt::Display for SchemaCompilationError {
@@ -95,14 +140,164 @@ impl fmt::Display for SchemaCompilationError {
95
140
 
96
141
  impl std::error::Error for SchemaCompilationError {}
97
142
 
98
- /// Evaluation recursed in place beyond `max_depth` (a schema that loops without consuming the instance).
143
+ /// A regular expression of a schema is over a limit of size, so the schema was not compiled.
144
+ #[derive(Clone, Debug, PartialEq, Eq)]
145
+ pub struct PatternTooLargeError {
146
+ pattern: String,
147
+ keyword: &'static str,
148
+ limit: &'static str,
149
+ value: usize,
150
+ reason: String,
151
+ }
152
+
153
+ impl PatternTooLargeError {
154
+ pub(crate) fn new(pattern: &str, keyword: &'static str, limit: &'static str, value: usize, reason: String) -> Self {
155
+ PatternTooLargeError { pattern: pattern.to_string(), keyword, limit, value, reason }
156
+ }
157
+
158
+ /// The regular expression.
159
+ pub fn pattern(&self) -> &str {
160
+ &self.pattern
161
+ }
162
+
163
+ /// The keyword that holds it, `pattern` or `patternProperties`.
164
+ pub fn keyword(&self) -> &str {
165
+ self.keyword
166
+ }
167
+
168
+ /// The name of the limit: `length` (the bytes of the pattern, 1,048,576 at most), `nesting` (the depth of its
169
+ /// groups, 1,000 at most) or `size` (the bytes counted for it as it is read, 1,048,576 at most).
170
+ pub fn limit(&self) -> &str {
171
+ self.limit
172
+ }
173
+
174
+ /// The value of that limit.
175
+ pub fn value(&self) -> usize {
176
+ self.value
177
+ }
178
+ }
179
+
180
+ impl fmt::Display for PatternTooLargeError {
181
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182
+ write!(
183
+ f,
184
+ "The regular expression {} in {} is too large to read as an ECMA-262 regular expression: {}.",
185
+ crate::pattern::shown_pattern(&self.pattern),
186
+ self.keyword,
187
+ self.reason
188
+ )
189
+ }
190
+ }
191
+
192
+ impl std::error::Error for PatternTooLargeError {}
193
+
194
+ /// A validation was abandoned because one match of a `pattern` or `patternProperties` regular expression reached
195
+ /// its limit. The instance is then neither valid nor not valid.
196
+ #[derive(Clone, Debug, PartialEq, Eq)]
197
+ pub struct PatternMatchLimitError {
198
+ pattern: String,
199
+ limit: Duration,
200
+ memory: bool,
201
+ }
202
+
203
+ impl PatternMatchLimitError {
204
+ pub(crate) fn new(pattern: &str, limit: Duration, memory: bool) -> Self {
205
+ PatternMatchLimitError { pattern: pattern.to_string(), limit, memory }
206
+ }
207
+
208
+ /// The regular expression.
209
+ pub fn pattern(&self) -> &str {
210
+ &self.pattern
211
+ }
212
+
213
+ /// The time the match was allowed (see [`CompileOptions::pattern_match_limit`]).
214
+ pub fn limit(&self) -> Duration {
215
+ self.limit
216
+ }
217
+
218
+ /// Whether the match was abandoned for the memory of its backtracking, not for its time: it held 1,048,576
219
+ /// choices, which is 24 MB.
220
+ pub fn is_memory(&self) -> bool {
221
+ self.memory
222
+ }
223
+ }
224
+
225
+ impl fmt::Display for PatternMatchLimitError {
226
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227
+ let pattern = crate::pattern::shown_pattern(&self.pattern);
228
+ if self.memory {
229
+ write!(
230
+ f,
231
+ "A match of the regular expression {pattern} was abandoned: it needed more than the limit of \
232
+ 1,048,576 backtracking entries."
233
+ )
234
+ } else {
235
+ write!(
236
+ f,
237
+ "A match of the regular expression {pattern} was abandoned: it took longer than the limit of {:?}.",
238
+ self.limit
239
+ )
240
+ }
241
+ }
242
+ }
243
+
244
+ impl std::error::Error for PatternMatchLimitError {}
245
+
246
+ /// The validation was abandoned at a limit, and gave no answer. It is the error of the functions whose error type
247
+ /// was there before the limit of a match was (`validate`, `validate_instance`, `evaluate`, `evaluate_instance`, and
248
+ /// inside [`crate::JsonValidationError::DepthExceeded`] `validate_json` and `evaluate_json`).
249
+ ///
250
+ /// The name is from when there was one limit: an evaluation that recursed in place beyond `max_depth` (a schema
251
+ /// that loops without consuming the instance). A match of a regular expression that reached its limit (see
252
+ /// [`CompileOptions::pattern_match_limit`]) is reported as this error too. The type has one value, so it does not
253
+ /// say which limit it was, and its message names both. The `try_` functions (`Validator::try_validate` and the
254
+ /// rest) return a [`SchemaEvaluationError`], which says which.
99
255
  #[derive(Clone, Copy, Debug, PartialEq, Eq)]
100
256
  pub struct SchemaEvaluationDepthError;
101
257
 
102
258
  impl fmt::Display for SchemaEvaluationDepthError {
103
259
  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104
- f.write_str("The schema recursed in place beyond the maximum depth.")
260
+ f.write_str(
261
+ "The validation was abandoned at a limit: the schema recursed in place beyond the maximum depth, or a \
262
+ match of a regular expression reached its limit. The try_ functions say which.",
263
+ )
105
264
  }
106
265
  }
107
266
 
108
267
  impl std::error::Error for SchemaEvaluationDepthError {}
268
+
269
+ /// Why an evaluation gave no answer. The instance is then neither valid nor not valid.
270
+ #[derive(Clone, Debug, PartialEq, Eq)]
271
+ #[non_exhaustive]
272
+ pub enum SchemaEvaluationError {
273
+ /// The text is not valid JSON (the functions that take JSON text).
274
+ InvalidJson(crate::JsonParseError),
275
+ /// Evaluation recursed in place beyond the maximum depth.
276
+ DepthExceeded(SchemaEvaluationDepthError),
277
+ /// A match of a regular expression reached its limit.
278
+ PatternMatchLimit(PatternMatchLimitError),
279
+ }
280
+
281
+ impl fmt::Display for SchemaEvaluationError {
282
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283
+ match self {
284
+ SchemaEvaluationError::InvalidJson(e) => write!(f, "invalid JSON: {e}"),
285
+ // Not the message of the value inside, which is the one error of the functions that cannot say which
286
+ // limit it was, and so names both.
287
+ SchemaEvaluationError::DepthExceeded(_) => {
288
+ f.write_str("The schema recursed in place beyond the maximum depth.")
289
+ }
290
+ SchemaEvaluationError::PatternMatchLimit(e) => e.fmt(f),
291
+ }
292
+ }
293
+ }
294
+
295
+ impl std::error::Error for SchemaEvaluationError {
296
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
297
+ match self {
298
+ SchemaEvaluationError::InvalidJson(e) => Some(e),
299
+ SchemaEvaluationError::DepthExceeded(e) => Some(e),
300
+ SchemaEvaluationError::PatternMatchLimit(e) => Some(e),
301
+ }
302
+ }
303
+ }