corvus_json_schema 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +13 -12
- data/README.md +12 -1
- data/THIRD-PARTY-NOTICES.txt +96 -0
- data/VERSIONHISTORY.md +33 -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 +70 -20
- data/ext/corvus_json_schema/vendor/corvus-json-schema/Cargo.toml +2 -2
- data/ext/corvus_json_schema/vendor/corvus-json-schema/README.md +68 -4
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/compiler.rs +91 -97
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/document.rs +2 -2
- 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 +102 -7
- 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 +211 -4
- data/ext/corvus_json_schema/vendor/corvus-json-schema/src/pattern.rs +635 -586
- 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 +18 -3
- metadata +3 -2
|
@@ -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,
|
|
@@ -30,7 +30,8 @@ pub(crate) enum FormatKind {
|
|
|
30
30
|
UriTemplate,
|
|
31
31
|
JsonPointer,
|
|
32
32
|
RelativeJsonPointer,
|
|
33
|
-
|
|
33
|
+
/// The regex format, read in the grammar the dialect reads patterns with.
|
|
34
|
+
Regex(crate::pattern::Grammar),
|
|
34
35
|
// Numeric formats (a Corvus extension).
|
|
35
36
|
Byte,
|
|
36
37
|
UInt16,
|
|
@@ -49,7 +50,7 @@ pub(crate) enum FormatKind {
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
impl FormatKind {
|
|
52
|
-
pub fn of(format: &str, dialect: Dialect) -> FormatKind {
|
|
53
|
+
pub fn of(format: &str, dialect: Dialect, grammar: crate::pattern::Grammar) -> FormatKind {
|
|
53
54
|
use FormatKind::*;
|
|
54
55
|
let at_least = |d: Dialect, k: FormatKind| if dialect >= d { k } else { Unknown };
|
|
55
56
|
match format {
|
|
@@ -78,7 +79,7 @@ impl FormatKind {
|
|
|
78
79
|
"json-pointer" => at_least(Dialect::Draft6, JsonPointer),
|
|
79
80
|
"date" => at_least(Dialect::Draft7, Date),
|
|
80
81
|
"time" => at_least(Dialect::Draft7, Time),
|
|
81
|
-
"regex" => at_least(Dialect::Draft7, Regex),
|
|
82
|
+
"regex" => at_least(Dialect::Draft7, Regex(grammar)),
|
|
82
83
|
"relative-json-pointer" => at_least(Dialect::Draft7, RelativeJsonPointer),
|
|
83
84
|
"idn-email" => at_least(Dialect::Draft7, IdnEmail),
|
|
84
85
|
"idn-hostname" => at_least(Dialect::Draft7, IdnHostname),
|
|
@@ -133,7 +134,7 @@ impl FormatKind {
|
|
|
133
134
|
UriTemplate => "uri-template",
|
|
134
135
|
JsonPointer => "json-pointer",
|
|
135
136
|
RelativeJsonPointer => "relative-json-pointer",
|
|
136
|
-
Regex => "regex",
|
|
137
|
+
Regex(_) => "regex",
|
|
137
138
|
Byte => "byte",
|
|
138
139
|
UInt16 => "uint16",
|
|
139
140
|
UInt32 => "uint32",
|
|
@@ -175,7 +176,7 @@ impl FormatKind {
|
|
|
175
176
|
RelativeJsonPointer => {
|
|
176
177
|
"Expected a Relative JSON Pointer. (https://json-schema.org/draft/2020-12/relative-json-pointer)."
|
|
177
178
|
}
|
|
178
|
-
Regex => "Expected a regular expression specification.",
|
|
179
|
+
Regex(_) => "Expected a regular expression specification.",
|
|
179
180
|
_ => return None,
|
|
180
181
|
})
|
|
181
182
|
}
|
|
@@ -208,7 +209,7 @@ impl FormatKind {
|
|
|
208
209
|
UriTemplate => URI_TEMPLATE_RE.is_match(s),
|
|
209
210
|
JsonPointer => JSON_POINTER_RE.is_match(s),
|
|
210
211
|
RelativeJsonPointer => RELATIVE_JSON_POINTER_RE.is_match(s),
|
|
211
|
-
Regex => crate::pattern::is_valid_ecma_regex(s),
|
|
212
|
+
Regex(grammar) => crate::pattern::is_valid_ecma_regex(s, grammar),
|
|
212
213
|
_ => true,
|
|
213
214
|
}
|
|
214
215
|
}
|
|
@@ -264,7 +265,7 @@ impl FormatKind {
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
fn is_leap_year(y: u32) -> bool {
|
|
267
|
-
y
|
|
268
|
+
y.is_multiple_of(4) && (!y.is_multiple_of(100) || y.is_multiple_of(400))
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
fn digits(s: &[u8]) -> Option<u32> {
|
|
@@ -773,7 +774,20 @@ fn idn_label_ok(label: &str) -> bool {
|
|
|
773
774
|
true
|
|
774
775
|
}
|
|
775
776
|
|
|
777
|
+
/// The most bytes an internationalized host name has. A name is 253 characters at most as A-labels, and no label
|
|
778
|
+
/// is shorter as an A-label than it has characters: a letter of ASCII is itself there, any other character is a
|
|
779
|
+
/// digit or more, and `xn--` comes before them. So a name has 253 characters at most, each of four bytes at most.
|
|
780
|
+
const MAX_IDN_HOSTNAME_BYTES: usize = 4 * 253;
|
|
781
|
+
|
|
782
|
+
/// Checks an internationalized host name. A string over [`MAX_IDN_HOSTNAME_BYTES`] is not one, and is not read: the
|
|
783
|
+
/// check decodes and encodes Punycode and walks around every joiner and every katakana middle dot, which take time
|
|
784
|
+
/// that grows with the square of a label, so it must not be given a label of any length.
|
|
776
785
|
pub(crate) fn idn_hostname(s: &str) -> bool {
|
|
786
|
+
s.len() <= MAX_IDN_HOSTNAME_BYTES && idn_hostname_of_any_length(s)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/// The check itself, for a string of any length.
|
|
790
|
+
fn idn_hostname_of_any_length(s: &str) -> bool {
|
|
777
791
|
if s.is_empty() {
|
|
778
792
|
return false;
|
|
779
793
|
}
|
|
@@ -932,6 +946,87 @@ mod tests {
|
|
|
932
946
|
assert!(DURATION_RE.is_match("P4DT12H30M5S") && !DURATION_RE.is_match("PT1D"));
|
|
933
947
|
}
|
|
934
948
|
|
|
949
|
+
/// The check of an internationalized host name decodes and encodes Punycode, looks for a kana for every katakana
|
|
950
|
+
/// middle dot and walks the neighbours of every joiner, each of which takes time that grows with the square of a
|
|
951
|
+
/// label. It did so for a label of any length: a host name of 100,000 different letters took 4 seconds to
|
|
952
|
+
/// refuse. A string over the bound is refused at once and not read. The time allowed here is generous.
|
|
953
|
+
#[test]
|
|
954
|
+
fn a_long_string_is_not_an_internationalized_host_name() {
|
|
955
|
+
const N: usize = 1_000_000;
|
|
956
|
+
let distinct: String = (0..N).map(|i| char::from_u32(0x4e00 + (i % 20000) as u32).unwrap()).collect();
|
|
957
|
+
for (name, text) in [
|
|
958
|
+
("katakana middle dots after a kana", format!("\u{30a2}{}", "\u{30fb}".repeat(N))),
|
|
959
|
+
("different letters", distinct),
|
|
960
|
+
("one letter", "\u{e9}".repeat(N)),
|
|
961
|
+
("joiners", format!("\u{628}{}\u{628}", "\u{64b}\u{200c}".repeat(N / 2))),
|
|
962
|
+
("a long A-label", format!("xn--{}-{}", "a".repeat(N / 2), "b9".repeat(N / 4))),
|
|
963
|
+
("labels", format!("{}a", "a.".repeat(N / 2))),
|
|
964
|
+
] {
|
|
965
|
+
for (format, text) in [(FormatKind::IdnHostname, text.clone()), (FormatKind::IdnEmail, format!("a@{text}"))]
|
|
966
|
+
{
|
|
967
|
+
let start = std::time::Instant::now();
|
|
968
|
+
let valid = format.check_string(&text, false);
|
|
969
|
+
let took = start.elapsed();
|
|
970
|
+
assert!(
|
|
971
|
+
!valid && took < std::time::Duration::from_secs(2),
|
|
972
|
+
"{format:?}, {name}: valid {valid} in {took:?}"
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
/// What the bound rests on: every host name the check accepts has 253 characters at most, so 1,012 bytes at
|
|
979
|
+
/// most. Names are built from labels of letters that take one to four bytes, on both sides of the bound, and
|
|
980
|
+
/// the check is compared with the check that has no bound.
|
|
981
|
+
#[test]
|
|
982
|
+
fn no_internationalized_host_name_is_over_the_bound() {
|
|
983
|
+
// A small generator of its own, so that the test needs no crate: the numbers only have to be spread.
|
|
984
|
+
let mut state = 20261011u64;
|
|
985
|
+
let mut next = |below: usize| {
|
|
986
|
+
state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
|
987
|
+
((state >> 33) as usize) % below
|
|
988
|
+
};
|
|
989
|
+
let letters =
|
|
990
|
+
['a', 'z', '0', '\u{e9}', '\u{3b1}', '\u{4e00}', '\u{30a2}', '\u{20000}', '\u{2a700}', '\u{10428}'];
|
|
991
|
+
let (mut accepted, mut longest, mut over) = (0, 0, 0);
|
|
992
|
+
for round in 0..20000 {
|
|
993
|
+
let mut name = String::new();
|
|
994
|
+
// A label of one letter of four bytes is the most bytes a name can have for its length as an A-label,
|
|
995
|
+
// so every fourth name is made of those.
|
|
996
|
+
let wide = round % 4 == 0;
|
|
997
|
+
let labels = 1 + next(1 + round % 500);
|
|
998
|
+
for label in 0..labels {
|
|
999
|
+
if wide {
|
|
1000
|
+
name.push(letters[7 + next(2)]);
|
|
1001
|
+
} else {
|
|
1002
|
+
for _ in 0..1 + next(3) {
|
|
1003
|
+
name.push(letters[next(letters.len())]);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if label + 1 < labels {
|
|
1007
|
+
name.push('.');
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
let (bounded, unbounded) = (idn_hostname(&name), idn_hostname_of_any_length(&name));
|
|
1011
|
+
assert_eq!(bounded, unbounded, "{name} ({} bytes)", name.len());
|
|
1012
|
+
if unbounded {
|
|
1013
|
+
accepted += 1;
|
|
1014
|
+
longest = longest.max(name.len());
|
|
1015
|
+
}
|
|
1016
|
+
if name.len() > MAX_IDN_HOSTNAME_BYTES {
|
|
1017
|
+
over += 1;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
assert!(
|
|
1021
|
+
accepted >= 1000 && over >= 1000 && (100..=MAX_IDN_HOSTNAME_BYTES).contains(&longest),
|
|
1022
|
+
"{accepted} names accepted, the longest of {longest} bytes, {over} over the bound"
|
|
1023
|
+
);
|
|
1024
|
+
// The longest names there are: 253 ASCII characters, and a name over that by one.
|
|
1025
|
+
let label = "a".repeat(63);
|
|
1026
|
+
let at = format!("{label}.{label}.{label}.{}", "a".repeat(61));
|
|
1027
|
+
assert!(at.len() == 253 && idn_hostname(&at) && !idn_hostname(&format!("{at}a")));
|
|
1028
|
+
}
|
|
1029
|
+
|
|
935
1030
|
/// The straightforward (allocating) readings of the IP address formats.
|
|
936
1031
|
fn reference_ipv4(s: &str) -> bool {
|
|
937
1032
|
let parts: Vec<&str> = s.split('.').collect();
|