hecks 3.6.0 → 3.7.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/lib/hecks/bluebook/meta_validator/syntax_boot/disk_cache.rb +28 -3
- data/lib/hecks/bluebook/meta_validator/verdict_cache/prebuilt.rb +44 -0
- data/lib/hecks/bluebook/meta_validator/verdict_cache.rb +3 -17
- data/lib/hecks/cache_dir.rb +10 -0
- data/lib/hecks/deploy/bluebook/deploy.bluebook +109 -0
- data/lib/hecks/deploy/oidc.json +10 -0
- data/lib/hecks/doors/usage_cache.rb +19 -9
- data/lib/hecks/fuzzing/coverage_campaign.rb +42 -17
- data/lib/hecks/fuzzing/failure_triage.rb +80 -0
- data/lib/hecks/fuzzing/form_census/chapter_forms.rb +38 -0
- data/lib/hecks/fuzzing/form_census/reference_forms.rb +49 -0
- data/lib/hecks/fuzzing/form_census.rb +28 -52
- data/lib/hecks/fuzzing/isolated_boot/postgres_scratch.rb +22 -7
- data/lib/hecks/fuzzing/isolated_boot/scratch_schema.rb +27 -0
- data/lib/hecks/fuzzing/isolated_boot.rb +6 -2
- data/lib/hecks/fuzzing/mutation/checks.rb +76 -0
- data/lib/hecks/fuzzing/mutation/finders.rb +136 -0
- data/lib/hecks/fuzzing/mutation/judge.rb +94 -0
- data/lib/hecks/fuzzing/mutation/observation.rb +38 -0
- data/lib/hecks/fuzzing/mutation/operators.rb +52 -0
- data/lib/hecks/fuzzing/mutation/report.rb +78 -0
- data/lib/hecks/fuzzing/mutation/run.rb +99 -0
- data/lib/hecks/fuzzing/mutation/selection.rb +32 -0
- data/lib/hecks/fuzzing/mutation/site.rb +21 -0
- data/lib/hecks/fuzzing/mutation/source_map.rb +59 -0
- data/lib/hecks/fuzzing/mutation.rb +12 -0
- data/lib/hecks/fuzzing/properties/catalog.rb +10 -59
- data/lib/hecks/fuzzing/properties/guaranteed_by_construction.rb +63 -0
- data/lib/hecks/fuzzing/properties/read_model_names.rb +50 -0
- data/lib/hecks/fuzzing/properties/read_models.rb +138 -0
- data/lib/hecks/fuzzing/properties/role_checks.rb +50 -0
- data/lib/hecks/fuzzing/properties.rb +7 -0
- data/lib/hecks/fuzzing/replay/observation.rb +1 -1
- data/lib/hecks/fuzzing/replay/role_check.rb +49 -0
- data/lib/hecks/fuzzing/replay/session.rb +22 -6
- data/lib/hecks/fuzzing/replay.rb +1 -0
- data/lib/hecks/fuzzing/runtime_coverage.rb +70 -0
- data/lib/hecks/fuzzing/shrinker/value_simplification.rb +64 -0
- data/lib/hecks/fuzzing/shrinker.rb +3 -0
- data/lib/hecks/fuzzing.rb +3 -0
- data/lib/hecks/gate/stages.yml +1 -1
- data/lib/hecks/hecks/adapters/deploy_toolchain/answers.rb +2 -1
- data/lib/hecks/hecks/adapters/github_rulesets.rb +7 -5
- data/lib/hecks/hecks/adapters/process_pool.rb +26 -4
- data/lib/hecks/hecks/custodian.behaviors +6 -0
- data/lib/hecks/hecks/custodian.bluebook +37 -0
- data/lib/hecks/hecks/hecks.hecksagon +6 -0
- data/lib/hecks/language/bluebook/vocabulary.bluebook +3 -0
- data/lib/hecks/projections/deploy/box/roll_scripts.rb +6 -4
- data/lib/hecks/projections/deploy/box/settings/patterns.rb +4 -0
- data/lib/hecks/projections/deploy/box/settings/types.rb +7 -3
- data/lib/hecks/projections/deploy/box/settings.rb +18 -0
- data/lib/hecks/projections/deploy/box/shared_database.rb +69 -0
- data/lib/hecks/projections/deploy/box/templates/Makefile.tmpl +6 -7
- data/lib/hecks/projections/deploy/box/templates/deploy-box.sh.tmpl +1 -1
- data/lib/hecks/projections/deploy/box/templates/provision-database.sh.tmpl +97 -0
- data/lib/hecks/projections/deploy/box/templates/restore-to-rds.sh.tmpl +4 -2
- data/lib/hecks/projections/deploy/box/templates/verify-copy.sh.tmpl +2 -1
- data/lib/hecks/projections/deploy/box.rb +14 -2
- data/lib/hecks/projections/deploy/shared_database_instance/templates/Makefile.tmpl +11 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/README.md.tmpl +25 -0
- data/lib/hecks/projections/deploy/shared_database_instance/templates/rds.yaml.tmpl +180 -0
- data/lib/hecks/projections/deploy/shared_database_instance.rb +106 -0
- data/lib/hecks/projections/deploy/vercel/settings.rb +83 -0
- data/lib/hecks/projections/deploy/vercel.rb +134 -0
- data/lib/hecks/projections/site/site_host/templates.rb +12 -4
- data/lib/hecks/projections.rb +2 -0
- data/lib/hecks/quality_control/adapters/github_checks.rb +9 -2
- data/lib/hecks/quality_control/cli/qa_sweep/dials.rb +1 -0
- data/lib/hecks/quality_control/cli/qa_sweep/seed_loop.rb +10 -2
- data/lib/hecks/rust_build/native_build.rb +29 -1
- data/lib/hecks/rust_build/wasm.rb +16 -3
- data/lib/hecks/tools/deploy_recipe/generation.rb +59 -0
- data/lib/hecks/tools/deploy_recipe/targets.rb +56 -2
- data/lib/hecks/tools/deploy_recipe/worlds.rb +2 -1
- data/lib/hecks/tools/deploy_recipe.rb +20 -10
- data/lib/hecks/tools/fuzz_sweep/options.rb +21 -10
- data/lib/hecks/tools/fuzz_sweep/reporting.rb +25 -3
- data/lib/hecks/tools/fuzz_sweep.rb +26 -9
- data/lib/hecks/tools/lanes/files.rb +2 -4
- data/lib/hecks/tools/lanes/live.rb +3 -4
- data/lib/hecks/tools/lanes/ruleset.rb +23 -1
- data/lib/hecks/tools/lanes.rb +12 -1
- data/lib/hecks/tools/mutation_run.rb +73 -0
- data/lib/hecks/tools.rb +1 -0
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +2 -0
- data/prebuilt/945481eefbdb584649d45f256307053558966a868b9c38dee7ee88f8169610f8.marshal +0 -0
- data/prebuilt/a0818e2de67364dc1ab22868b6cbf869c62da6f8d9b924c778fa5cce2e76ee94.marshal +0 -0
- data/prebuilt/verdicts-be73f0df9eee0abe2f4aadc968321c4b919d19d424a3ccfe1777a71dabf7d9e7.json +1 -0
- data/qa/settings.yml +1 -0
- data/rust/host/Cargo.toml +2 -2
- data/rust/host/HECKS_RELEASE +1 -1
- data/rust/host/src/api.rs +28 -5
- data/rust/host/src/auth.rs +11 -1
- data/rust/host/src/boundary_fuzz/api.rs +260 -0
- data/rust/host/src/boundary_fuzz/auth.rs +119 -0
- data/rust/host/src/boundary_fuzz/expr_json.rs +158 -0
- data/rust/host/src/boundary_fuzz/ir.rs +78 -0
- data/rust/host/src/boundary_fuzz/server.rs +227 -0
- data/rust/host/src/boundary_fuzz/web.rs +125 -0
- data/rust/host/src/fuzz_support.rs +165 -0
- data/rust/host/src/ir.rs +4 -0
- data/rust/host/src/main.rs +9 -4
- data/rust/host/src/secrets.rs +44 -0
- data/rust/host/src/server.rs +6 -1
- data/rust/host/src/web.rs +12 -2
- data/rust/parser/fuzz/.gitignore +4 -0
- data/rust/parser/fuzz/Cargo.lock +89 -0
- data/rust/parser/fuzz/Cargo.toml +27 -0
- data/rust/parser/fuzz/README.md +19 -0
- data/rust/parser/fuzz/fuzz_targets/chapter.rs +50 -0
- data/rust/parser/fuzz/fuzz_targets/resolve.rs +35 -0
- data/rust/parser/src/parse/command.rs +14 -2
- data/rust/parser/src/parse/mod.rs +25 -0
- data/rust/parser/src/ruby_value.rs +95 -6
- data/rust/src/kernel/json.rs +34 -3
- metadata +50 -1
data/qa/settings.yml
CHANGED
data/rust/host/Cargo.toml
CHANGED
|
@@ -61,6 +61,6 @@ async-trait = "0.1.92"
|
|
|
61
61
|
debug = 0
|
|
62
62
|
|
|
63
63
|
# The tests compile wasm modules through wasmtime's Cranelift, which is far slower unoptimized.
|
|
64
|
-
# Optimize dependencies only; the host crate
|
|
65
|
-
[profile.
|
|
64
|
+
# Optimize dependencies only, and only for cargo test; the host crate and cargo build stay at opt-level 0.
|
|
65
|
+
[profile.test.package."*"]
|
|
66
66
|
opt-level = 2
|
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.7.0
|
data/rust/host/src/api.rs
CHANGED
|
@@ -479,8 +479,13 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
479
479
|
}
|
|
480
480
|
Some("sequence") => {
|
|
481
481
|
let prefix = rule.get("prefix").and_then(|v| v.as_str()).unwrap_or("");
|
|
482
|
-
let pad = rule.get("pad").and_then(|v| v.as_u64()).unwrap_or(3)
|
|
483
|
-
|
|
482
|
+
let pad = rule.get("pad").and_then(|v| v.as_u64()).unwrap_or(3);
|
|
483
|
+
// `pad` rides in as free-form config JSON, and a width like 10^15 would have the
|
|
484
|
+
// formatter allocate that many bytes: refuse it rather than abort the process.
|
|
485
|
+
if pad > MAX_IDENTITY_PAD {
|
|
486
|
+
return Err(internal_error(&format!("the identity pad {pad} for {name} is wider than the {MAX_IDENTITY_PAD} this host will mint")));
|
|
487
|
+
}
|
|
488
|
+
Some(format!("{prefix}{:0>pad$}", existing + 1, pad = pad as usize))
|
|
484
489
|
}
|
|
485
490
|
// `port` delegates to a Ruby adapter this host has no runtime for
|
|
486
491
|
// (ADR 0007); refusing names the real reason rather than
|
|
@@ -499,6 +504,9 @@ fn apply_identity(presentation: &Value, aggregate: &Value, args: &mut Value, exi
|
|
|
499
504
|
Ok(())
|
|
500
505
|
}
|
|
501
506
|
|
|
507
|
+
/// The widest zero-padded `sequence` identity `apply_identity` will mint.
|
|
508
|
+
const MAX_IDENTITY_PAD: u64 = 64;
|
|
509
|
+
|
|
502
510
|
/// The wire key an identity value has to be wrapped in — whatever the
|
|
503
511
|
/// target value object's own single attribute is actually called. This
|
|
504
512
|
/// domain always spells it "value"; nothing here assumes that.
|
|
@@ -727,10 +735,21 @@ fn compare_sort_keys(left: Option<&Value>, right: Option<&Value>, descending: bo
|
|
|
727
735
|
}
|
|
728
736
|
}
|
|
729
737
|
|
|
738
|
+
/// Numbers, then strings, then anything else (all equal, so the id breaks the tie). A column can
|
|
739
|
+
/// hold mixed types, and `sort_by` panics on a comparison that is not a total order: comparing a
|
|
740
|
+
/// number with a string as `""` made `5`, `""` and `3` each equal to a neighbour but not to each other.
|
|
730
741
|
fn compare_values(left: &Value, right: &Value) -> Ordering {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
742
|
+
fn rank(value: &Value) -> u8 {
|
|
743
|
+
match value {
|
|
744
|
+
Value::Number(_) => 0,
|
|
745
|
+
Value::String(_) => 1,
|
|
746
|
+
_ => 2,
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
match (left, right) {
|
|
750
|
+
(Value::Number(a), Value::Number(b)) => a.as_f64().unwrap_or(0.0).total_cmp(&b.as_f64().unwrap_or(0.0)),
|
|
751
|
+
(Value::String(a), Value::String(b)) => a.cmp(b),
|
|
752
|
+
_ => rank(left).cmp(&rank(right)),
|
|
734
753
|
}
|
|
735
754
|
}
|
|
736
755
|
|
|
@@ -1594,3 +1613,7 @@ mod tests {
|
|
|
1594
1613
|
);
|
|
1595
1614
|
}
|
|
1596
1615
|
}
|
|
1616
|
+
|
|
1617
|
+
#[cfg(test)]
|
|
1618
|
+
#[path = "boundary_fuzz/api.rs"]
|
|
1619
|
+
mod boundary_fuzz;
|
data/rust/host/src/auth.rs
CHANGED
|
@@ -882,13 +882,19 @@ fn sign(secret: &str, payload: &str) -> String {
|
|
|
882
882
|
|
|
883
883
|
fn verify_sig(secret: &str, token: &str) -> Option<String> {
|
|
884
884
|
let (payload, sig) = token.rsplit_once('.')?;
|
|
885
|
-
if sign(secret, payload)
|
|
885
|
+
if constant_time_eq(sign(secret, payload).as_bytes(), sig.as_bytes()) {
|
|
886
886
|
Some(payload.to_string())
|
|
887
887
|
} else {
|
|
888
888
|
None
|
|
889
889
|
}
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
/// Compares two signatures without stopping at the first differing byte, so how long a forged
|
|
893
|
+
/// cookie takes to refuse does not tell the caller how many leading characters were right.
|
|
894
|
+
fn constant_time_eq(a: &[u8], b: &[u8]) -> bool {
|
|
895
|
+
a.len() == b.len() && a.iter().zip(b).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
|
|
896
|
+
}
|
|
897
|
+
|
|
892
898
|
fn hex_encode(bytes: &[u8]) -> String {
|
|
893
899
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
|
894
900
|
}
|
|
@@ -1341,3 +1347,7 @@ mod tests {
|
|
|
1341
1347
|
assert_eq!(state["role"]["value"], "Admin", "a lower ordinal must never move the snapshot backward");
|
|
1342
1348
|
}
|
|
1343
1349
|
}
|
|
1350
|
+
|
|
1351
|
+
#[cfg(test)]
|
|
1352
|
+
#[path = "boundary_fuzz/auth.rs"]
|
|
1353
|
+
mod boundary_fuzz;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
//! Seeded fuzz of the `/api` request-shaping helpers that run on caller-controlled input before
|
|
2
|
+
//! anything reaches the database: body parsing, identity minting, preconditions, sorting and
|
|
3
|
+
//! query-argument shaping. Each must refuse bad input with a structured answer, never panic, and
|
|
4
|
+
//! leave the arguments it was given untouched when it refuses.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::fuzz_support::*;
|
|
8
|
+
|
|
9
|
+
fn envelope_is_structured(response: &Value) {
|
|
10
|
+
let status = response["statusCode"].as_u64().expect("a refusal carries its status");
|
|
11
|
+
assert!((400..600).contains(&status), "a refusal is a 4xx/5xx, got {status}");
|
|
12
|
+
assert_eq!(response["headers"]["content-type"], "application/json");
|
|
13
|
+
let body: Value = serde_json::from_str(response["body"].as_str().expect("a JSON body")).expect("the body is JSON");
|
|
14
|
+
assert!(body["error"].is_string() && body["message"].is_string(), "the refusal names itself: {body}");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn client_aggregate() -> Value {
|
|
18
|
+
json!({
|
|
19
|
+
"name": "Client",
|
|
20
|
+
"identified_by": ["reference.value"],
|
|
21
|
+
"attributes": [
|
|
22
|
+
{"name": "reference", "type": "ClientReference", "list": false, "optional": false},
|
|
23
|
+
{"name": "name", "type": "ClientName", "list": false, "optional": false},
|
|
24
|
+
{"name": "owner", "type": "Reference<Member>", "list": false, "optional": true},
|
|
25
|
+
{"name": "status", "type": "String", "list": false, "optional": true}
|
|
26
|
+
],
|
|
27
|
+
"value_objects": [
|
|
28
|
+
{"name": "ClientReference", "attributes": [{"name": "value", "type": "String"}], "closed_set": false, "members": []},
|
|
29
|
+
{"name": "ClientName", "attributes": [{"name": "value", "type": "String"}], "closed_set": false, "members": []}
|
|
30
|
+
],
|
|
31
|
+
"entities": [], "commands": [], "queries": [], "lifecycle": {"field": "status"}
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[test]
|
|
36
|
+
fn parsed_body_refuses_everything_that_is_not_an_object_with_a_structured_400() {
|
|
37
|
+
for seed in 0..seeds() {
|
|
38
|
+
guarded(&format!("seed {seed}"), move || {
|
|
39
|
+
let mut rng = Rng::new(seed);
|
|
40
|
+
for _ in 0..50 {
|
|
41
|
+
let text = match rng.below(4) {
|
|
42
|
+
0 => random_text(&mut rng),
|
|
43
|
+
1 => {
|
|
44
|
+
let valid = random_value(&mut rng, 0).to_string();
|
|
45
|
+
mutate_text(&mut rng, &valid)
|
|
46
|
+
}
|
|
47
|
+
2 => random_value(&mut rng, 0).to_string(),
|
|
48
|
+
_ => format!("{}{}", " \t\n".repeat(rng.below(3)), random_value(&mut rng, 1)),
|
|
49
|
+
};
|
|
50
|
+
match parsed_body(&text) {
|
|
51
|
+
Ok(body) => {
|
|
52
|
+
assert!(body.is_object(), "seed {seed}: {text:?} parsed to a non-object");
|
|
53
|
+
assert!(text.trim().is_empty() || serde_json::from_str::<Value>(&text).is_ok(), "seed {seed}: {text:?} is not JSON yet was accepted");
|
|
54
|
+
}
|
|
55
|
+
Err(refusal) => {
|
|
56
|
+
assert_eq!(refusal["statusCode"], 400, "seed {seed}: {text:?}");
|
|
57
|
+
envelope_is_structured(&refusal);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn parsed_body_survives_depth_width_and_size() {
|
|
67
|
+
guarded("depth and size", || {
|
|
68
|
+
for text in [
|
|
69
|
+
"[".repeat(1_000_000),
|
|
70
|
+
format!("{}1{}", "[".repeat(100_000), "]".repeat(100_000)),
|
|
71
|
+
format!("{}1{}", "{\"a\":".repeat(100_000), "}".repeat(100_000)),
|
|
72
|
+
format!("[{}]", vec!["1"; 500_000].join(",")),
|
|
73
|
+
format!("{{\"a\":\"{}\"}}", "x".repeat(16 * 1024 * 1024)),
|
|
74
|
+
] {
|
|
75
|
+
let _ = parsed_body(&text);
|
|
76
|
+
}
|
|
77
|
+
for text in ["[]", "1", "\"s\"", "null", "true", "{", "{\"a\":1}{}", "{} {}", "\u{feff}{}", "{\"a\":NaN}", "{\"a\":1e999}"] {
|
|
78
|
+
if let Err(refusal) = parsed_body(text) {
|
|
79
|
+
assert_eq!(refusal["statusCode"], 400, "{text:?}");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
assert_eq!(parsed_body(" ").unwrap(), json!({}));
|
|
83
|
+
assert!(parsed_body("{\"a\":1,\"a\":2}").unwrap().is_object());
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[test]
|
|
88
|
+
fn slugify_always_answers_a_clean_non_empty_slug() {
|
|
89
|
+
for seed in 0..seeds() {
|
|
90
|
+
guarded(&format!("seed {seed}"), move || {
|
|
91
|
+
let mut rng = Rng::new(seed);
|
|
92
|
+
for _ in 0..100 {
|
|
93
|
+
let text = random_text(&mut rng);
|
|
94
|
+
let slug = slugify(&text);
|
|
95
|
+
assert!(!slug.is_empty(), "seed {seed}: {text:?} slugged to nothing");
|
|
96
|
+
assert!(slug.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-'), "seed {seed}: {text:?} -> {slug:?}");
|
|
97
|
+
assert!(!slug.starts_with('-') && !slug.ends_with('-') && !slug.contains("--"), "seed {seed}: {text:?} -> {slug:?}");
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[test]
|
|
104
|
+
fn identity_minting_survives_any_rule_and_leaves_args_alone_when_it_refuses() {
|
|
105
|
+
for seed in 0..seeds() {
|
|
106
|
+
guarded(&format!("seed {seed}"), move || {
|
|
107
|
+
let mut rng = Rng::new(seed ^ 0x1D);
|
|
108
|
+
let aggregate = client_aggregate();
|
|
109
|
+
for _ in 0..50 {
|
|
110
|
+
let mut rule = serde_json::Map::new();
|
|
111
|
+
for key in ["field", "strategy", "source", "prefix", "pad"] {
|
|
112
|
+
if rng.chance(5) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let value = match key {
|
|
116
|
+
"field" if rng.chance(2) => json!(*rng.pick(&["reference", "name", "owner", "status", "missing", ""])),
|
|
117
|
+
"strategy" if rng.chance(2) => json!(*rng.pick(&["slug", "sequence", "port", "nope", ""])),
|
|
118
|
+
"source" if rng.chance(2) => json!(*rng.pick(&["name", "reference", "owner", "missing", ""])),
|
|
119
|
+
"pad" if rng.chance(2) => json!(*rng.pick(&[0u64, 1, 3, 64, 65, 1_000, 1_000_000, 1u64 << 40, 1u64 << 62, u64::MAX])),
|
|
120
|
+
_ => random_value(&mut rng, 2),
|
|
121
|
+
};
|
|
122
|
+
rule.insert(key.to_string(), value);
|
|
123
|
+
}
|
|
124
|
+
let presentation = json!({"collections": {"Client": {"identity": Value::Object(rule)}}});
|
|
125
|
+
let mut args = random_value(&mut rng, 0);
|
|
126
|
+
if rng.chance(2) {
|
|
127
|
+
args = json!({"name": random_value(&mut rng, 2)});
|
|
128
|
+
}
|
|
129
|
+
let before = args.clone();
|
|
130
|
+
let existing = *rng.pick(&[0usize, 1, 41, 999_999, usize::MAX - 1]);
|
|
131
|
+
match apply_identity(&presentation, &aggregate, &mut args, existing) {
|
|
132
|
+
Ok(()) => {}
|
|
133
|
+
Err(refusal) => {
|
|
134
|
+
envelope_is_structured(&refusal);
|
|
135
|
+
assert_eq!(args, before, "seed {seed}: a refused identity must not have touched the args");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#[test]
|
|
144
|
+
fn an_absurd_sequence_pad_is_refused_rather_than_allocating_it() {
|
|
145
|
+
let aggregate = client_aggregate();
|
|
146
|
+
for pad in [65u64, 1_000_000, 1 << 40, u64::MAX] {
|
|
147
|
+
let presentation = json!({"collections": {"Client": {"identity": {"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": pad}}}});
|
|
148
|
+
let mut args = json!({"name": {"value": "Acme"}});
|
|
149
|
+
let refusal = apply_identity(&presentation, &aggregate, &mut args, 1).expect_err("a pad this wide is refused");
|
|
150
|
+
envelope_is_structured(&refusal);
|
|
151
|
+
assert_eq!(args, json!({"name": {"value": "Acme"}}), "pad {pad}: nothing was minted");
|
|
152
|
+
}
|
|
153
|
+
let presentation = json!({"collections": {"Client": {"identity": {"field": "reference", "strategy": "sequence", "prefix": "C-", "pad": 64}}}});
|
|
154
|
+
let mut args = json!({});
|
|
155
|
+
apply_identity(&presentation, &aggregate, &mut args, 0).expect("the widest allowed pad still mints");
|
|
156
|
+
assert_eq!(args["reference"]["value"].as_str().unwrap().len(), 2 + 64);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[test]
|
|
160
|
+
fn preconditions_survive_any_config_and_instances_shape() {
|
|
161
|
+
for seed in 0..seeds() {
|
|
162
|
+
guarded(&format!("seed {seed}"), move || {
|
|
163
|
+
let mut rng = Rng::new(seed ^ 0xC0);
|
|
164
|
+
for _ in 0..50 {
|
|
165
|
+
let domain = json!({"name": random_text(&mut rng), "aggregates": [client_aggregate(), {"name": "Member", "lifecycle": random_value(&mut rng, 2)}]});
|
|
166
|
+
let aggregate = client_aggregate();
|
|
167
|
+
let rule = if rng.chance(3) {
|
|
168
|
+
random_value(&mut rng, 0)
|
|
169
|
+
} else {
|
|
170
|
+
json!({"field": *rng.pick(&["owner", "name", "status", "missing"]), "state": random_value(&mut rng, 2), "message": random_value(&mut rng, 2)})
|
|
171
|
+
};
|
|
172
|
+
let args = if rng.chance(3) { random_value(&mut rng, 0) } else { json!({"owner": random_value(&mut rng, 2)}) };
|
|
173
|
+
let instances = if rng.chance(3) { random_value(&mut rng, 0) } else { json!({format!("{}::Member#m1", domain_name(&domain)): random_value(&mut rng, 2)}) };
|
|
174
|
+
if let Err(refusal) = check_precondition(&domain, &aggregate, &rule, &args, &instances) {
|
|
175
|
+
envelope_is_structured(&refusal);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[test]
|
|
183
|
+
fn sorting_records_of_mixed_types_never_panics_on_an_inconsistent_order() {
|
|
184
|
+
// `sort_by` may panic when its comparator is not a total order, so a column holding numbers,
|
|
185
|
+
// empty strings, strings, nulls and objects together must still order consistently.
|
|
186
|
+
for seed in 0..seeds() {
|
|
187
|
+
guarded(&format!("seed {seed}"), move || {
|
|
188
|
+
let mut rng = Rng::new(seed ^ 0x50);
|
|
189
|
+
for _ in 0..10 {
|
|
190
|
+
let mut records: Vec<(String, Value)> = (0..20 + rng.below(200))
|
|
191
|
+
.map(|i| {
|
|
192
|
+
let cell = match rng.below(8) {
|
|
193
|
+
0 => json!(rng.below(5)),
|
|
194
|
+
1 => json!(rng.below(5) as f64 / 2.0),
|
|
195
|
+
2 => json!(""),
|
|
196
|
+
3 => json!(*rng.pick(&["a", "b", "B", "10", "9", "é", "🍕"])),
|
|
197
|
+
4 => Value::Null,
|
|
198
|
+
5 => json!({"cents": rng.below(50)}),
|
|
199
|
+
6 => json!({"value": random_text(&mut rng)}),
|
|
200
|
+
_ => random_value(&mut rng, 2),
|
|
201
|
+
};
|
|
202
|
+
(format!("id{}", i % 17), json!({"col": cell}))
|
|
203
|
+
})
|
|
204
|
+
.collect();
|
|
205
|
+
for descending in [false, true] {
|
|
206
|
+
let spec = Some(SortSpec { field: "col".to_string(), descending });
|
|
207
|
+
sort_records(&mut records, &spec);
|
|
208
|
+
sort_records(&mut records, &None);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn query_arguments_and_sort_params_survive_hostile_input() {
|
|
217
|
+
for seed in 0..seeds() {
|
|
218
|
+
guarded(&format!("seed {seed}"), move || {
|
|
219
|
+
let mut rng = Rng::new(seed ^ 0x9A);
|
|
220
|
+
let aggregate = client_aggregate();
|
|
221
|
+
let domain = json!({"name": "Crm", "aggregates": [aggregate.clone()]});
|
|
222
|
+
for _ in 0..30 {
|
|
223
|
+
let named = if rng.chance(3) {
|
|
224
|
+
random_value(&mut rng, 0)
|
|
225
|
+
} else {
|
|
226
|
+
json!({"attributes": [
|
|
227
|
+
{"name": *rng.pick(&["name", "owner", "reference", "x"]), "type": *rng.pick(&["ClientName", "String", "Reference<Member>", "Nope", "Integer"]), "optional": rng.chance(2)},
|
|
228
|
+
random_value(&mut rng, 2)
|
|
229
|
+
]})
|
|
230
|
+
};
|
|
231
|
+
let mut params: HashMap<String, String> = HashMap::new();
|
|
232
|
+
for _ in 0..rng.below(5) {
|
|
233
|
+
params.insert(random_text(&mut rng), random_text(&mut rng));
|
|
234
|
+
}
|
|
235
|
+
params.insert("name".to_string(), random_text(&mut rng));
|
|
236
|
+
let _ = query_args_from_params(&aggregate, &named, ¶ms);
|
|
237
|
+
|
|
238
|
+
let sort: HashMap<String, String> = [("sort", random_text(&mut rng)), ("direction", random_text(&mut rng))].into_iter().map(|(k, v)| (k.to_string(), v)).collect();
|
|
239
|
+
let config = random_value(&mut rng, 0);
|
|
240
|
+
let _ = resolve_sort(&domain, &config, "Client", &aggregate, &sort);
|
|
241
|
+
let _ = resolve_sort(&domain, &json!({}), "Client", &aggregate, &HashMap::from([("sort".to_string(), "__state__".to_string())]));
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn identity_sources_and_scalars_render_any_json_value() {
|
|
249
|
+
for seed in 0..seeds() {
|
|
250
|
+
guarded(&format!("seed {seed}"), move || {
|
|
251
|
+
let mut rng = Rng::new(seed ^ 0x5C);
|
|
252
|
+
for _ in 0..50 {
|
|
253
|
+
let args = json!({"name": random_value(&mut rng, 0)});
|
|
254
|
+
let _ = dig_source(&args, "name");
|
|
255
|
+
let _ = dig_source(&random_value(&mut rng, 0), &random_text(&mut rng));
|
|
256
|
+
let _ = scalar_to_string(&random_value(&mut rng, 0));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
//! Seeded fuzz of the signed-cookie and token readers: whatever text arrives in a `Cookie`
|
|
2
|
+
//! header or a query string, it either verifies as exactly what this host signed or yields
|
|
3
|
+
//! nothing. A forged, truncated or tampered token must never read as a session, and none of it
|
|
4
|
+
//! may panic.
|
|
5
|
+
|
|
6
|
+
use super::*;
|
|
7
|
+
use crate::fuzz_support::*;
|
|
8
|
+
|
|
9
|
+
const SECRET: &str = "fuzz-secret";
|
|
10
|
+
|
|
11
|
+
fn session(rng: &mut Rng) -> Session {
|
|
12
|
+
Session { identity_id: random_text(rng), email: random_text(rng), name: random_text(rng), role: if rng.chance(2) { Some(random_text(rng)) } else { None } }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn a_signed_cookie_reads_back_as_the_session_it_was_made_from() {
|
|
17
|
+
for seed in 0..seeds() {
|
|
18
|
+
guarded(&format!("seed {seed}"), move || {
|
|
19
|
+
let mut rng = Rng::new(seed);
|
|
20
|
+
for _ in 0..30 {
|
|
21
|
+
let original = session(&mut rng);
|
|
22
|
+
let cookie = session_cookie(SECRET, &original);
|
|
23
|
+
let read = parse_session_cookie(SECRET, &cookie).unwrap_or_else(|| panic!("seed {seed}: own cookie for {:?} refused", original.email));
|
|
24
|
+
assert_eq!((read.identity_id, read.email, read.name, read.role), (original.identity_id, original.email, original.name, original.role), "seed {seed}");
|
|
25
|
+
assert!(parse_session_cookie("another-secret", &cookie).is_none(), "seed {seed}: a different secret verified the cookie");
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[test]
|
|
32
|
+
fn no_arbitrary_or_tampered_token_ever_verifies() {
|
|
33
|
+
for seed in 0..seeds() {
|
|
34
|
+
guarded(&format!("seed {seed}"), move || {
|
|
35
|
+
let mut rng = Rng::new(seed ^ 0x70C);
|
|
36
|
+
let genuine_cookie = session_cookie(SECRET, &session(&mut rng));
|
|
37
|
+
let genuine_account = account_token(SECRET, &random_text(&mut rng), 60);
|
|
38
|
+
let genuine_purpose = purpose_token(SECRET, "reset", json!({"email": random_text(&mut rng)}), 60);
|
|
39
|
+
for _ in 0..40 {
|
|
40
|
+
let junk = match rng.below(4) {
|
|
41
|
+
0 => random_text(&mut rng),
|
|
42
|
+
1 => format!("{}.{}", random_text(&mut rng), random_text(&mut rng)),
|
|
43
|
+
2 => ".".repeat(rng.below(4)),
|
|
44
|
+
_ => String::from_utf8_lossy(&random_bytes(&mut rng)).into_owned(),
|
|
45
|
+
};
|
|
46
|
+
assert!(parse_session_cookie(SECRET, &junk).is_none(), "seed {seed}: {junk:?} read as a session");
|
|
47
|
+
assert!(verify_account_token(SECRET, &junk).is_none(), "seed {seed}: {junk:?} read as an account token");
|
|
48
|
+
assert!(verify_purpose_token(SECRET, "reset", &junk).is_none(), "seed {seed}: {junk:?} read as a purpose token");
|
|
49
|
+
|
|
50
|
+
for (genuine, verifies) in [
|
|
51
|
+
(&genuine_cookie, (|t: &str| parse_session_cookie(SECRET, t).is_some()) as fn(&str) -> bool),
|
|
52
|
+
(&genuine_account, |t: &str| verify_account_token(SECRET, t).is_some()),
|
|
53
|
+
(&genuine_purpose, |t: &str| verify_purpose_token(SECRET, "reset", t).is_some()),
|
|
54
|
+
] {
|
|
55
|
+
assert!(verifies(genuine), "seed {seed}: the untampered token must verify");
|
|
56
|
+
let tampered = mutate_text(&mut rng, genuine);
|
|
57
|
+
assert!(tampered == *genuine || !verifies(&tampered), "seed {seed}: tampered token {tampered:?} verified");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[test]
|
|
65
|
+
fn a_token_for_one_purpose_never_verifies_for_another_or_as_an_account_token() {
|
|
66
|
+
let token = purpose_token(SECRET, "reset", json!({"email": "a@example.com"}), 60);
|
|
67
|
+
assert!(verify_purpose_token(SECRET, "reset", &token).is_some());
|
|
68
|
+
for other in ["", "RESET", "reset ", "reset:", "invite", "reset:fuzz-secret"] {
|
|
69
|
+
assert!(verify_purpose_token(SECRET, other, &token).is_none(), "purpose {other:?}");
|
|
70
|
+
}
|
|
71
|
+
assert!(verify_account_token(SECRET, &token).is_none());
|
|
72
|
+
assert!(parse_session_cookie(SECRET, &token).is_none());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn an_expired_or_unexpiring_signed_payload_is_not_a_session() {
|
|
77
|
+
let sign_payload = |payload: Value| {
|
|
78
|
+
let encoded = base64_encode(payload.to_string().as_bytes());
|
|
79
|
+
format!("{encoded}.{}", sign(SECRET, &encoded))
|
|
80
|
+
};
|
|
81
|
+
let person = json!({"identity_id": "i", "email": "e", "name": "n"});
|
|
82
|
+
for exp in [json!(0), json!(1), json!(-1), json!(1.5), json!("9999999999"), json!(null), json!(u64::MAX)] {
|
|
83
|
+
let mut payload = person.clone();
|
|
84
|
+
payload["exp"] = exp.clone();
|
|
85
|
+
let verdict = parse_session_cookie(SECRET, &sign_payload(payload)).is_some();
|
|
86
|
+
assert_eq!(verdict, exp == json!(u64::MAX), "exp {exp}");
|
|
87
|
+
}
|
|
88
|
+
assert!(parse_session_cookie(SECRET, &sign_payload(person)).is_none(), "no exp is no session");
|
|
89
|
+
for payload in [json!([]), json!(1), json!("s"), json!(null), json!({"exp": u64::MAX}), json!({"exp": u64::MAX, "identity_id": 1, "email": "e", "name": "n"})] {
|
|
90
|
+
assert!(parse_session_cookie(SECRET, &sign_payload(payload.clone())).is_none(), "{payload}");
|
|
91
|
+
assert!(verify_account_token(SECRET, &sign_payload(payload.clone())).is_none() || payload.get("email").is_some(), "{payload}");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn signature_comparison_checks_every_byte_and_the_length() {
|
|
97
|
+
assert!(constant_time_eq(b"", b""));
|
|
98
|
+
assert!(constant_time_eq(b"abc", b"abc"));
|
|
99
|
+
assert!(!constant_time_eq(b"abc", b"abd"));
|
|
100
|
+
assert!(!constant_time_eq(b"abc", b"xbc"));
|
|
101
|
+
assert!(!constant_time_eq(b"abc", b"ab"));
|
|
102
|
+
assert!(!constant_time_eq(b"ab", b"abc"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn base64_decoding_and_cookie_names_take_any_text() {
|
|
107
|
+
for seed in 0..seeds() {
|
|
108
|
+
guarded(&format!("seed {seed}"), move || {
|
|
109
|
+
let mut rng = Rng::new(seed ^ 0xBA5E);
|
|
110
|
+
for _ in 0..100 {
|
|
111
|
+
let text = random_text(&mut rng);
|
|
112
|
+
let _ = base64_decode(&text);
|
|
113
|
+
let _ = resolve_account_cookie(Some(&text));
|
|
114
|
+
let bytes = random_bytes(&mut rng);
|
|
115
|
+
assert_eq!(base64_decode(&base64_encode(&bytes)), bytes, "seed {seed}");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
//! Seeded fuzz of the invariant interpreter: an `ast` node read from the IR sidecar is parsed
|
|
2
|
+
//! and evaluated against a value object's fields, both of which can be wrong in every way
|
|
3
|
+
//! (unknown ops, wrong-typed fields, hostile regexes, overflowing arithmetic). The answer is an
|
|
4
|
+
//! `Ok` or an `Err(String)`, never a panic or a hang.
|
|
5
|
+
|
|
6
|
+
use crate::expr_json::{interpret, parse};
|
|
7
|
+
use crate::fuzz_support::*;
|
|
8
|
+
use serde_json::{json, Value};
|
|
9
|
+
|
|
10
|
+
const BINARY: &[&str] = &["or", "and", "compare", "include", "add", "modulo"];
|
|
11
|
+
const UNARY: &[&str] = &["not", "sign_test", "empty", "to_s", "size", "presence", "assignment", "first", "last", "split", "starts_with", "ends_with", "matches_regex"];
|
|
12
|
+
|
|
13
|
+
const PATTERNS: &[&str] = &[
|
|
14
|
+
"", "^$", "a", "(a*)*b", "(a|aa)+$", "[", "]", "[]", "[^]", "[a-", "(", ")", "(?", "(?P<x>", "\\", "\\d+", "\\D\\W\\S\\H", "\\Z", "[\\d]", "[\\Z]", "\\p{L}+", "\\p{Nope}",
|
|
15
|
+
"a{1000}", "a{1000}{1000}", "(((a{100}){100}){100})", "(?i)ABC", "(?x) a b", "\\x{110000}", "\\u{1F355}", ".*.*.*.*.*.*.*b", "\\b\\B", "(?<n>a)\\k<n>", "(a)\\1", "^*", "$+", "é+", "🍕{2}",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
fn leaf(rng: &mut Rng) -> Value {
|
|
19
|
+
match rng.below(9) {
|
|
20
|
+
0 => json!({"op": "int", "value": edge_number(rng)}),
|
|
21
|
+
1 => json!({"op": "float", "value": edge_number(rng)}),
|
|
22
|
+
2 => json!({"op": "str", "value": random_text(rng)}),
|
|
23
|
+
3 => json!({"op": "bool", "value": rng.chance(2)}),
|
|
24
|
+
4 => json!({"op": "nil"}),
|
|
25
|
+
5 => json!({"op": "lookup", "path": (0..1 + rng.below(3)).map(|_| *rng.pick(&["a", "b", "items", "name", "missing", "x", ""])).collect::<Vec<_>>()}),
|
|
26
|
+
6 => json!({"op": "array", "elements": (0..rng.below(4)).map(|_| json!({"op": "int", "value": rng.below(5)})).collect::<Vec<_>>()}),
|
|
27
|
+
7 => json!({"op": "int", "value": i64::MAX}),
|
|
28
|
+
_ => json!({"op": "str", "value": ""}),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn node(rng: &mut Rng, depth: usize) -> Value {
|
|
33
|
+
if depth >= 4 || rng.chance(4) {
|
|
34
|
+
return leaf(rng);
|
|
35
|
+
}
|
|
36
|
+
let cmp = || json!({"less_than": true, "equal": true, "negated": false});
|
|
37
|
+
match rng.below(10) {
|
|
38
|
+
0..=3 => {
|
|
39
|
+
let op = *rng.pick(BINARY);
|
|
40
|
+
let mut value = json!({"op": op, "left": node(rng, depth + 1), "right": node(rng, depth + 1), "receiver": node(rng, depth + 1), "divisor": node(rng, depth + 1), "haystack": node(rng, depth + 1), "needle": node(rng, depth + 1)});
|
|
41
|
+
value["cmp"] = cmp();
|
|
42
|
+
value
|
|
43
|
+
}
|
|
44
|
+
4..=7 => {
|
|
45
|
+
let op = *rng.pick(UNARY);
|
|
46
|
+
let mut value = json!({"op": op, "receiver": node(rng, depth + 1), "expr": node(rng, depth + 1), "negated": rng.chance(2), "separator": random_text(rng), "substring": random_text(rng), "pattern": *rng.pick(PATTERNS), "flags": *rng.pick(&["", "i", "m", "x", "imx", "z"])});
|
|
47
|
+
value["cmp"] = json!({"less_than": rng.chance(2), "equal": rng.chance(2), "negated": rng.chance(2)});
|
|
48
|
+
value
|
|
49
|
+
}
|
|
50
|
+
8 => json!({"op": *rng.pick(&["block_predicate", "find"]), "mode": *rng.pick(&["all", "any", "none", "some"]), "receiver": node(rng, depth + 1), "param": *rng.pick(&["i", "a", ""]), "predicate": node(rng, depth + 1), "path": [*rng.pick(&["a", "x"])]}),
|
|
51
|
+
_ => random_value(rng, 1),
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fn instance(rng: &mut Rng) -> Value {
|
|
56
|
+
let mut fields = serde_json::Map::new();
|
|
57
|
+
for key in ["a", "b", "items", "name", "x"] {
|
|
58
|
+
if !rng.chance(4) {
|
|
59
|
+
fields.insert(key.to_string(), if rng.chance(2) { random_value(rng, 1) } else { json!([{"x": 1}, {"x": "t"}, 3]) });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
Value::Object(fields)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[test]
|
|
66
|
+
fn any_expression_tree_parses_or_refuses_and_evaluates_or_refuses() {
|
|
67
|
+
for seed in 0..seeds() {
|
|
68
|
+
guarded(&format!("seed {seed}"), move || {
|
|
69
|
+
let mut rng = Rng::new(seed);
|
|
70
|
+
for _ in 0..40 {
|
|
71
|
+
let tree = node(&mut rng, 0);
|
|
72
|
+
let Ok(expr) = parse(&tree) else { continue };
|
|
73
|
+
for _ in 0..3 {
|
|
74
|
+
let _ = interpret(&expr, &instance(&mut rng));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#[test]
|
|
82
|
+
fn a_damaged_ast_document_is_refused_not_a_panic() {
|
|
83
|
+
for seed in 0..seeds() {
|
|
84
|
+
guarded(&format!("seed {seed}"), move || {
|
|
85
|
+
let mut rng = Rng::new(seed ^ 0xA57);
|
|
86
|
+
for _ in 0..40 {
|
|
87
|
+
let valid = node(&mut rng, 0).to_string();
|
|
88
|
+
let text = mutate_text(&mut rng, &valid);
|
|
89
|
+
if let Ok(tree) = serde_json::from_str::<Value>(&text) {
|
|
90
|
+
if let Ok(expr) = parse(&tree) {
|
|
91
|
+
let _ = interpret(&expr, &instance(&mut rng));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#[test]
|
|
100
|
+
fn arbitrary_json_is_never_an_expression() {
|
|
101
|
+
for seed in 0..seeds() {
|
|
102
|
+
guarded(&format!("seed {seed}"), move || {
|
|
103
|
+
let mut rng = Rng::new(seed ^ 0x77);
|
|
104
|
+
for _ in 0..100 {
|
|
105
|
+
let _ = parse(&random_value(&mut rng, 0));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn hostile_regexes_compile_or_refuse_quickly_and_match_without_blowing_up() {
|
|
113
|
+
for pattern in PATTERNS {
|
|
114
|
+
for flags in ["", "i", "mx"] {
|
|
115
|
+
let tree = json!({"op": "matches_regex", "receiver": {"op": "str", "value": "a".repeat(5_000)}, "pattern": pattern, "flags": flags});
|
|
116
|
+
let expr = parse(&tree).expect("a well-formed node");
|
|
117
|
+
guarded(&format!("{pattern:?} {flags:?}"), move || {
|
|
118
|
+
let _ = interpret(&expr, &json!({}));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn integer_arithmetic_at_the_limits_refuses_instead_of_overflowing() {
|
|
126
|
+
let int = |n: i64| json!({"op": "int", "value": n});
|
|
127
|
+
let cases = [
|
|
128
|
+
json!({"op": "add", "left": int(i64::MAX), "right": int(1)}),
|
|
129
|
+
json!({"op": "add", "left": int(i64::MIN), "right": int(-1)}),
|
|
130
|
+
json!({"op": "modulo", "receiver": int(i64::MIN), "divisor": int(-1)}),
|
|
131
|
+
json!({"op": "modulo", "receiver": int(i64::MAX), "divisor": int(i64::MIN)}),
|
|
132
|
+
json!({"op": "modulo", "receiver": int(1), "divisor": int(0)}),
|
|
133
|
+
json!({"op": "modulo", "receiver": {"op": "float", "value": 1.5}, "divisor": {"op": "float", "value": 0.0}}),
|
|
134
|
+
json!({"op": "add", "left": {"op": "float", "value": 1.7976931348623157e308}, "right": {"op": "float", "value": 1.7976931348623157e308}}),
|
|
135
|
+
];
|
|
136
|
+
for tree in cases {
|
|
137
|
+
let expr = parse(&tree).expect("well-formed");
|
|
138
|
+
let _ = interpret(&expr, &json!({}));
|
|
139
|
+
}
|
|
140
|
+
assert!(interpret(&parse(&cases_add_overflow()).unwrap(), &json!({})).is_err(), "i64::MAX + 1 must refuse");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
fn cases_add_overflow() -> Value {
|
|
144
|
+
json!({"op": "add", "left": {"op": "int", "value": i64::MAX}, "right": {"op": "int", "value": 1}})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[test]
|
|
148
|
+
fn a_document_nested_past_the_json_limit_never_reaches_the_interpreter() {
|
|
149
|
+
// The IR is read with serde_json, whose recursion limit refuses what would overflow `parse`.
|
|
150
|
+
let nested = format!("{}{{\"op\":\"nil\"}}{}", "{\"op\":\"not\",\"expr\":".repeat(10_000), "}".repeat(10_000));
|
|
151
|
+
assert!(serde_json::from_str::<Value>(&nested).is_err());
|
|
152
|
+
let within = format!("{}{{\"op\":\"nil\"}}{}", "{\"op\":\"not\",\"expr\":".repeat(60), "}".repeat(60));
|
|
153
|
+
guarded("depth 60", move || {
|
|
154
|
+
let tree: Value = serde_json::from_str(&within).expect("within serde's limit");
|
|
155
|
+
let expr = parse(&tree).expect("well-formed");
|
|
156
|
+
let _ = interpret(&expr, &json!({}));
|
|
157
|
+
});
|
|
158
|
+
}
|