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
@@ -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 { allowed & (1 << t) != 0 } else { test.holds(v) };
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
- patterns.filter(|(_, pp)| pp.pattern.is_match(name)).map(|(j, _)| j as u16).collect()
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
- let mut chunks = b.chunks_exact(8);
317
- for c in &mut chunks {
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() % 4 != 0 {
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 { p, annotations, c, scope: Vec::new(), depth: 0, depth_exceeded: false, name_buffer: String::new() }
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
- self.fast(root, x) && !self.depth_exceeded
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
- if let Some(list) = &self.annotations[id as usize] {
728
- for a in list {
729
- if a.strings_only && x.kind() != Kind::String {
730
- continue;
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 && n.format_kind.is_numeric() {
746
- if let Some(f) = &n.format {
747
- let m = match self.p.formats.get(f) {
748
- Some(custom) => custom(&x.to_string()),
749
- None => n.format_kind.check_number(x),
750
- };
751
- let kind = n.format_kind.name();
752
- check!(
753
- self,
754
- M,
755
- ok,
756
- m,
757
- Message::Lazy(&|| format!(
758
- "The value was expected to be in a supported format, and within bounds for '{kind}'"
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 && !n.format_kind.is_numeric() {
865
- if let Some(f) = &n.format {
866
- let (m, message): (bool, Message) = match self.p.formats.get(f) {
867
- Some(custom) => (custom(s), Message::Lazy(&|| format!("Expected a string in the '{f}' format."))),
868
- None if n.format_kind == FormatKind::Unknown => (true, Message::None),
869
- None => (
870
- n.format_kind.check_string(s, n.dialect <= Dialect::Draft6),
871
- n.format_kind.message().map_or(Message::None, Message::Static),
872
- ),
873
- };
874
- check!(self, M, ok, m, message, "format");
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
- if !matched {
988
- if let Some(b) = bits.as_deref_mut() {
989
- b.set(i);
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
- if let Some(b) = bits.as_deref_mut() {
1190
- b.set(i);
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
- if let (Some(s), Some(b)) = (&scratch, bits) {
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
- if let (Some(b), Some(o)) = (bits.as_deref_mut(), &only) {
1407
- b.merge(o);
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 % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
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();