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.
- checksums.yaml +4 -4
- data/Cargo.lock +2 -12
- data/README.md +10 -1
- data/THIRD-PARTY-NOTICES.txt +95 -0
- data/VERSIONHISTORY.md +24 -0
- data/ext/corvus_json_schema/Cargo.toml +2 -2
- data/ext/corvus_json_schema/clippy.toml +3 -4
- data/ext/corvus_json_schema/src/lib.rs +42 -19
- data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +9 -4
- data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +87 -4
- data/ext/corvus_json_schema/vendor/corvus-json-schema/THIRD-PARTY-NOTICES.txt +93 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +88 -96
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +2 -2
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/charset.rs +271 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/compile.rs +941 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/dfa.rs +366 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/fold.rs +138 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/mod.rs +329 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/parse.rs +1128 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/re2.rs +129 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/tables_gen.rs +6685 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/tests.rs +1672 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/ucd.rs +309 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/units.rs +165 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/ecmaregex/vm.rs +981 -0
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan/fused.rs +13 -9
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval/plan.rs +12 -11
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/eval.rs +105 -73
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/formats.rs +95 -1
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/lib.rs +165 -30
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/loader.rs +19 -21
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/numbers.rs +1 -1
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/options.rs +199 -4
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +758 -79
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/uri.rs +4 -4
- data/lib/corvus_json_schema/version.rb +1 -1
- data/lib/corvus_json_schema.rb +13 -3
- metadata +16 -2
|
@@ -26,7 +26,7 @@ use crate::eval::{json_equal, matches_type};
|
|
|
26
26
|
use crate::instance::{Instance, ObjectView, View};
|
|
27
27
|
use crate::node::*;
|
|
28
28
|
use crate::numbers::Num;
|
|
29
|
-
use crate::pattern::Pattern;
|
|
29
|
+
use crate::pattern::{Pattern, PatternBudget};
|
|
30
30
|
|
|
31
31
|
const MAX_NAMES: usize = 256;
|
|
32
32
|
const MAX_CONDITIONS: usize = 64;
|
|
@@ -132,11 +132,11 @@ enum TestKind {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
impl ValueTest {
|
|
135
|
-
fn holds<'x, I: Instance<'x>>(&self, v: I) -> bool {
|
|
135
|
+
fn holds<'x, I: Instance<'x>>(&self, v: I, budget: &mut PatternBudget) -> bool {
|
|
136
136
|
match &self.kind {
|
|
137
137
|
TestKind::Allowed(values) => values.iter().any(|a| json_equal(a, v)),
|
|
138
138
|
TestKind::Pattern { pattern, requires_string } => match v.view() {
|
|
139
|
-
View::String(s) => pattern.is_match(s),
|
|
139
|
+
View::String(s) => pattern.is_match(s, budget),
|
|
140
140
|
_ => !requires_string,
|
|
141
141
|
},
|
|
142
142
|
}
|
|
@@ -696,7 +696,7 @@ pub(super) fn try_fuse(
|
|
|
696
696
|
// A known name matching an absent pattern fails its condition whatever its value: no constant is allowed.
|
|
697
697
|
for (e, name) in names.iter().enumerate() {
|
|
698
698
|
for (condition, pattern) in &absent {
|
|
699
|
-
if pattern.is_match(name) {
|
|
699
|
+
if pattern.is_match(name, &mut p.compile_budget.lock().unwrap_or_else(|e| e.into_inner())) {
|
|
700
700
|
tests[e].push(ValueTest { condition: *condition, kind: TestKind::Allowed(Box::new([])) });
|
|
701
701
|
}
|
|
702
702
|
}
|
|
@@ -714,7 +714,7 @@ pub(super) fn try_fuse(
|
|
|
714
714
|
apps.push((c as u16, app_child(schema), Some(p.fast_target[schema as usize])));
|
|
715
715
|
}
|
|
716
716
|
for pp in b.pattern_properties.iter().flatten() {
|
|
717
|
-
if pp.pattern.is_match(name) {
|
|
717
|
+
if pp.pattern.is_match(name, &mut p.compile_budget.lock().unwrap_or_else(|e| e.into_inner())) {
|
|
718
718
|
matched = true;
|
|
719
719
|
apps.push((c as u16, app_child(pp.node), Some(p.fast_target[pp.node as usize])));
|
|
720
720
|
}
|
|
@@ -942,7 +942,7 @@ impl Evaluator<'_, '_> {
|
|
|
942
942
|
fn resolve_unknown<'x, I: Instance<'x>>(&mut self, c: &Contributor, name: &str, v: I) -> Option<bool> {
|
|
943
943
|
let mut matched = false;
|
|
944
944
|
for (pattern, child) in c.patterns.iter() {
|
|
945
|
-
if pattern.is_match(name) {
|
|
945
|
+
if pattern.is_match(name, &mut self.budget) {
|
|
946
946
|
matched = true;
|
|
947
947
|
if !self.apply(*child, v) {
|
|
948
948
|
return None;
|
|
@@ -965,7 +965,11 @@ impl Evaluator<'_, '_> {
|
|
|
965
965
|
Some(merged) => {
|
|
966
966
|
let allowed = merged.allowed(v);
|
|
967
967
|
for (t, test) in entry.tests.iter().enumerate() {
|
|
968
|
-
let holds = if merged.keyed & (1 << t) != 0 {
|
|
968
|
+
let holds = if merged.keyed & (1 << t) != 0 {
|
|
969
|
+
allowed & (1 << t) != 0
|
|
970
|
+
} else {
|
|
971
|
+
test.holds(v, &mut self.budget)
|
|
972
|
+
};
|
|
969
973
|
if !holds {
|
|
970
974
|
pass.failed |= 1 << test.condition;
|
|
971
975
|
}
|
|
@@ -973,7 +977,7 @@ impl Evaluator<'_, '_> {
|
|
|
973
977
|
}
|
|
974
978
|
None => {
|
|
975
979
|
for test in entry.tests.iter() {
|
|
976
|
-
if !test.holds(v) {
|
|
980
|
+
if !test.holds(v, &mut self.budget) {
|
|
977
981
|
pass.failed |= 1 << test.condition;
|
|
978
982
|
}
|
|
979
983
|
}
|
|
@@ -1004,7 +1008,7 @@ impl Evaluator<'_, '_> {
|
|
|
1004
1008
|
return Outcome::Done { cover, defer };
|
|
1005
1009
|
}
|
|
1006
1010
|
for (c, pattern) in f.absent.iter() {
|
|
1007
|
-
if pass.failed & (1 << c) == 0 && pattern.is_match(name) {
|
|
1011
|
+
if pass.failed & (1 << c) == 0 && pattern.is_match(name, &mut self.budget) {
|
|
1008
1012
|
pass.failed |= 1 << c;
|
|
1009
1013
|
}
|
|
1010
1014
|
}
|
|
@@ -21,7 +21,7 @@ use crate::instance::{ArrayView, Instance, Kind, ObjectView, View, str_eq};
|
|
|
21
21
|
use crate::node::*;
|
|
22
22
|
use crate::numbers::{Divisor, Num, cmp};
|
|
23
23
|
use crate::options::FormatValidator;
|
|
24
|
-
use crate::pattern::Pattern;
|
|
24
|
+
use crate::pattern::{Pattern, PatternBudget};
|
|
25
25
|
|
|
26
26
|
/// Every JSON type (a node without `type`).
|
|
27
27
|
const ANY: u8 = 0x7f;
|
|
@@ -790,7 +790,8 @@ fn plan_node(p: &Program, id: NodeId, n: &SchemaNode, target: &dyn Fn(NodeId) ->
|
|
|
790
790
|
.iter()
|
|
791
791
|
.map(|name| {
|
|
792
792
|
let patterns = n.pattern_properties.iter().flatten().enumerate();
|
|
793
|
-
|
|
793
|
+
let mut budget = p.compile_budget.lock().unwrap_or_else(|e| e.into_inner());
|
|
794
|
+
patterns.filter(|(_, pp)| pp.pattern.is_match(name, &mut budget)).map(|(j, _)| j as u16).collect()
|
|
794
795
|
})
|
|
795
796
|
.collect(),
|
|
796
797
|
additional: n.additional_properties.map(child),
|
|
@@ -1086,13 +1087,13 @@ impl Evaluator<'_, '_> {
|
|
|
1086
1087
|
#[inline(always)]
|
|
1087
1088
|
fn enter<'x, I: Instance<'x>>(&mut self, shape: Shape, b: &Body, x: I) -> bool {
|
|
1088
1089
|
match shape {
|
|
1089
|
-
Shape::Leaf => run_leaf(b, x),
|
|
1090
|
+
Shape::Leaf => run_leaf(b, x, &mut self.budget),
|
|
1090
1091
|
Shape::StringEnum => match (&b.values[0], x.view()) {
|
|
1091
1092
|
(Op::EnumStrings(values), View::String(s)) => values.contains(s),
|
|
1092
1093
|
_ => false,
|
|
1093
1094
|
},
|
|
1094
1095
|
Shape::Strings => match x.view() {
|
|
1095
|
-
View::String(s) => run_string(&b.string, s),
|
|
1096
|
+
View::String(s) => run_string(&b.string, s, &mut self.budget),
|
|
1096
1097
|
_ => true,
|
|
1097
1098
|
},
|
|
1098
1099
|
Shape::Object => match x.view() {
|
|
@@ -1219,7 +1220,7 @@ impl Evaluator<'_, '_> {
|
|
|
1219
1220
|
}
|
|
1220
1221
|
let ok = match view {
|
|
1221
1222
|
View::Number(n) => b.number.is_empty() || run_number(&b.number, &n),
|
|
1222
|
-
View::String(s) => b.string.is_empty() || run_string(&b.string, s),
|
|
1223
|
+
View::String(s) => b.string.is_empty() || run_string(&b.string, s, &mut self.budget),
|
|
1223
1224
|
View::Object(o) => b.object.as_ref().is_none_or(|plan| self.run_object(plan, o, x)),
|
|
1224
1225
|
View::Array(a) => {
|
|
1225
1226
|
b.array.as_ref().is_none_or(|plan| self.run_array::<I>(plan, a))
|
|
@@ -1423,7 +1424,7 @@ impl Evaluator<'_, '_> {
|
|
|
1423
1424
|
fn visit_pattern<'x, I: Instance<'x>>(&mut self, plan: &ObjectPlan, o: I::Object) -> bool {
|
|
1424
1425
|
let (pattern, c) = &plan.patterns[0];
|
|
1425
1426
|
for (k, v) in o.iter() {
|
|
1426
|
-
let applies = if pattern.is_match(k) { Some(*c) } else { plan.additional };
|
|
1427
|
+
let applies = if pattern.is_match(k, &mut self.budget) { Some(*c) } else { plan.additional };
|
|
1427
1428
|
if let Some(c) = applies
|
|
1428
1429
|
&& !self.run_child(c, v)
|
|
1429
1430
|
{
|
|
@@ -1455,7 +1456,7 @@ impl Evaluator<'_, '_> {
|
|
|
1455
1456
|
}
|
|
1456
1457
|
} else {
|
|
1457
1458
|
for (pattern, c) in plan.patterns.iter() {
|
|
1458
|
-
if pattern.is_match(k) {
|
|
1459
|
+
if pattern.is_match(k, &mut self.budget) {
|
|
1459
1460
|
matched = true;
|
|
1460
1461
|
if !self.run_child(*c, v) {
|
|
1461
1462
|
return None;
|
|
@@ -1557,7 +1558,7 @@ fn all_of_type<'x, I: Instance<'x>>(mut items: impl Iterator<Item = I>, types: u
|
|
|
1557
1558
|
|
|
1558
1559
|
/// A leaf's keywords: its value constraints, then those for the instance's type.
|
|
1559
1560
|
#[inline]
|
|
1560
|
-
fn run_leaf<'x, I: Instance<'x>>(b: &Body, x: I) -> bool {
|
|
1561
|
+
fn run_leaf<'x, I: Instance<'x>>(b: &Body, x: I, budget: &mut PatternBudget) -> bool {
|
|
1561
1562
|
for op in b.values.iter() {
|
|
1562
1563
|
let ok = match op {
|
|
1563
1564
|
Op::Const(c) => json_equal(x, c),
|
|
@@ -1571,7 +1572,7 @@ fn run_leaf<'x, I: Instance<'x>>(b: &Body, x: I) -> bool {
|
|
|
1571
1572
|
}
|
|
1572
1573
|
match x.view() {
|
|
1573
1574
|
View::Number(n) => b.number.is_empty() || run_number(&b.number, &n),
|
|
1574
|
-
View::String(s) => b.string.is_empty() || run_string(&b.string, s),
|
|
1575
|
+
View::String(s) => b.string.is_empty() || run_string(&b.string, s, budget),
|
|
1575
1576
|
_ => true,
|
|
1576
1577
|
}
|
|
1577
1578
|
}
|
|
@@ -1590,10 +1591,10 @@ fn run_number(ops: &[NumberOp], n: &Number) -> bool {
|
|
|
1590
1591
|
}
|
|
1591
1592
|
|
|
1592
1593
|
#[inline(never)]
|
|
1593
|
-
fn run_string(ops: &[StringOp], s: &str) -> bool {
|
|
1594
|
+
fn run_string(ops: &[StringOp], s: &str, budget: &mut PatternBudget) -> bool {
|
|
1594
1595
|
ops.iter().all(|op| match op {
|
|
1595
1596
|
StringOp::Length { min, max } => length_ok(s, *min, *max),
|
|
1596
|
-
StringOp::Pattern(p) => p.is_match(s),
|
|
1597
|
+
StringOp::Pattern(p) => p.is_match(s, budget),
|
|
1597
1598
|
StringOp::Format(f) => f.string(s),
|
|
1598
1599
|
StringOp::Content(kind) => content_ok(s, *kind),
|
|
1599
1600
|
})
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
//! the C# collecting mode (keyword order, paths, messages, which subschema results are committed or discarded).
|
|
5
5
|
|
|
6
6
|
use std::collections::HashMap;
|
|
7
|
-
use std::sync::OnceLock;
|
|
7
|
+
use std::sync::{Mutex, OnceLock};
|
|
8
8
|
|
|
9
9
|
use serde_json::{Number, Value};
|
|
10
10
|
|
|
@@ -14,7 +14,8 @@ use crate::formats::FormatKind;
|
|
|
14
14
|
use crate::instance::{ArrayView, Instance, Kind, ObjectView, View};
|
|
15
15
|
use crate::node::*;
|
|
16
16
|
use crate::numbers::{Num, cmp, multiple_of, num_eq, number_text};
|
|
17
|
-
use crate::options::FormatValidator;
|
|
17
|
+
use crate::options::{FormatValidator, SchemaEvaluationDepthError, SchemaEvaluationError};
|
|
18
|
+
use crate::pattern::{PatternBudget, PatternLimit};
|
|
18
19
|
use crate::results::{JsonSchemaResultsCollector, Message, encode_pointer_segment};
|
|
19
20
|
use crate::uri::resolve_pointer;
|
|
20
21
|
|
|
@@ -36,6 +37,11 @@ pub(crate) struct Program {
|
|
|
36
37
|
pub annotation_sources: Vec<Option<AnnotationSource>>,
|
|
37
38
|
pub annotations: OnceLock<Vec<Option<Vec<AnnotationEntry>>>>,
|
|
38
39
|
pub assert_format_set: bool,
|
|
40
|
+
/// The limit of a match of a pattern, which every evaluation gives its matches.
|
|
41
|
+
pub pattern_limit: PatternLimit,
|
|
42
|
+
/// The budget of the matches made while the plans are compiled (a pattern against the property names the
|
|
43
|
+
/// schema declares). It is not used once the program is made.
|
|
44
|
+
pub compile_budget: Mutex<PatternBudget>,
|
|
39
45
|
/// Fail-fast plans (with a dynamic scope kept, bodies push their resource on entry).
|
|
40
46
|
plans: Vec<plan::Plan>,
|
|
41
47
|
}
|
|
@@ -53,6 +59,7 @@ impl Program {
|
|
|
53
59
|
documents: Vec<Box<Value>>,
|
|
54
60
|
annotation_sources: Vec<Option<AnnotationSource>>,
|
|
55
61
|
assert_format_set: bool,
|
|
62
|
+
pattern_limit: PatternLimit,
|
|
56
63
|
) -> Program {
|
|
57
64
|
let fast_target = (0..nodes.len() as NodeId)
|
|
58
65
|
.map(|id| {
|
|
@@ -98,6 +105,8 @@ impl Program {
|
|
|
98
105
|
annotation_sources,
|
|
99
106
|
annotations: OnceLock::new(),
|
|
100
107
|
assert_format_set,
|
|
108
|
+
pattern_limit,
|
|
109
|
+
compile_budget: Mutex::new(PatternBudget::new(pattern_limit)),
|
|
101
110
|
plans: Vec::new(),
|
|
102
111
|
};
|
|
103
112
|
program.plans = plan::compile_plans(&program);
|
|
@@ -313,9 +322,8 @@ fn str_hash(s: &str) -> u64 {
|
|
|
313
322
|
const K: u64 = 0x9e37_79b9_7f4a_7c15;
|
|
314
323
|
let b = s.as_bytes();
|
|
315
324
|
let mut h = (b.len() as u64).wrapping_mul(K);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
h = (h.rotate_left(5) ^ u64::from_le_bytes(c.try_into().unwrap())).wrapping_mul(K);
|
|
325
|
+
for c in b.as_chunks::<8>().0 {
|
|
326
|
+
h = (h.rotate_left(5) ^ u64::from_le_bytes(*c)).wrapping_mul(K);
|
|
319
327
|
}
|
|
320
328
|
// The tail as one word without copying it out: the last eight bytes when there are that many (overlapping the
|
|
321
329
|
// chunks already hashed), else the overlapping first and last four, else the bytes themselves.
|
|
@@ -390,7 +398,7 @@ fn code_points(s: &str) -> u64 {
|
|
|
390
398
|
|
|
391
399
|
fn base64_decode(s: &str) -> Option<Vec<u8>> {
|
|
392
400
|
let b = s.as_bytes();
|
|
393
|
-
if b.len()
|
|
401
|
+
if !b.len().is_multiple_of(4) {
|
|
394
402
|
return None;
|
|
395
403
|
}
|
|
396
404
|
let val = |c: u8| -> Option<u32> {
|
|
@@ -548,6 +556,8 @@ pub(crate) struct Evaluator<'p, 'c> {
|
|
|
548
556
|
scope: Vec<u32>,
|
|
549
557
|
depth: u32,
|
|
550
558
|
pub depth_exceeded: bool,
|
|
559
|
+
/// What every match of a pattern in this evaluation has, and the first match that reached a limit.
|
|
560
|
+
pub(crate) budget: PatternBudget,
|
|
551
561
|
/// A string reused to evaluate property names against propertyNames (a `String`, not a `Value`: dropping a
|
|
552
562
|
/// `Value` is an out-of-line call, which every evaluation paid).
|
|
553
563
|
name_buffer: String,
|
|
@@ -581,7 +591,16 @@ macro_rules! and {
|
|
|
581
591
|
impl<'p, 'c> Evaluator<'p, 'c> {
|
|
582
592
|
pub fn new(p: &'p Program, c: Option<&'c mut JsonSchemaResultsCollector>) -> Self {
|
|
583
593
|
let annotations: &'p [Option<Vec<AnnotationEntry>>] = if c.is_some() { p.annotations() } else { &[] };
|
|
584
|
-
Evaluator {
|
|
594
|
+
Evaluator {
|
|
595
|
+
p,
|
|
596
|
+
annotations,
|
|
597
|
+
c,
|
|
598
|
+
scope: Vec::new(),
|
|
599
|
+
depth: 0,
|
|
600
|
+
depth_exceeded: false,
|
|
601
|
+
budget: PatternBudget::new(p.pattern_limit),
|
|
602
|
+
name_buffer: String::new(),
|
|
603
|
+
}
|
|
585
604
|
}
|
|
586
605
|
|
|
587
606
|
#[inline(always)]
|
|
@@ -598,8 +617,21 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
598
617
|
pub fn validate<'x, I: Instance<'x>>(&mut self, x: I) -> bool {
|
|
599
618
|
let root = self.p.fast_target[self.p.root as usize];
|
|
600
619
|
// An evaluation that recursed in place beyond the maximum depth is not valid, whatever it came to: the
|
|
601
|
-
// branch that was abandoned counts as false, which a not above it turns into true.
|
|
602
|
-
|
|
620
|
+
// branch that was abandoned counts as false, which a not above it turns into true. The same holds for a
|
|
621
|
+
// match of a pattern that reached its limit.
|
|
622
|
+
self.fast(root, x) && !self.depth_exceeded && !self.budget.failed()
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/// What an evaluation that came to `ok` reports: no answer, and why, when a match of a pattern reached its
|
|
626
|
+
/// limit or the schema recursed in place beyond the maximum depth.
|
|
627
|
+
pub fn outcome(&mut self, ok: bool) -> Result<bool, SchemaEvaluationError> {
|
|
628
|
+
if let Some(e) = self.budget.take_failure() {
|
|
629
|
+
return Err(SchemaEvaluationError::PatternMatchLimit(e));
|
|
630
|
+
}
|
|
631
|
+
if self.depth_exceeded {
|
|
632
|
+
return Err(SchemaEvaluationError::DepthExceeded(SchemaEvaluationDepthError));
|
|
633
|
+
}
|
|
634
|
+
Ok(ok)
|
|
603
635
|
}
|
|
604
636
|
|
|
605
637
|
/// Fail-fast evaluation of a node, through its plan when there are plans.
|
|
@@ -723,15 +755,15 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
723
755
|
}
|
|
724
756
|
_ => {}
|
|
725
757
|
}
|
|
726
|
-
if M::COLLECT
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
let v = &a.value;
|
|
733
|
-
self.col().ignored_keyword(Message::Lazy(&|| v.to_string()), &a.keyword);
|
|
758
|
+
if M::COLLECT
|
|
759
|
+
&& let Some(list) = &self.annotations[id as usize]
|
|
760
|
+
{
|
|
761
|
+
for a in list {
|
|
762
|
+
if a.strings_only && x.kind() != Kind::String {
|
|
763
|
+
continue;
|
|
734
764
|
}
|
|
765
|
+
let v = &a.value;
|
|
766
|
+
self.col().ignored_keyword(Message::Lazy(&|| v.to_string()), &a.keyword);
|
|
735
767
|
}
|
|
736
768
|
}
|
|
737
769
|
ok
|
|
@@ -742,24 +774,25 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
742
774
|
|
|
743
775
|
fn eval_number<M: Mode>(&mut self, n: &'p SchemaNode, x: &Number) -> bool {
|
|
744
776
|
let mut ok = true;
|
|
745
|
-
if n.assert_format
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
"format"
|
|
761
|
-
)
|
|
762
|
-
|
|
777
|
+
if n.assert_format
|
|
778
|
+
&& n.format_kind.is_numeric()
|
|
779
|
+
&& let Some(f) = &n.format
|
|
780
|
+
{
|
|
781
|
+
let m = match self.p.formats.get(f) {
|
|
782
|
+
Some(custom) => custom(&x.to_string()),
|
|
783
|
+
None => n.format_kind.check_number(x),
|
|
784
|
+
};
|
|
785
|
+
let kind = n.format_kind.name();
|
|
786
|
+
check!(
|
|
787
|
+
self,
|
|
788
|
+
M,
|
|
789
|
+
ok,
|
|
790
|
+
m,
|
|
791
|
+
Message::Lazy(&|| format!(
|
|
792
|
+
"The value was expected to be in a supported format, and within bounds for '{kind}'"
|
|
793
|
+
)),
|
|
794
|
+
"format"
|
|
795
|
+
);
|
|
763
796
|
}
|
|
764
797
|
let xv = Num::of(x);
|
|
765
798
|
use std::cmp::Ordering::*;
|
|
@@ -856,23 +889,24 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
856
889
|
self,
|
|
857
890
|
M,
|
|
858
891
|
ok,
|
|
859
|
-
p.is_match(s),
|
|
892
|
+
p.is_match(s, &mut self.budget),
|
|
860
893
|
Message::Lazy(&|| format!("Expected the value to match the regular expression{}", q(&p.source))),
|
|
861
894
|
"pattern"
|
|
862
895
|
);
|
|
863
896
|
}
|
|
864
|
-
if n.assert_format
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
),
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
}
|
|
897
|
+
if n.assert_format
|
|
898
|
+
&& !n.format_kind.is_numeric()
|
|
899
|
+
&& let Some(f) = &n.format
|
|
900
|
+
{
|
|
901
|
+
let (m, message): (bool, Message) = match self.p.formats.get(f) {
|
|
902
|
+
Some(custom) => (custom(s), Message::Lazy(&|| format!("Expected a string in the '{f}' format."))),
|
|
903
|
+
None if n.format_kind == FormatKind::Unknown => (true, Message::None),
|
|
904
|
+
None => (
|
|
905
|
+
n.format_kind.check_string(s, n.dialect <= Dialect::Draft6),
|
|
906
|
+
n.format_kind.message().map_or(Message::None, Message::Static),
|
|
907
|
+
),
|
|
908
|
+
};
|
|
909
|
+
check!(self, M, ok, m, message, "format");
|
|
876
910
|
}
|
|
877
911
|
if n.assert_content {
|
|
878
912
|
let m = content_ok(s, n.content);
|
|
@@ -963,7 +997,7 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
963
997
|
}
|
|
964
998
|
if let Some(pps) = &n.pattern_properties {
|
|
965
999
|
for pp in pps {
|
|
966
|
-
if !pp.pattern.is_match(k) {
|
|
1000
|
+
if !pp.pattern.is_match(k, &mut self.budget) {
|
|
967
1001
|
continue;
|
|
968
1002
|
}
|
|
969
1003
|
matched = true;
|
|
@@ -983,19 +1017,19 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
983
1017
|
);
|
|
984
1018
|
}
|
|
985
1019
|
}
|
|
986
|
-
if let Some(ap) = n.additional_properties
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
and!(
|
|
992
|
-
M,
|
|
993
|
-
ok,
|
|
994
|
-
self.eval_at::<M, I>(ap, &|| "additionalProperties".to_string(), v, &|| {
|
|
995
|
-
encode_pointer_segment(k).into_owned()
|
|
996
|
-
})
|
|
997
|
-
);
|
|
1020
|
+
if let Some(ap) = n.additional_properties
|
|
1021
|
+
&& !matched
|
|
1022
|
+
{
|
|
1023
|
+
if let Some(b) = bits.as_deref_mut() {
|
|
1024
|
+
b.set(i);
|
|
998
1025
|
}
|
|
1026
|
+
and!(
|
|
1027
|
+
M,
|
|
1028
|
+
ok,
|
|
1029
|
+
self.eval_at::<M, I>(ap, &|| "additionalProperties".to_string(), v, &|| {
|
|
1030
|
+
encode_pointer_segment(k).into_owned()
|
|
1031
|
+
})
|
|
1032
|
+
);
|
|
999
1033
|
}
|
|
1000
1034
|
if let Some(pn) = n.property_names {
|
|
1001
1035
|
let name = Value::String(k.to_string());
|
|
@@ -1185,10 +1219,10 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
1185
1219
|
};
|
|
1186
1220
|
if matched {
|
|
1187
1221
|
count += 1;
|
|
1188
|
-
if n.contains_marks_evaluated
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1222
|
+
if n.contains_marks_evaluated
|
|
1223
|
+
&& let Some(b) = bits.as_deref_mut()
|
|
1224
|
+
{
|
|
1225
|
+
b.set(i);
|
|
1192
1226
|
}
|
|
1193
1227
|
}
|
|
1194
1228
|
}
|
|
@@ -1296,10 +1330,8 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
1296
1330
|
if guarded {
|
|
1297
1331
|
self.depth -= 1;
|
|
1298
1332
|
}
|
|
1299
|
-
if ok {
|
|
1300
|
-
|
|
1301
|
-
b.merge(s);
|
|
1302
|
-
}
|
|
1333
|
+
if ok && let (Some(s), Some(b)) = (&scratch, bits) {
|
|
1334
|
+
b.merge(s);
|
|
1303
1335
|
}
|
|
1304
1336
|
(ok, scratch)
|
|
1305
1337
|
}
|
|
@@ -1402,10 +1434,10 @@ impl<'p, 'c> Evaluator<'p, 'c> {
|
|
|
1402
1434
|
}
|
|
1403
1435
|
}
|
|
1404
1436
|
}
|
|
1405
|
-
if matched == 1
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1437
|
+
if matched == 1
|
|
1438
|
+
&& let (Some(b), Some(o)) = (bits.as_deref_mut(), &only)
|
|
1439
|
+
{
|
|
1440
|
+
b.merge(o);
|
|
1409
1441
|
}
|
|
1410
1442
|
let message = match matched {
|
|
1411
1443
|
0 => MATCHED_NO_SCHEMA,
|
|
@@ -264,7 +264,7 @@ impl FormatKind {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
fn is_leap_year(y: u32) -> bool {
|
|
267
|
-
y
|
|
267
|
+
y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400))
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
fn digits(s: &[u8]) -> Option<u32> {
|
|
@@ -773,7 +773,20 @@ fn idn_label_ok(label: &str) -> bool {
|
|
|
773
773
|
true
|
|
774
774
|
}
|
|
775
775
|
|
|
776
|
+
/// The most bytes an internationalized host name has. A name is 253 characters at most as A-labels, and no label
|
|
777
|
+
/// is shorter as an A-label than it has characters: a letter of ASCII is itself there, any other character is a
|
|
778
|
+
/// digit or more, and `xn--` comes before them. So a name has 253 characters at most, each of four bytes at most.
|
|
779
|
+
const MAX_IDN_HOSTNAME_BYTES: usize = 4 * 253;
|
|
780
|
+
|
|
781
|
+
/// Checks an internationalized host name. A string over [`MAX_IDN_HOSTNAME_BYTES`] is not one, and is not read: the
|
|
782
|
+
/// check decodes and encodes Punycode and walks around every joiner and every katakana middle dot, which take time
|
|
783
|
+
/// that grows with the square of a label, so it must not be given a label of any length.
|
|
776
784
|
pub(crate) fn idn_hostname(s: &str) -> bool {
|
|
785
|
+
s.len() <= MAX_IDN_HOSTNAME_BYTES && idn_hostname_of_any_length(s)
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/// The check itself, for a string of any length.
|
|
789
|
+
fn idn_hostname_of_any_length(s: &str) -> bool {
|
|
777
790
|
if s.is_empty() {
|
|
778
791
|
return false;
|
|
779
792
|
}
|
|
@@ -932,6 +945,87 @@ mod tests {
|
|
|
932
945
|
assert!(DURATION_RE.is_match("P4DT12H30M5S") && !DURATION_RE.is_match("PT1D"));
|
|
933
946
|
}
|
|
934
947
|
|
|
948
|
+
/// The check of an internationalized host name decodes and encodes Punycode, looks for a kana for every katakana
|
|
949
|
+
/// middle dot and walks the neighbours of every joiner, each of which takes time that grows with the square of a
|
|
950
|
+
/// label. It did so for a label of any length: a host name of 100,000 different letters took 4 seconds to
|
|
951
|
+
/// refuse. A string over the bound is refused at once and not read. The time allowed here is generous.
|
|
952
|
+
#[test]
|
|
953
|
+
fn a_long_string_is_not_an_internationalized_host_name() {
|
|
954
|
+
const N: usize = 1_000_000;
|
|
955
|
+
let distinct: String = (0..N).map(|i| char::from_u32(0x4e00 + (i % 20000) as u32).unwrap()).collect();
|
|
956
|
+
for (name, text) in [
|
|
957
|
+
("katakana middle dots after a kana", format!("\u{30a2}{}", "\u{30fb}".repeat(N))),
|
|
958
|
+
("different letters", distinct),
|
|
959
|
+
("one letter", "\u{e9}".repeat(N)),
|
|
960
|
+
("joiners", format!("\u{628}{}\u{628}", "\u{64b}\u{200c}".repeat(N / 2))),
|
|
961
|
+
("a long A-label", format!("xn--{}-{}", "a".repeat(N / 2), "b9".repeat(N / 4))),
|
|
962
|
+
("labels", format!("{}a", "a.".repeat(N / 2))),
|
|
963
|
+
] {
|
|
964
|
+
for (format, text) in [(FormatKind::IdnHostname, text.clone()), (FormatKind::IdnEmail, format!("a@{text}"))]
|
|
965
|
+
{
|
|
966
|
+
let start = std::time::Instant::now();
|
|
967
|
+
let valid = format.check_string(&text, false);
|
|
968
|
+
let took = start.elapsed();
|
|
969
|
+
assert!(
|
|
970
|
+
!valid && took < std::time::Duration::from_secs(2),
|
|
971
|
+
"{format:?}, {name}: valid {valid} in {took:?}"
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/// What the bound rests on: every host name the check accepts has 253 characters at most, so 1,012 bytes at
|
|
978
|
+
/// most. Names are built from labels of letters that take one to four bytes, on both sides of the bound, and
|
|
979
|
+
/// the check is compared with the check that has no bound.
|
|
980
|
+
#[test]
|
|
981
|
+
fn no_internationalized_host_name_is_over_the_bound() {
|
|
982
|
+
// A small generator of its own, so that the test needs no crate: the numbers only have to be spread.
|
|
983
|
+
let mut state = 20261011u64;
|
|
984
|
+
let mut next = |below: usize| {
|
|
985
|
+
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
|
986
|
+
((state >> 33) as usize) % below
|
|
987
|
+
};
|
|
988
|
+
let letters =
|
|
989
|
+
['a', 'z', '0', '\u{e9}', '\u{3b1}', '\u{4e00}', '\u{30a2}', '\u{20000}', '\u{2a700}', '\u{10428}'];
|
|
990
|
+
let (mut accepted, mut longest, mut over) = (0, 0, 0);
|
|
991
|
+
for round in 0..20000 {
|
|
992
|
+
let mut name = String::new();
|
|
993
|
+
// A label of one letter of four bytes is the most bytes a name can have for its length as an A-label,
|
|
994
|
+
// so every fourth name is made of those.
|
|
995
|
+
let wide = round % 4 == 0;
|
|
996
|
+
let labels = 1 + next(1 + round % 500);
|
|
997
|
+
for label in 0..labels {
|
|
998
|
+
if wide {
|
|
999
|
+
name.push(letters[7 + next(2)]);
|
|
1000
|
+
} else {
|
|
1001
|
+
for _ in 0..1 + next(3) {
|
|
1002
|
+
name.push(letters[next(letters.len())]);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if label + 1 < labels {
|
|
1006
|
+
name.push('.');
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
let (bounded, unbounded) = (idn_hostname(&name), idn_hostname_of_any_length(&name));
|
|
1010
|
+
assert_eq!(bounded, unbounded, "{name} ({} bytes)", name.len());
|
|
1011
|
+
if unbounded {
|
|
1012
|
+
accepted += 1;
|
|
1013
|
+
longest = longest.max(name.len());
|
|
1014
|
+
}
|
|
1015
|
+
if name.len() > MAX_IDN_HOSTNAME_BYTES {
|
|
1016
|
+
over += 1;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
assert!(
|
|
1020
|
+
accepted >= 1000 && over >= 1000 && (100..=MAX_IDN_HOSTNAME_BYTES).contains(&longest),
|
|
1021
|
+
"{accepted} names accepted, the longest of {longest} bytes, {over} over the bound"
|
|
1022
|
+
);
|
|
1023
|
+
// The longest names there are: 253 ASCII characters, and a name over that by one.
|
|
1024
|
+
let label = "a".repeat(63);
|
|
1025
|
+
let at = format!("{label}.{label}.{label}.{}", "a".repeat(61));
|
|
1026
|
+
assert!(at.len() == 253 && idn_hostname(&at) && !idn_hostname(&format!("{at}a")));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
935
1029
|
/// The straightforward (allocating) readings of the IP address formats.
|
|
936
1030
|
fn reference_ipv4(s: &str) -> bool {
|
|
937
1031
|
let parts: Vec<&str> = s.split('.').collect();
|