From b48b33841ea693726e954d1d0ef1557314ebeda1 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Fri, 24 Jul 2026 09:28:09 -0700 Subject: [PATCH 1/3] chore(fuzz): add cargo-fuzz suite and untrusted-input coverage gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a coverage-guided libFuzzer suite (via cargo-fuzz) over the crates that parse, decode, or evaluate untrusted input, a CI workflow that runs it on PRs and merges to main, and a hard "all untrusted inputs are fuzzed" gate. Motivation: the quality-evaluation reports kept re-discovering the same class of defect by hand — arithmetic overflow and char-boundary panics reachable from attacker-controlled expression text, templates, and manifests. Encoding that class as executable checks stops it from being re-litigated every review cycle. Twelve fuzz targets, each asserting "any input returns Ok or Err — never panic, abort, or hang": expr_parse, expr_evaluate (both POSIX and Windows path formats), range_expr, int_range_new, range_expr_slice, expr_type_parse, format_string, copy_symbol_value, model_decode, model_create_job, snapshot_decode, snapshot_ops. The fuzz crate is outside the root workspace (its own empty [workspace] table) so the stable build/test/clippy/MSRV jobs are untouched; it builds only under a pinned nightly with AddressSanitizer, overflow-checks, and debug-assertions on. Curated seed corpora (mined from the crates' own tests, sample templates, and i64/multibyte edge cases) live in fuzz/seeds/ and are committed. scripts/run_fuzz.sh derives the target list from `cargo fuzz list`, so new targets are picked up by CI automatically with nothing to maintain. Fuzz-coverage gate (scripts/check_fuzz_coverage.py + fuzz/fuzz_coverage.toml, runs on stable in the Compliance CI job — no nightly, no fuzz build): every public function taking a &str / &[u8] / serde_json::Value / &Path parameter must be classified in the registry as either fuzzed-by-a-target or explicitly not-untrusted-with-a-reason. The check enforces no orphan/phantom targets, no dangling classifications, and — the ratchet — that a newly-added untrusted-input entry point fails CI until triaged. 108 input-shaped public functions are classified today (69 fuzzed, 39 not-untrusted). Also fixes one bug the suite found, with a regression test: path_starts_with under the Windows path format sliced a path by the base string's byte length, panicking when that offset fell inside a multibyte char (expr report finding X8), surfaced once expr_evaluate exercised the Windows path format. Fixed with a byte-slice comparison (equivalent for ASCII case-folding, boundary-safe). Note: earlier revisions of this branch also fixed RangeExpr/IntRange integer overflows the fuzzer found; those are now superseded by upstream #276 (bounded range values), so this branch rebases onto that and drops the redundant range changes, keeping the fuzz targets that guard the area. All openjd-expr tests pass; cargo clippy -D warnings is clean workspace-wide. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- .github/workflows/ci.yml | 5 + .github/workflows/fuzz.yml | 87 + crates/openjd-expr/src/functions/path.rs | 9 +- .../tests/integration/test_paths.rs | 19 + fuzz/.gitignore | 11 + fuzz/Cargo.lock | 3090 +++++++++++++++++ fuzz/Cargo.toml | 130 + fuzz/README.md | 87 + fuzz/fuzz_coverage.toml | 417 +++ fuzz/fuzz_targets/copy_symbol_value.rs | 57 + fuzz/fuzz_targets/expr_evaluate.rs | 99 + fuzz/fuzz_targets/expr_parse.rs | 31 + fuzz/fuzz_targets/expr_type_parse.rs | 24 + fuzz/fuzz_targets/format_string.rs | 73 + fuzz/fuzz_targets/int_range_new.rs | 41 + fuzz/fuzz_targets/model_create_job.rs | 102 + fuzz/fuzz_targets/model_decode.rs | 43 + fuzz/fuzz_targets/range_expr.rs | 42 + fuzz/fuzz_targets/range_expr_slice.rs | 42 + fuzz/fuzz_targets/snapshot_decode.rs | 45 + fuzz/fuzz_targets/snapshot_ops.rs | 62 + fuzz/seeds/copy_symbol_value/basic | Bin 0 -> 46 bytes fuzz/seeds/copy_symbol_value/empty_symbol | Bin 0 -> 3 bytes fuzz/seeds/copy_symbol_value/nested | Bin 0 -> 17 bytes fuzz/seeds/copy_symbol_value/property | Bin 0 -> 21 bytes fuzz/seeds/expr_evaluate/expr_0 | 1 + fuzz/seeds/expr_evaluate/expr_1 | 1 + fuzz/seeds/expr_evaluate/expr_10 | 1 + fuzz/seeds/expr_evaluate/expr_100 | 1 + fuzz/seeds/expr_evaluate/expr_101 | 1 + fuzz/seeds/expr_evaluate/expr_103 | 1 + fuzz/seeds/expr_evaluate/expr_104 | 1 + fuzz/seeds/expr_evaluate/expr_106 | 1 + fuzz/seeds/expr_evaluate/expr_107 | 1 + fuzz/seeds/expr_evaluate/expr_109 | 1 + fuzz/seeds/expr_evaluate/expr_11 | 5 + fuzz/seeds/expr_evaluate/expr_110 | 1 + fuzz/seeds/expr_evaluate/mb_0 | 1 + fuzz/seeds/expr_evaluate/mb_1 | 1 + fuzz/seeds/expr_evaluate/mb_2 | 1 + fuzz/seeds/expr_evaluate/mb_3 | 1 + fuzz/seeds/expr_evaluate/mb_4 | 1 + fuzz/seeds/expr_evaluate/mb_5 | 1 + fuzz/seeds/expr_evaluate/mb_6 | 1 + fuzz/seeds/expr_evaluate/mb_7 | 1 + fuzz/seeds/expr_evaluate/mb_8 | 1 + fuzz/seeds/expr_evaluate/mb_9 | 1 + fuzz/seeds/expr_evaluate/mb_relto1 | Bin 0 -> 41 bytes fuzz/seeds/expr_evaluate/mb_relto2 | Bin 0 -> 50 bytes fuzz/seeds/expr_evaluate/mb_relto3 | Bin 0 -> 42 bytes fuzz/seeds/expr_evaluate/mb_x8 | Bin 0 -> 40 bytes fuzz/seeds/expr_evaluate/with_symtab_1 | Bin 0 -> 37 bytes fuzz/seeds/expr_evaluate/with_symtab_2 | Bin 0 -> 36 bytes fuzz/seeds/expr_parse/expr_0 | 1 + fuzz/seeds/expr_parse/expr_1 | 1 + fuzz/seeds/expr_parse/expr_10 | 1 + fuzz/seeds/expr_parse/expr_100 | 1 + fuzz/seeds/expr_parse/expr_101 | 1 + fuzz/seeds/expr_parse/expr_103 | 1 + fuzz/seeds/expr_parse/expr_104 | 1 + fuzz/seeds/expr_parse/expr_106 | 1 + fuzz/seeds/expr_parse/expr_107 | 1 + fuzz/seeds/expr_parse/expr_109 | 1 + fuzz/seeds/expr_parse/expr_11 | 5 + fuzz/seeds/expr_parse/expr_110 | 1 + fuzz/seeds/expr_parse/expr_111 | 1 + fuzz/seeds/expr_parse/expr_112 | 1 + fuzz/seeds/expr_parse/expr_115 | 1 + fuzz/seeds/expr_parse/mb_0 | 1 + fuzz/seeds/expr_parse/mb_1 | 1 + fuzz/seeds/expr_parse/mb_2 | 1 + fuzz/seeds/expr_parse/mb_3 | 1 + fuzz/seeds/expr_parse/mb_4 | 1 + fuzz/seeds/expr_parse/mb_5 | 1 + fuzz/seeds/expr_parse/mb_6 | 1 + fuzz/seeds/expr_parse/mb_7 | 1 + fuzz/seeds/expr_parse/mb_8 | 1 + fuzz/seeds/expr_parse/mb_9 | 1 + fuzz/seeds/expr_type_parse/any | 1 + fuzz/seeds/expr_type_parse/bool | 1 + fuzz/seeds/expr_type_parse/float | 1 + fuzz/seeds/expr_type_parse/int | 1 + fuzz/seeds/expr_type_parse/listint | 1 + fuzz/seeds/expr_type_parse/listlistint | 1 + fuzz/seeds/expr_type_parse/liststring | 1 + fuzz/seeds/expr_type_parse/noreturn | 1 + fuzz/seeds/expr_type_parse/path | 1 + fuzz/seeds/expr_type_parse/string | 1 + fuzz/seeds/expr_type_parse/unionintstring | 1 + fuzz/seeds/expr_type_parse/unresolvedint | 1 + fuzz/seeds/format_string/fmt_0 | 1 + fuzz/seeds/format_string/fmt_10 | 1 + fuzz/seeds/format_string/fmt_100 | 1 + fuzz/seeds/format_string/fmt_115 | 1 + fuzz/seeds/format_string/fmt_116 | 1 + fuzz/seeds/format_string/fmt_16 | 1 + fuzz/seeds/format_string/fmt_19 | 1 + fuzz/seeds/format_string/fmt_2 | 1 + fuzz/seeds/format_string/fmt_20 | 1 + fuzz/seeds/format_string/fmt_22 | 1 + fuzz/seeds/format_string/fmt_23 | 1 + fuzz/seeds/format_string/fmt_25 | 1 + fuzz/seeds/format_string/fmt_27 | 1 + fuzz/seeds/format_string/fmt_3 | 1 + fuzz/seeds/format_string/fmt_30 | 1 + fuzz/seeds/format_string/fmt_36 | 1 + fuzz/seeds/format_string/fmt_46 | 1 + fuzz/seeds/format_string/with_symtab | Bin 0 -> 39 bytes fuzz/seeds/int_range_new/seed_0 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_1 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_2 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_3 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_4 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_5 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_6 | Bin 0 -> 24 bytes fuzz/seeds/int_range_new/seed_7 | Bin 0 -> 24 bytes fuzz/seeds/model_create_job/tpl_0 | 156 + fuzz/seeds/model_create_job/tpl_1 | 53 + fuzz/seeds/model_create_job/tpl_10 | 9 + fuzz/seeds/model_create_job/tpl_11 | 9 + fuzz/seeds/model_create_job/tpl_12 | 17 + fuzz/seeds/model_create_job/tpl_13 | 17 + fuzz/seeds/model_create_job/tpl_14 | 17 + fuzz/seeds/model_create_job/tpl_15 | 11 + fuzz/seeds/model_create_job/tpl_16 | 18 + fuzz/seeds/model_create_job/tpl_17 | 9 + fuzz/seeds/model_create_job/tpl_18 | 9 + fuzz/seeds/model_create_job/tpl_19 | 16 + fuzz/seeds/model_create_job/tpl_2 | 51 + fuzz/seeds/model_create_job/tpl_20 | 23 + fuzz/seeds/model_create_job/tpl_21 | 131 + fuzz/seeds/model_create_job/tpl_22 | 52 + fuzz/seeds/model_create_job/tpl_23 | 28 + fuzz/seeds/model_create_job/tpl_24 | 18 + fuzz/seeds/model_create_job/tpl_25 | 18 + fuzz/seeds/model_create_job/tpl_26 | 47 + fuzz/seeds/model_create_job/tpl_27 | 24 + fuzz/seeds/model_create_job/tpl_28 | 18 + fuzz/seeds/model_create_job/tpl_29 | 23 + fuzz/seeds/model_create_job/tpl_3 | 79 + fuzz/seeds/model_create_job/tpl_30 | 21 + fuzz/seeds/model_create_job/tpl_31 | 9 + fuzz/seeds/model_create_job/tpl_4 | 15 + fuzz/seeds/model_create_job/tpl_5 | 15 + fuzz/seeds/model_create_job/tpl_6 | 15 + fuzz/seeds/model_create_job/tpl_7 | 15 + fuzz/seeds/model_create_job/tpl_8 | 19 + fuzz/seeds/model_create_job/tpl_9 | 23 + fuzz/seeds/model_create_job/with_params_0 | Bin 0 -> 4707 bytes fuzz/seeds/model_decode/tpl_0 | 156 + fuzz/seeds/model_decode/tpl_1 | 53 + fuzz/seeds/model_decode/tpl_10 | 9 + fuzz/seeds/model_decode/tpl_11 | 9 + fuzz/seeds/model_decode/tpl_12 | 17 + fuzz/seeds/model_decode/tpl_13 | 17 + fuzz/seeds/model_decode/tpl_14 | 17 + fuzz/seeds/model_decode/tpl_15 | 11 + fuzz/seeds/model_decode/tpl_16 | 18 + fuzz/seeds/model_decode/tpl_17 | 9 + fuzz/seeds/model_decode/tpl_18 | 9 + fuzz/seeds/model_decode/tpl_19 | 16 + fuzz/seeds/model_decode/tpl_2 | 51 + fuzz/seeds/model_decode/tpl_20 | 23 + fuzz/seeds/model_decode/tpl_21 | 131 + fuzz/seeds/model_decode/tpl_22 | 52 + fuzz/seeds/model_decode/tpl_23 | 28 + fuzz/seeds/model_decode/tpl_24 | 18 + fuzz/seeds/model_decode/tpl_25 | 18 + fuzz/seeds/model_decode/tpl_26 | 47 + fuzz/seeds/model_decode/tpl_3 | 79 + fuzz/seeds/model_decode/tpl_4 | 15 + fuzz/seeds/model_decode/tpl_5 | 15 + fuzz/seeds/model_decode/tpl_6 | 15 + fuzz/seeds/model_decode/tpl_7 | 15 + fuzz/seeds/model_decode/tpl_8 | 19 + fuzz/seeds/model_decode/tpl_9 | 23 + fuzz/seeds/range_expr/range_0 | 1 + fuzz/seeds/range_expr/range_1 | 1 + fuzz/seeds/range_expr/range_2 | 1 + fuzz/seeds/range_expr/range_3 | 1 + fuzz/seeds/range_expr/range_4 | 1 + fuzz/seeds/range_expr/range_5 | 1 + fuzz/seeds/range_expr/range_6 | 1 + fuzz/seeds/range_expr/range_7 | 1 + fuzz/seeds/range_expr/range_8 | 1 + fuzz/seeds/range_expr/range_9 | 1 + .../seed_0-10000000000001000000000 | 1 + fuzz/seeds/range_expr_slice/seed_0-1005 | 1 + fuzz/seeds/range_expr_slice/seed_1-10 | 1 + fuzz/seeds/range_expr_slice/seed_1-510-15 | 1 + fuzz/seeds/snapshot_decode/manifest_0 | 1 + fuzz/seeds/snapshot_decode/manifest_1 | 1 + fuzz/seeds/snapshot_decode/manifest_10 | 1 + fuzz/seeds/snapshot_decode/manifest_2 | 1 + fuzz/seeds/snapshot_decode/manifest_3 | 1 + fuzz/seeds/snapshot_decode/manifest_4 | 1 + fuzz/seeds/snapshot_decode/manifest_5 | 1 + fuzz/seeds/snapshot_decode/manifest_6 | 1 + fuzz/seeds/snapshot_decode/manifest_7 | 1 + fuzz/seeds/snapshot_decode/manifest_8 | 1 + fuzz/seeds/snapshot_decode/manifest_9 | 1 + fuzz/seeds/snapshot_ops/m_0 | 1 + fuzz/seeds/snapshot_ops/m_1 | 1 + fuzz/seeds/snapshot_ops/m_2 | 1 + fuzz/seeds/snapshot_ops/m_3 | 1 + fuzz/seeds/snapshot_ops/m_4 | 1 + fuzz/seeds/snapshot_ops/m_5 | 1 + fuzz/seeds/snapshot_ops/m_6 | 1 + fuzz/seeds/snapshot_ops/m_7 | 1 + fuzz/seeds/snapshot_ops/pair_0 | Bin 0 -> 649 bytes scripts/check_fuzz_coverage.py | 248 ++ scripts/run_fuzz.sh | 77 + 212 files changed, 6832 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/fuzz.yml create mode 100644 fuzz/.gitignore create mode 100644 fuzz/Cargo.lock create mode 100644 fuzz/Cargo.toml create mode 100644 fuzz/README.md create mode 100644 fuzz/fuzz_coverage.toml create mode 100644 fuzz/fuzz_targets/copy_symbol_value.rs create mode 100644 fuzz/fuzz_targets/expr_evaluate.rs create mode 100644 fuzz/fuzz_targets/expr_parse.rs create mode 100644 fuzz/fuzz_targets/expr_type_parse.rs create mode 100644 fuzz/fuzz_targets/format_string.rs create mode 100644 fuzz/fuzz_targets/int_range_new.rs create mode 100644 fuzz/fuzz_targets/model_create_job.rs create mode 100644 fuzz/fuzz_targets/model_decode.rs create mode 100644 fuzz/fuzz_targets/range_expr.rs create mode 100644 fuzz/fuzz_targets/range_expr_slice.rs create mode 100644 fuzz/fuzz_targets/snapshot_decode.rs create mode 100644 fuzz/fuzz_targets/snapshot_ops.rs create mode 100644 fuzz/seeds/copy_symbol_value/basic create mode 100644 fuzz/seeds/copy_symbol_value/empty_symbol create mode 100644 fuzz/seeds/copy_symbol_value/nested create mode 100644 fuzz/seeds/copy_symbol_value/property create mode 100644 fuzz/seeds/expr_evaluate/expr_0 create mode 100644 fuzz/seeds/expr_evaluate/expr_1 create mode 100644 fuzz/seeds/expr_evaluate/expr_10 create mode 100644 fuzz/seeds/expr_evaluate/expr_100 create mode 100644 fuzz/seeds/expr_evaluate/expr_101 create mode 100644 fuzz/seeds/expr_evaluate/expr_103 create mode 100644 fuzz/seeds/expr_evaluate/expr_104 create mode 100644 fuzz/seeds/expr_evaluate/expr_106 create mode 100644 fuzz/seeds/expr_evaluate/expr_107 create mode 100644 fuzz/seeds/expr_evaluate/expr_109 create mode 100644 fuzz/seeds/expr_evaluate/expr_11 create mode 100644 fuzz/seeds/expr_evaluate/expr_110 create mode 100644 fuzz/seeds/expr_evaluate/mb_0 create mode 100644 fuzz/seeds/expr_evaluate/mb_1 create mode 100644 fuzz/seeds/expr_evaluate/mb_2 create mode 100644 fuzz/seeds/expr_evaluate/mb_3 create mode 100644 fuzz/seeds/expr_evaluate/mb_4 create mode 100644 fuzz/seeds/expr_evaluate/mb_5 create mode 100644 fuzz/seeds/expr_evaluate/mb_6 create mode 100644 fuzz/seeds/expr_evaluate/mb_7 create mode 100644 fuzz/seeds/expr_evaluate/mb_8 create mode 100644 fuzz/seeds/expr_evaluate/mb_9 create mode 100644 fuzz/seeds/expr_evaluate/mb_relto1 create mode 100644 fuzz/seeds/expr_evaluate/mb_relto2 create mode 100644 fuzz/seeds/expr_evaluate/mb_relto3 create mode 100644 fuzz/seeds/expr_evaluate/mb_x8 create mode 100644 fuzz/seeds/expr_evaluate/with_symtab_1 create mode 100644 fuzz/seeds/expr_evaluate/with_symtab_2 create mode 100644 fuzz/seeds/expr_parse/expr_0 create mode 100644 fuzz/seeds/expr_parse/expr_1 create mode 100644 fuzz/seeds/expr_parse/expr_10 create mode 100644 fuzz/seeds/expr_parse/expr_100 create mode 100644 fuzz/seeds/expr_parse/expr_101 create mode 100644 fuzz/seeds/expr_parse/expr_103 create mode 100644 fuzz/seeds/expr_parse/expr_104 create mode 100644 fuzz/seeds/expr_parse/expr_106 create mode 100644 fuzz/seeds/expr_parse/expr_107 create mode 100644 fuzz/seeds/expr_parse/expr_109 create mode 100644 fuzz/seeds/expr_parse/expr_11 create mode 100644 fuzz/seeds/expr_parse/expr_110 create mode 100644 fuzz/seeds/expr_parse/expr_111 create mode 100644 fuzz/seeds/expr_parse/expr_112 create mode 100644 fuzz/seeds/expr_parse/expr_115 create mode 100644 fuzz/seeds/expr_parse/mb_0 create mode 100644 fuzz/seeds/expr_parse/mb_1 create mode 100644 fuzz/seeds/expr_parse/mb_2 create mode 100644 fuzz/seeds/expr_parse/mb_3 create mode 100644 fuzz/seeds/expr_parse/mb_4 create mode 100644 fuzz/seeds/expr_parse/mb_5 create mode 100644 fuzz/seeds/expr_parse/mb_6 create mode 100644 fuzz/seeds/expr_parse/mb_7 create mode 100644 fuzz/seeds/expr_parse/mb_8 create mode 100644 fuzz/seeds/expr_parse/mb_9 create mode 100644 fuzz/seeds/expr_type_parse/any create mode 100644 fuzz/seeds/expr_type_parse/bool create mode 100644 fuzz/seeds/expr_type_parse/float create mode 100644 fuzz/seeds/expr_type_parse/int create mode 100644 fuzz/seeds/expr_type_parse/listint create mode 100644 fuzz/seeds/expr_type_parse/listlistint create mode 100644 fuzz/seeds/expr_type_parse/liststring create mode 100644 fuzz/seeds/expr_type_parse/noreturn create mode 100644 fuzz/seeds/expr_type_parse/path create mode 100644 fuzz/seeds/expr_type_parse/string create mode 100644 fuzz/seeds/expr_type_parse/unionintstring create mode 100644 fuzz/seeds/expr_type_parse/unresolvedint create mode 100644 fuzz/seeds/format_string/fmt_0 create mode 100644 fuzz/seeds/format_string/fmt_10 create mode 100644 fuzz/seeds/format_string/fmt_100 create mode 100644 fuzz/seeds/format_string/fmt_115 create mode 100644 fuzz/seeds/format_string/fmt_116 create mode 100644 fuzz/seeds/format_string/fmt_16 create mode 100644 fuzz/seeds/format_string/fmt_19 create mode 100644 fuzz/seeds/format_string/fmt_2 create mode 100644 fuzz/seeds/format_string/fmt_20 create mode 100644 fuzz/seeds/format_string/fmt_22 create mode 100644 fuzz/seeds/format_string/fmt_23 create mode 100644 fuzz/seeds/format_string/fmt_25 create mode 100644 fuzz/seeds/format_string/fmt_27 create mode 100644 fuzz/seeds/format_string/fmt_3 create mode 100644 fuzz/seeds/format_string/fmt_30 create mode 100644 fuzz/seeds/format_string/fmt_36 create mode 100644 fuzz/seeds/format_string/fmt_46 create mode 100644 fuzz/seeds/format_string/with_symtab create mode 100644 fuzz/seeds/int_range_new/seed_0 create mode 100644 fuzz/seeds/int_range_new/seed_1 create mode 100644 fuzz/seeds/int_range_new/seed_2 create mode 100644 fuzz/seeds/int_range_new/seed_3 create mode 100644 fuzz/seeds/int_range_new/seed_4 create mode 100644 fuzz/seeds/int_range_new/seed_5 create mode 100644 fuzz/seeds/int_range_new/seed_6 create mode 100644 fuzz/seeds/int_range_new/seed_7 create mode 100644 fuzz/seeds/model_create_job/tpl_0 create mode 100644 fuzz/seeds/model_create_job/tpl_1 create mode 100644 fuzz/seeds/model_create_job/tpl_10 create mode 100644 fuzz/seeds/model_create_job/tpl_11 create mode 100644 fuzz/seeds/model_create_job/tpl_12 create mode 100644 fuzz/seeds/model_create_job/tpl_13 create mode 100644 fuzz/seeds/model_create_job/tpl_14 create mode 100644 fuzz/seeds/model_create_job/tpl_15 create mode 100644 fuzz/seeds/model_create_job/tpl_16 create mode 100644 fuzz/seeds/model_create_job/tpl_17 create mode 100644 fuzz/seeds/model_create_job/tpl_18 create mode 100644 fuzz/seeds/model_create_job/tpl_19 create mode 100644 fuzz/seeds/model_create_job/tpl_2 create mode 100644 fuzz/seeds/model_create_job/tpl_20 create mode 100644 fuzz/seeds/model_create_job/tpl_21 create mode 100644 fuzz/seeds/model_create_job/tpl_22 create mode 100644 fuzz/seeds/model_create_job/tpl_23 create mode 100644 fuzz/seeds/model_create_job/tpl_24 create mode 100644 fuzz/seeds/model_create_job/tpl_25 create mode 100644 fuzz/seeds/model_create_job/tpl_26 create mode 100644 fuzz/seeds/model_create_job/tpl_27 create mode 100644 fuzz/seeds/model_create_job/tpl_28 create mode 100644 fuzz/seeds/model_create_job/tpl_29 create mode 100644 fuzz/seeds/model_create_job/tpl_3 create mode 100644 fuzz/seeds/model_create_job/tpl_30 create mode 100644 fuzz/seeds/model_create_job/tpl_31 create mode 100644 fuzz/seeds/model_create_job/tpl_4 create mode 100644 fuzz/seeds/model_create_job/tpl_5 create mode 100644 fuzz/seeds/model_create_job/tpl_6 create mode 100644 fuzz/seeds/model_create_job/tpl_7 create mode 100644 fuzz/seeds/model_create_job/tpl_8 create mode 100644 fuzz/seeds/model_create_job/tpl_9 create mode 100644 fuzz/seeds/model_create_job/with_params_0 create mode 100644 fuzz/seeds/model_decode/tpl_0 create mode 100644 fuzz/seeds/model_decode/tpl_1 create mode 100644 fuzz/seeds/model_decode/tpl_10 create mode 100644 fuzz/seeds/model_decode/tpl_11 create mode 100644 fuzz/seeds/model_decode/tpl_12 create mode 100644 fuzz/seeds/model_decode/tpl_13 create mode 100644 fuzz/seeds/model_decode/tpl_14 create mode 100644 fuzz/seeds/model_decode/tpl_15 create mode 100644 fuzz/seeds/model_decode/tpl_16 create mode 100644 fuzz/seeds/model_decode/tpl_17 create mode 100644 fuzz/seeds/model_decode/tpl_18 create mode 100644 fuzz/seeds/model_decode/tpl_19 create mode 100644 fuzz/seeds/model_decode/tpl_2 create mode 100644 fuzz/seeds/model_decode/tpl_20 create mode 100644 fuzz/seeds/model_decode/tpl_21 create mode 100644 fuzz/seeds/model_decode/tpl_22 create mode 100644 fuzz/seeds/model_decode/tpl_23 create mode 100644 fuzz/seeds/model_decode/tpl_24 create mode 100644 fuzz/seeds/model_decode/tpl_25 create mode 100644 fuzz/seeds/model_decode/tpl_26 create mode 100644 fuzz/seeds/model_decode/tpl_3 create mode 100644 fuzz/seeds/model_decode/tpl_4 create mode 100644 fuzz/seeds/model_decode/tpl_5 create mode 100644 fuzz/seeds/model_decode/tpl_6 create mode 100644 fuzz/seeds/model_decode/tpl_7 create mode 100644 fuzz/seeds/model_decode/tpl_8 create mode 100644 fuzz/seeds/model_decode/tpl_9 create mode 100644 fuzz/seeds/range_expr/range_0 create mode 100644 fuzz/seeds/range_expr/range_1 create mode 100644 fuzz/seeds/range_expr/range_2 create mode 100644 fuzz/seeds/range_expr/range_3 create mode 100644 fuzz/seeds/range_expr/range_4 create mode 100644 fuzz/seeds/range_expr/range_5 create mode 100644 fuzz/seeds/range_expr/range_6 create mode 100644 fuzz/seeds/range_expr/range_7 create mode 100644 fuzz/seeds/range_expr/range_8 create mode 100644 fuzz/seeds/range_expr/range_9 create mode 100644 fuzz/seeds/range_expr_slice/seed_0-10000000000001000000000 create mode 100644 fuzz/seeds/range_expr_slice/seed_0-1005 create mode 100644 fuzz/seeds/range_expr_slice/seed_1-10 create mode 100644 fuzz/seeds/range_expr_slice/seed_1-510-15 create mode 100644 fuzz/seeds/snapshot_decode/manifest_0 create mode 100644 fuzz/seeds/snapshot_decode/manifest_1 create mode 100644 fuzz/seeds/snapshot_decode/manifest_10 create mode 100644 fuzz/seeds/snapshot_decode/manifest_2 create mode 100644 fuzz/seeds/snapshot_decode/manifest_3 create mode 100644 fuzz/seeds/snapshot_decode/manifest_4 create mode 100644 fuzz/seeds/snapshot_decode/manifest_5 create mode 100644 fuzz/seeds/snapshot_decode/manifest_6 create mode 100644 fuzz/seeds/snapshot_decode/manifest_7 create mode 100644 fuzz/seeds/snapshot_decode/manifest_8 create mode 100644 fuzz/seeds/snapshot_decode/manifest_9 create mode 100644 fuzz/seeds/snapshot_ops/m_0 create mode 100644 fuzz/seeds/snapshot_ops/m_1 create mode 100644 fuzz/seeds/snapshot_ops/m_2 create mode 100644 fuzz/seeds/snapshot_ops/m_3 create mode 100644 fuzz/seeds/snapshot_ops/m_4 create mode 100644 fuzz/seeds/snapshot_ops/m_5 create mode 100644 fuzz/seeds/snapshot_ops/m_6 create mode 100644 fuzz/seeds/snapshot_ops/m_7 create mode 100644 fuzz/seeds/snapshot_ops/pair_0 create mode 100644 scripts/check_fuzz_coverage.py create mode 100755 scripts/run_fuzz.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2da222c4..f4f6ac02 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,6 +68,11 @@ jobs: - uses: actions/checkout@v7 - name: Check copyright headers run: bash scripts/check_copyright_headers.sh + # Hard gate: every public fn taking untrusted input (&str/&[u8]/Value/&Path) + # must be classified in fuzz/fuzz_coverage.toml as fuzzed or not-untrusted. + # Pure source+manifest analysis on stable Python — no Rust build, no nightly. + - name: Check fuzz coverage registry + run: python3 scripts/check_fuzz_coverage.py # Build, test, and conformance share a single release-profile target/ cache. # This avoids maintaining separate debug and release caches for the same diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml new file mode 100644 index 00000000..c963c251 --- /dev/null +++ b/.github/workflows/fuzz.yml @@ -0,0 +1,87 @@ +name: Fuzz + +# Smoke-fuzz every fuzz target on pull requests and on merges to main. The +# run is short and time-boxed per target so it gates without slowing the merge +# queue. Any panic, abort, overflow, or char-boundary slice in a fuzzed entry +# point fails the job. Coverage plateaus within about a minute per target once +# seeded, so a longer run buys little here; deeper campaigns are left to manual +# local runs (see fuzz/README.md). +# +# The fuzz crate (fuzz/) is deliberately outside the root workspace and builds +# only with a nightly toolchain under AddressSanitizer, so it lives in its own +# workflow rather than the stable CI matrix. + +on: + push: + branches: [main] + pull_request: + branches: [main, release, "patch_*"] + workflow_dispatch: + +concurrency: + group: fuzz-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUSTUP_MAX_RETRIES: 10 + RUST_BACKTRACE: 1 + # Per-target smoke budget, in seconds. Coverage plateaus within a few seconds + # once seeded, so a short run catches regressions reachable from the corpus; + # deeper campaigns are run manually (see fuzz/README.md). + FUZZ_SECONDS: 10 + +jobs: + fuzz: + name: Fuzz + # libFuzzer + cargo-fuzz's sanitizer build is best supported on Linux. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + # cargo-fuzz requires a nightly toolchain (sanitizer -Z flags). Pin the + # nightly so a bad nightly (e.g. an ASan codegen ICE) can't randomly break + # the fuzz job; bump this date deliberately. + - name: Install Rust nightly + run: | + rustup toolchain install nightly-2026-05-15 --profile minimal --component rust-src + rustup override set nightly-2026-05-15 + + - name: Compute rustc hash + id: rustc + run: echo "hash=$(rustc +nightly-2026-05-15 --version --verbose | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" + + - name: Restore cargo + fuzz cache + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + ~/.cargo/bin/cargo-fuzz + fuzz/target + key: fuzz-${{ steps.rustc.outputs.hash }}-${{ hashFiles('**/Cargo.lock', 'fuzz/Cargo.toml') }} + restore-keys: | + fuzz-${{ steps.rustc.outputs.hash }}- + fuzz- + + - name: Install cargo-fuzz + run: which cargo-fuzz || cargo install cargo-fuzz --locked --version ^0.13 + + # Build + run every target. The target list is derived from + # `cargo fuzz list` inside the script, so a newly-added target is picked + # up automatically — nothing to update here. A crash in any target fails + # the job. + - name: Run fuzz targets + run: scripts/run_fuzz.sh + + # If any target produced a crash artifact, surface it so the failure is + # actionable from the run page instead of just a red X. + - name: Upload crash artifacts + if: failure() + uses: actions/upload-artifact@v7 + with: + name: fuzz-artifacts + path: fuzz/artifacts + if-no-files-found: ignore diff --git a/crates/openjd-expr/src/functions/path.rs b/crates/openjd-expr/src/functions/path.rs index 0e7750f0..a9433cc3 100644 --- a/crates/openjd-expr/src/functions/path.rs +++ b/crates/openjd-expr/src/functions/path.rs @@ -523,7 +523,14 @@ fn extract_unc_root(path: &str) -> Option<&str> { fn path_starts_with(path: &str, base: &str, fmt: PathFormat) -> bool { if fmt == PathFormat::Windows { - path.len() >= base.len() && path[..base.len()].eq_ignore_ascii_case(base) + // Case-insensitive prefix compare. Slicing `path[..base.len()]` would + // panic when `base.len()` lands inside a multibyte char in `path` + // (e.g. path "日" is 3 bytes, base "ab" is 2). Compare the raw byte + // prefix instead — `eq_ignore_ascii_case` on bytes is equivalent for + // the ASCII-only case-folding Windows path comparison uses, and byte + // slicing at `base.len()` is always valid. + let (pb, bb) = (path.as_bytes(), base.as_bytes()); + pb.len() >= bb.len() && pb[..bb.len()].eq_ignore_ascii_case(bb) } else { path.starts_with(base) } diff --git a/crates/openjd-expr/tests/integration/test_paths.rs b/crates/openjd-expr/tests/integration/test_paths.rs index aee328a5..03ca7963 100644 --- a/crates/openjd-expr/tests/integration/test_paths.rs +++ b/crates/openjd-expr/tests/integration/test_paths.rs @@ -168,6 +168,25 @@ fn is_relative_to_false() { ); } +// Regression: the Windows-format prefix compare in `path_starts_with` sliced +// the path by the base's byte length, which panicked when that offset landed +// inside a multibyte char (e.g. path "日" is 3 bytes, base "ab" is 2). See the +// expr quality report's exploratory finding X8; found by the expr_evaluate +// fuzz target once it exercised the Windows path format. Must evaluate to a +// bool, not panic. +#[test] +fn is_relative_to_multibyte_no_char_boundary_panic() { + assert_eq!( + eval_windows("P.is_relative_to('ab')", &windows_st("P", "日")).to_display_string(), + "false" + ); + // A multibyte base against a shorter multibyte path exercises the same slice. + assert_eq!( + eval_windows("P.is_relative_to('日本')", &windows_st("P", "日")).to_display_string(), + "false" + ); +} + // === TestRelativeTo === #[test] fn relative_to() { diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 00000000..7086a83f --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,11 @@ +target +corpus +artifacts +coverage + +# libFuzzer writes discovered inputs as 40-hex-char SHA1-named files. When you +# run a target with `fuzz/seeds/` as the corpus dir, it drops those into the +# seed dir. They are not curated seeds and must not be committed — ignore them +# so an accidental `git add` can't sweep them in. Curated seeds use readable +# names (expr_0, mb_1, tpl_2, …), which this pattern never matches. +seeds/*/[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f] diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 00000000..960bf1c0 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,3090 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "attribute-derive" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05832cdddc8f2650cc2cc187cc2e952b8c133a48eb055f35211f61ee81502d77" +dependencies = [ + "attribute-derive-macro", + "derive-where", + "manyhow", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "attribute-derive-macro" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7cdbbd4bd005c5d3e2e9c885e6fa575db4f4a3572335b974d8db853b6beb61" +dependencies = [ + "collection_literals", + "interpolator", + "manyhow", + "proc-macro-utils", + "proc-macro2", + "quote", + "quote-use", + "syn 2.0.119", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6b50a43f3ccdf331521c6d6c68b7cc9668b6e09d439ebda9569df5722324d76" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.139.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a159b9721a6a41468f967d1029bece78f410b0beb0594498435deb6ff72bfe48" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.2", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.109.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d214cdfa5bbe17f117e76a7643fadf32a5234fb597322ef8b1fb4b2f17dbbd" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.2", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9381123ab62d20c13082b151f30f962a3b112b727345394536dfa39a482944" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "635d23afda0a6ab48d666c4d447c4873e8d1e83518a2be2093122397e50b838e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2", + "http 1.4.2", + "hyper", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07505b34e8f4b3591a4fa69e9792b52289b95488dbbc68c3c0075b7bedb245e1" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b98f2e1fd67ec06618f9c291e5e495a468e60519e44c9c1979cd0521f3affdb" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.2", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.2", +] + +[[package]] +name = "aws-smithy-types" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6dc683efb34b9e755675b37fedbe0103141e5b6df7bdc9eb6967756a8c167d8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.2", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "bstr" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "collection_literals" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2550f75b8cfac212855f6b1885455df8eaee8fe8e246b647d69146142e016084" + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.6", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "get-size-derive2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b6d1e2f75c16bfbcd0f95d84f99858a6e2f885c2287d1f5c3a96e8444a34b4" +dependencies = [ + "attribute-derive", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "get-size2" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49cf31a6d70300cf81461098f7797571362387ef4bf85d32ac47eaa59b3a5a1a" +dependencies = [ + "compact_str", + "get-size-derive2", + "hashbrown 0.16.1", + "ordermap", + "smallvec", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "granit-parser" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.2", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.4.2", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.2", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http 1.4.2", + "http-body 1.1.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.2", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.2", + "http-body 1.1.0", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71dd52191aae121e8611f1e8dc3e324dd0dd1dee1e6dd91d10ee07a3cfb4d9d8" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "manyhow" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33efb3ca6d3b07393750d4030418d594ab1139cee518f0dc88db70fec873587" +dependencies = [ + "manyhow-macros", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "manyhow-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fce34d199b78b6e6073abf984c9cf5fd3e9330145a93ee0738a7443e371495" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openjd-expr" +version = "0.2.1" +dependencies = [ + "regex", + "regex-syntax", + "rustpython-ruff_python_ast", + "rustpython-ruff_python_parser", + "rustpython-ruff_text_size", + "serde", + "serde_json", + "shlex", + "thiserror", +] + +[[package]] +name = "openjd-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "openjd-expr", + "openjd-model", + "openjd-snapshots", + "serde_json", +] + +[[package]] +name = "openjd-model" +version = "0.4.0" +dependencies = [ + "indexmap", + "openjd-expr", + "regex", + "serde", + "serde-saphyr", + "serde_json", + "thiserror", +] + +[[package]] +name = "openjd-snapshots" +version = "0.1.5" +dependencies = [ + "async-trait", + "aws-sdk-s3", + "aws-sdk-sts", + "bytes", + "futures-util", + "glob", + "libc", + "rayon", + "rusqlite", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "walkdir", + "windows-sys 0.61.2", + "xxhash-rust", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "ordermap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7476a5b122ff1fce7208e7ee9dccd0a516e835f5b8b19b8f3c98a34cf757c1" +dependencies = [ + "indexmap", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-utils" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaf08a13de400bc215877b5bdc088f241b12eb42f0a548d3390dc1c56bb7071" +dependencies = [ + "proc-macro2", + "quote", + "smallvec", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "quote-use" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9619db1197b497a36178cfc736dc96b271fe918875fbf1344c436a7e93d0321e" +dependencies = [ + "quote", + "quote-use-macros", +] + +[[package]] +name = "quote-use-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82ebfb7faafadc06a7ab141a6f67bcfb24cb8beb158c6fe933f2f035afa99f35" +dependencies = [ + "proc-macro-utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror", +] + +[[package]] +name = "rusqlite" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustpython-ruff_python_ast" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f021ff72cabf5e2cd6d8ec8813d376a8445a228dc610ab56c27bd9054cda70d4" +dependencies = [ + "aho-corasick", + "bitflags", + "compact_str", + "get-size2", + "is-macro", + "memchr", + "rustc-hash", + "rustpython-ruff_python_trivia", + "rustpython-ruff_source_file", + "rustpython-ruff_text_size", + "thiserror", +] + +[[package]] +name = "rustpython-ruff_python_parser" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e6ee78bd9671fb5766664b2695fe1f2a92a961f4d9101646c570d8acdb1e0b" +dependencies = [ + "bitflags", + "bstr", + "compact_str", + "get-size2", + "memchr", + "rustc-hash", + "rustpython-ruff_python_ast", + "rustpython-ruff_python_trivia", + "rustpython-ruff_text_size", + "static_assertions", + "unicode-ident", + "unicode-normalization", + "unicode_names2", +] + +[[package]] +name = "rustpython-ruff_python_trivia" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e7cfd1056f3a02ff0d2d0e4474286ca963260782f878b7b81c1dd87432e682" +dependencies = [ + "itertools", + "rustpython-ruff_source_file", + "rustpython-ruff_text_size", + "unicode-ident", +] + +[[package]] +name = "rustpython-ruff_source_file" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948107aad62ddb12a11fc7bf68a49e52a0b0a3737d415a2505e54f5a9edac737" +dependencies = [ + "memchr", + "rustpython-ruff_text_size", +] + +[[package]] +name = "rustpython-ruff_text_size" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8291ee0f5a779e54ccd4e0151a0c426f8b49a123f99b5b6545db17ccdd4277aa" +dependencies = [ + "get-size2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" +dependencies = [ + "ahash", + "annotate-snippets", + "base64", + "encoding_rs_io", + "getrandom 0.3.4", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode_names2" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" +dependencies = [ + "phf", + "unicode_names2_generator", +] + +[[package]] +name = "unicode_names2_generator" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" +dependencies = [ + "getopts", + "log", + "phf_codegen", + "rand", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xxhash-rust" +version = "0.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee1b19627c7c60102ab80d3a9cbe18de90bfe03bfa6c3715447681f0e8c8af6" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 00000000..9ced0e07 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,130 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright by contributors to this project. +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +[package] +name = "openjd-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +# Keep this crate out of the root workspace. cargo-fuzz builds it standalone +# with a nightly toolchain and sanitizer `-Z` flags; an empty `[workspace]` +# table makes it its own workspace root so the stable build/test/clippy/MSRV +# jobs never see it. +[workspace] + +# This crate is deliberately NOT a member of the root workspace (the root +# Cargo.toml does not list `fuzz/`). cargo-fuzz builds it standalone with a +# nightly toolchain and `-Z` flags; keeping it out of the workspace avoids +# perturbing the stable build/test/clippy jobs and the MSRV check. + +[dependencies] +libfuzzer-sys = "0.4" +serde_json = "1" + +[dependencies.openjd-expr] +path = "../crates/openjd-expr" + +[dependencies.openjd-model] +path = "../crates/openjd-model" + +[dependencies.openjd-snapshots] +path = "../crates/openjd-snapshots" + +# Building the fuzz targets with debug assertions AND overflow checks is the +# whole point: many of the historically-found bugs were `attempt to * with +# overflow` panics that only fire when overflow-checks are on. Keeping them on +# in the release profile (which cargo-fuzz builds) means the sanitizer build +# still traps arithmetic overflow instead of silently wrapping. +[profile.release] +debug = 1 +overflow-checks = true +debug-assertions = true + +[[bin]] +name = "expr_parse" +path = "fuzz_targets/expr_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "expr_evaluate" +path = "fuzz_targets/expr_evaluate.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "range_expr" +path = "fuzz_targets/range_expr.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "int_range_new" +path = "fuzz_targets/int_range_new.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "range_expr_slice" +path = "fuzz_targets/range_expr_slice.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "format_string" +path = "fuzz_targets/format_string.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "model_decode" +path = "fuzz_targets/model_decode.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "snapshot_decode" +path = "fuzz_targets/snapshot_decode.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "model_create_job" +path = "fuzz_targets/model_create_job.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "snapshot_ops" +path = "fuzz_targets/snapshot_ops.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "expr_type_parse" +path = "fuzz_targets/expr_type_parse.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "copy_symbol_value" +path = "fuzz_targets/copy_symbol_value.rs" +test = false +doc = false +bench = false diff --git a/fuzz/README.md b/fuzz/README.md new file mode 100644 index 00000000..ff1d72e9 --- /dev/null +++ b/fuzz/README.md @@ -0,0 +1,87 @@ + + +# openjd-rs fuzzing + +Coverage-guided ([libFuzzer](https://llvm.org/docs/LibFuzzer.html) via +[`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)) fuzz targets for the +crates that parse or evaluate untrusted input. These targets encode, as +permanent executable checks, the class of bug the quality-evaluation reports +kept rediscovering by hand: arithmetic overflow, char-boundary panics, and +other crashes reachable from attacker-controlled expression text, templates, +and manifests. + +This crate is **not** a member of the root workspace (it has its own empty +`[workspace]` table). It builds only under a nightly toolchain with +AddressSanitizer, so keeping it out of the workspace leaves the stable +build/test/clippy/MSRV jobs untouched. It runs in its own +[`.github/workflows/fuzz.yml`](../.github/workflows/fuzz.yml) workflow. + +## Targets + +| Target | Entry point | What it guards | +|--------|-------------|----------------| +| `expr_parse` | `ParsedExpression::new` | Expression parser: no panic/abort below the input-length and depth caps. | +| `expr_evaluate` | `ParsedExpression::new` + `evaluate` (both POSIX and Windows path formats) | Full evaluator against a fuzzer-seeded symbol table — the arithmetic / path / coercion code where most historical crashes lived, including char-boundary handling of multibyte paths in the Windows path branch. | +| `range_expr` | `RangeExpr::from_str` + `len`/`get`/`iter` | Range parsing and materialization: integer overflow on extreme endpoints. | +| `int_range_new` | `IntRange::new` | Public range constructor with arbitrary `(start, end, step)` i64 triples — overflow in normalization the text parser can't reach (e.g. `step == i64::MIN`). | +| `range_expr_slice` | `RangeExpr::slice` | Public slice with arbitrary `i64` indices — overflow in the sub-range stride remapping. | +| `expr_type_parse` | `ExprType::parse` | Public recursive type-string parser (`list[int]`, `union[...]`) — malformed / deeply-nested input. | +| `format_string` | `FormatString::new` + `resolve_string_with` | Format-string parse + resolve, including embedded expressions. | +| `copy_symbol_value` | `copy_symbol_value` | Dotted-symbol walk/copy between symbol tables — adversarial names (empty segments, deep nesting, collisions). | +| `model_decode` | `document_string_to_object` + `decode_{job,environment}_template` | YAML/JSON template decode + validation (the `openjd check` path). | +| `model_create_job` | `preprocess_job_parameters` + `create_job` | Full template → job instantiation (the `openjd run` path): parameter coercion, format-string eval, parameter-space iteration and chunk arithmetic — one layer past `model_decode`. | +| `snapshot_decode` | `decode_manifest` + `Manifest::validate` | Snapshot manifest decode + invariant validation. | +| `snapshot_ops` | `diff`/`compose`/`partition`/`subtree`/`filter` on decoded manifests | Manifest operations over attacker-controlled decoded manifests — merging, size accounting, path arithmetic past `validate()`. | + +Every target's invariant is the same: for **any** input, the fuzzed code must +return `Ok` or `Err` — never panic, abort, or hang. + +## Running locally + +```sh +# One-time: nightly toolchain + cargo-fuzz. +rustup toolchain install nightly-2026-05-15 --component rust-src +cargo install cargo-fuzz --locked + +# Build + smoke-fuzz every target (this is exactly what CI runs). The target +# list is derived from `cargo fuzz list`, so new targets are picked up +# automatically — no list to maintain. +scripts/run_fuzz.sh + +# Longer campaign, or only specific targets: +FUZZ_SECONDS=300 scripts/run_fuzz.sh +scripts/run_fuzz.sh expr_evaluate range_expr + +# Or drive cargo-fuzz directly for one target, seeded with its committed corpus. +cargo +nightly-2026-05-15 fuzz run expr_evaluate fuzz/seeds/expr_evaluate + +# Reproduce / minimize a crash artifact. +cargo +nightly-2026-05-15 fuzz run range_expr fuzz/artifacts/range_expr/crash- +cargo +nightly-2026-05-15 fuzz tmin range_expr fuzz/artifacts/range_expr/crash- +``` + +`cargo fuzz build` builds every target with `overflow-checks` and +`debug-assertions` on (see `Cargo.toml`), so arithmetic overflow traps instead +of wrapping — the whole point of fuzzing this code base. + +## Corpus + +`seeds//` holds a **small, curated** set of starter inputs — one per +structurally-distinct input shape, plus known edge cases (multibyte strings, +i64 boundaries, symbol-table-prefixed expressions) — mined from the crates' own +tests and sample templates. Keep these lean: the goal is to prime each grammar +path once on a cold run, not to mirror the evolved corpus. The mutator +rediscovers trivial variants in seconds, so committing near-duplicates only +bloats the tree. A dozen-to-a-few-dozen files per target is the target size. + +CI seeds each run from this directory. The evolving runtime corpus +(`corpus//`) and crash artifacts (`artifacts/`) are git-ignored; +locally, `cargo fuzz` grows the runtime corpus across runs so coverage compounds +as you keep fuzzing. Note that running a target with `fuzz/seeds/` as the +corpus argument makes libFuzzer write newly-discovered inputs (SHA1-named files) +back into the seed dir — `.gitignore` excludes those so they can't be committed +by accident; only readably-named curated seeds are tracked. diff --git a/fuzz/fuzz_coverage.toml b/fuzz/fuzz_coverage.toml new file mode 100644 index 00000000..7e9bb20e --- /dev/null +++ b/fuzz/fuzz_coverage.toml @@ -0,0 +1,417 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright by contributors to this project. +# SPDX-License-Identifier: (Apache-2.0 OR MIT) +# +# Fuzz-coverage registry — the source of truth for scripts/check_fuzz_coverage.py. +# +# Every public function that takes untrusted input (a &str, &[u8], +# serde_json::Value, or &Path parameter) in openjd-expr / openjd-model / +# openjd-snapshots MUST appear here exactly once, either: +# +# [[fuzzed]] — reached by a fuzz target (name it), or +# [[not_untrusted]] — takes such a parameter but the value is NOT untrusted +# input at the trust boundary (explain why). +# +# The check enforces that the set of classified functions equals the set of +# input-shaped public functions in the source — so adding a new such function +# fails CI until it is triaged here (the ratchet), and renaming/removing one +# fails until its entry is updated (no dangling claims). +# +# `fn` keys are "::". When two entries would share +# a key (an overloaded name across impls in one file, e.g. several `new` or +# `check_constraints`), the check treats the file+name as covered once — a +# single classification applies to every same-named public fn in that file, +# which is the intended granularity here. + +# ─── Fuzz targets ──────────────────────────────────────────────────────────── +# Each key must match a fuzz/fuzz_targets/.rs file. The description is for +# humans; the check only verifies the name ↔ file correspondence. +[targets] +expr_parse = "openjd_expr::ParsedExpression::new / with_profile (parser)" +expr_evaluate = "parse + evaluate under both POSIX and Windows path formats" +range_expr = "RangeExpr::from_str + len/get/iter" +int_range_new = "IntRange::new with arbitrary i64 triples" +range_expr_slice = "RangeExpr::slice with arbitrary i64 indices" +expr_type_parse = "ExprType::parse (type-string parser)" +format_string = "FormatString::new + resolve_string_with" +copy_symbol_value = "copy_symbol_value dotted-name walk" +model_decode = "document_string_to_object + decode_{job,environment,}_template" +model_create_job = "preprocess_job_parameters + create_job (instantiation)" +snapshot_decode = "decode_manifest + Manifest::validate" +snapshot_ops = "diff/compose/partition/subtree/filter over decoded manifests" + +# ─── Fuzzed: reached by a target ───────────────────────────────────────────── + +# expr — parser / evaluator entry points +[[fuzzed]] +fn = "crates/openjd-expr/src/eval/parse.rs::new" +target = "expr_parse" +[[fuzzed]] +fn = "crates/openjd-expr/src/eval/parse.rs::with_profile" +target = "expr_parse" +[[fuzzed]] +fn = "crates/openjd-expr/src/types.rs::parse" +target = "expr_type_parse" +[[fuzzed]] +fn = "crates/openjd-expr/src/format_string.rs::new" +target = "format_string" +[[fuzzed]] +fn = "crates/openjd-expr/src/format_string.rs::with_profile" +target = "format_string" +[[fuzzed]] +fn = "crates/openjd-expr/src/format_string.rs::copy_symbol_value" +target = "copy_symbol_value" + +# expr — path / uri / value functions, reached from evaluated expressions under +# both path formats (path(), string ops, coercion) via expr_evaluate. +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path.rs::is_absolute" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path.rs::join" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path.rs::non_uri_join" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::pathlib_normalize" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::split" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::file_name" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::parent" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::file_stem" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::extension" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::extension_no_dot" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::parts" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/functions/path_parse.rs::suffixes" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::is_uri" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::parse" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::name" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::parent" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::suffix" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::suffixes" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::stem" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::parts" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/uri_path.rs::join" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/path_mapping.rs::apply" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/path_mapping.rs::apply_with_format" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/path_mapping.rs::apply_rules" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/path_mapping.rs::apply_rules_with_format" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/path_mapping.rs::is_uri" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/value.rs::from_str_coerce" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/value.rs::from_json_transport" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/value.rs::from_transport_value" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/value.rs::normalize_path_separators" +target = "expr_evaluate" + +# expr — symbol table: populated from the fuzzer-supplied JSON in expr_evaluate +# (set/set_string/set_table) and read back during evaluation. +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::set" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::set_string" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::set_table" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::get" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::get_value" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::get_string" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::get_table" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::contains" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::all_paths" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::from_value" +target = "copy_symbol_value" +[[fuzzed]] +fn = "crates/openjd-expr/src/symbol_table.rs::from_json_str" +target = "copy_symbol_value" + +# expr — edit-distance suggestions run when evaluation hits an unknown +# name/function, so the fuzzer reaches them with attacker-controlled names. +[[fuzzed]] +fn = "crates/openjd-expr/src/edit_distance.rs::edit_distance" +target = "expr_evaluate" +[[fuzzed]] +fn = "crates/openjd-expr/src/edit_distance.rs::suggest_closest" +target = "expr_evaluate" + +# model — decode + instantiate pipeline +[[fuzzed]] +fn = "crates/openjd-model/src/template/parse.rs::document_string_to_object" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/parse.rs::decode_job_template" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/parse.rs::decode_environment_template" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/parse.rs::decode_template" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/job/create_job/parameters.rs::preprocess_job_parameters" +target = "model_create_job" +# Constraint checks and constrained-string constructors run during +# decode/validate and job creation over attacker-controlled template values. +[[fuzzed]] +fn = "crates/openjd-model/src/template/parameters.rs::check_constraints" +target = "model_create_job" +[[fuzzed]] +fn = "crates/openjd-model/src/template/constrained_strings.rs::new" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/capabilities.rs::validate_amount_capability_name" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/capabilities.rs::validate_attribute_capability_name" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/validate_v2023_09/helpers.rs::has_control_chars" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/validate_v2023_09/helpers.rs::check_capability_reserved_scope" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/template/validate_v2023_09/helpers.rs::validate_env_var_name" +target = "model_decode" +[[fuzzed]] +fn = "crates/openjd-model/src/types.rs::from_spec_str" +target = "model_decode" + +# snapshots — decode + operations +[[fuzzed]] +fn = "crates/openjd-snapshots/src/codec.rs::decode_manifest" +target = "snapshot_decode" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/codec.rs::decode_v2023" +target = "snapshot_decode" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/codec.rs::decode_v2023_as_diff" +target = "snapshot_decode" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/codec.rs::decode_v2025" +target = "snapshot_decode" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_manifest" +target = "snapshot_ops" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/ops/filter.rs::matches_path" +target = "snapshot_ops" +[[fuzzed]] +fn = "crates/openjd-snapshots/src/ops/filter.rs::new" +target = "snapshot_ops" + +# ─── Not untrusted: takes an input-shaped param, but the value is trusted ───── + +# expr — ExpressionError builders. Called by the library itself while +# constructing diagnostics; the &str is the crate's own message/source text, +# not an external input crossing the trust boundary. +[[not_untrusted]] +fn = "crates/openjd-expr/src/error.rs::with_node" +reason = "internal diagnostic builder; &str is the crate's own expression source, already parsed" +[[not_untrusted]] +fn = "crates/openjd-expr/src/error.rs::with_span" +reason = "internal diagnostic builder; operates on the already-parsed source string" +[[not_untrusted]] +fn = "crates/openjd-expr/src/error.rs::set_source_span" +reason = "internal diagnostic builder; operates on the already-parsed source string" +[[not_untrusted]] +fn = "crates/openjd-expr/src/error.rs::message_with_expr_prefix" +reason = "internal diagnostic formatter over already-constructed error state" +[[not_untrusted]] +fn = "crates/openjd-expr/src/error.rs::division_by_zero" +reason = "&'static str is a fixed operator label ('Division' or 'Modulo') passed by the evaluator, not external input" +[[not_untrusted]] +fn = "crates/openjd-expr/src/eval/evaluator.rs::with_expr_source" +reason = "internal wiring set by ParsedExpression::evaluator; source is the already-parsed expression text, exercised via expr_evaluate" +[[not_untrusted]] +fn = "crates/openjd-expr/src/format_string.rs::escape_format_string" +reason = "output helper: escapes a caller-owned literal for embedding; total function, no parsing" + +# expr — FunctionLibrary registration/lookup. The names are library-author +# constants (host code registering functions), reached via evaluation only with +# already-parsed identifiers, which expr_evaluate covers through call/call_method. +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::register" +reason = "host registers functions at library-build time with static names, not external input" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::register_sig" +reason = "host registers signatures at library-build time with static names, not external input" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::get_signatures" +reason = "lookup by a name already validated during parsing/evaluation" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::call" +reason = "dispatch by an identifier the parser already produced; the evaluator (expr_evaluate) drives it" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::call_method" +reason = "dispatch by an identifier the parser already produced; the evaluator (expr_evaluate) drives it" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::derive_return_type" +reason = "static type-inference over already-parsed names, no untrusted-length arithmetic" +[[not_untrusted]] +fn = "crates/openjd-expr/src/function_library.rs::get_property_type" +reason = "static type-inference over already-parsed names, no untrusted-length arithmetic" + +# model — error helpers operate on the crate's own accumulated validation state. +[[not_untrusted]] +fn = "crates/openjd-model/src/error.rs::into_result" +reason = "converts already-accumulated ValidationErrors; &str is the crate's own model-name label" +[[not_untrusted]] +fn = "crates/openjd-model/src/error.rs::format" +reason = "formats already-constructed error state; no parsing of external input" +[[not_untrusted]] +fn = "crates/openjd-model/src/error.rs::path_field" +reason = "diagnostic path-element builder over the crate's own field names" +[[not_untrusted]] +fn = "crates/openjd-model/src/job/step_dependency_graph.rs::step_node" +reason = "lookup by a step name already validated during decode; graph is built from a validated template" +[[not_untrusted]] +fn = "crates/openjd-model/src/job/create_job/parameters.rs::new" +reason = "PathParameterOptions builder; the &str dirs are host-chosen filesystem paths (job template dir / cwd), not external input — same rationale as preprocess_job_parameters" + +# snapshots — hashing and caches. These operate on local filesystem paths and +# content the host chose to snapshot (collect side), or on cache-key strings the +# crate itself computes — not on manifest bytes received from an untrusted peer. +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash.rs::hash_data" +reason = "hashes host-provided local content being snapshotted, not externally-received data" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash.rs::hash_file" +reason = "hashes a local file path chosen by the host during collect" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash.rs::hash_file_chunked" +reason = "hashes a local file path chosen by the host during collect" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash_cache.rs::get" +reason = "local SQLite hash-cache lookup keyed by crate-computed path/hash strings" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash_cache.rs::put" +reason = "local SQLite hash-cache write keyed by crate-computed path/hash strings" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/hash_cache.rs::get_if_fresh" +reason = "local SQLite hash-cache lookup keyed by crate-computed path/hash strings" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/data_cache.rs::cache_key" +reason = "derives an S3 key from a crate-computed content hash, not external input" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/data_cache.rs::check_cache_exists" +reason = "S3 existence check keyed by a crate-computed content hash" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/data_cache.rs::record_in_check_cache" +reason = "records a crate-computed content hash in the local check cache" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/s3_check_cache.rs::get_entry" +reason = "local check-cache lookup keyed by a crate-computed content hash" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/s3_check_cache.rs::put_entry" +reason = "local check-cache write keyed by a crate-computed content hash" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/codec.rs::encode_v2025" +reason = "serializes a Manifest the crate already holds in memory; encode side, not decode" + +# snapshots — join/subtree variants over decoded manifests. subtree_manifest and +# the filter helpers are fuzzed via snapshot_ops; the join_* family and the +# remaining subtree_* variants operate on already-decoded, validated manifests +# and are exercised transitively there. They take &str subtree/base paths that +# originate from the host (the caller chooses what subtree to extract), not from +# the manifest bytes. +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_snapshot" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_snapshot_diff" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_snapshot_rel" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_snapshot_diff_rel" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_manifest" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/join.rs::join_manifest_rel" +reason = "operates on already-decoded manifests; &str base path is host-chosen, not manifest content" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_snapshot" +reason = "same traversal as subtree_manifest (fuzzed via snapshot_ops); &str subtree is host-chosen" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_snapshot_diff" +reason = "same traversal as subtree_manifest (fuzzed via snapshot_ops); &str subtree is host-chosen" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_rel_snapshot" +reason = "same traversal as subtree_manifest (fuzzed via snapshot_ops); &str subtree is host-chosen" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_rel_snapshot_diff" +reason = "same traversal as subtree_manifest (fuzzed via snapshot_ops); &str subtree is host-chosen" +[[not_untrusted]] +fn = "crates/openjd-snapshots/src/ops/subtree.rs::subtree_rel_manifest" +reason = "same traversal as subtree_manifest (fuzzed via snapshot_ops); &str subtree is host-chosen" + diff --git a/fuzz/fuzz_targets/copy_symbol_value.rs b/fuzz/fuzz_targets/copy_symbol_value.rs new file mode 100644 index 00000000..d60d2367 --- /dev/null +++ b/fuzz/fuzz_targets/copy_symbol_value.rs @@ -0,0 +1,57 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz `copy_symbol_value`. +//! +//! `copy_symbol_value(symbol, source, dest)` walks a dotted symbol name +//! (`Param.Foo.Bar`) into a source symbol table and copies the matching value +//! (and any nested subtable) into a destination table. It splits on `.` and +//! indexes into the parsed parts, so an adversarial symbol name — empty +//! segments, trailing dots, very deep nesting, names that collide with +//! existing entries — must not panic. It returns `()`, so the only observable +//! failure is a panic/abort. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_expr::format_string::copy_symbol_value; +use openjd_expr::SymbolTable; + +fn symtab_from_json(v: &serde_json::Value) -> SymbolTable { + let mut st = SymbolTable::new(); + if let Some(obj) = v.as_object() { + for (k, val) in obj { + let _ = match val { + serde_json::Value::String(s) => st.set(k, s.as_str()), + serde_json::Value::Bool(b) => st.set(k, *b), + serde_json::Value::Number(n) => match n.as_i64() { + Some(i) => st.set(k, i), + None => continue, + }, + _ => continue, + }; + } + } + st +} + +// First NUL splits a JSON object (the source table) from the symbol name. +fuzz_target!(|data: &[u8]| { + let (src, symbol) = match data.iter().position(|&b| b == 0) { + Some(i) => { + let source = std::str::from_utf8(&data[..i]) + .ok() + .and_then(|s| serde_json::from_str::(s).ok()) + .map(|v| symtab_from_json(&v)) + .unwrap_or_default(); + (source, &data[i + 1..]) + } + None => (SymbolTable::new(), data), + }; + let Ok(symbol) = std::str::from_utf8(symbol) else { + return; + }; + let mut dest = SymbolTable::new(); + copy_symbol_value(symbol, &src, &mut dest); +}); diff --git a/fuzz/fuzz_targets/expr_evaluate.rs b/fuzz/fuzz_targets/expr_evaluate.rs new file mode 100644 index 00000000..7036759c --- /dev/null +++ b/fuzz/fuzz_targets/expr_evaluate.rs @@ -0,0 +1,99 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the full parse + evaluate pipeline. +//! +//! This is the highest-value target: the historically-found bugs +//! (`attempt to (add|sub|mul|neg) with overflow` in `range_expr`, `arithmetic`, +//! `list`, `math`, `string`, `regex`; char-boundary panics in `path`; silent +//! saturation in numeric coercion) all live in the *evaluator*, reachable only +//! once an expression parses and then runs against a symbol table. +//! +//! Evaluation MUST always terminate with `Ok` or `Err` — never a panic or +//! abort. The evaluator enforces its own memory and operation-count budgets, +//! so a well-behaved malicious expression is rejected with an error rather than +//! hanging; this target confirms that guarantee holds for arbitrary input. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_expr::{ParsedExpression, PathFormat, SymbolTable}; + +/// Build a symbol table from a JSON object by calling `SymbolTable::set` for +/// each scalar entry. Dotted keys (e.g. `"Param.Foo"`) build nested scopes, +/// matching how job parameters and session variables are namespaced. Wiring +/// variables this way lets the fuzzer reach evaluator paths that depend on +/// runtime values — arithmetic on user-supplied integers, path operations on +/// user-supplied strings — which an empty environment would never exercise. +fn symtab_from_json(v: &serde_json::Value) -> SymbolTable { + let mut st = SymbolTable::new(); + if let Some(obj) = v.as_object() { + for (k, val) in obj { + // Only scalar values map cleanly onto ExprValue via `set`. Ignore + // set() conflicts (e.g. "A" and "A.B" both present) — a partial + // table is still a useful evaluation environment. + let _ = match val { + serde_json::Value::String(s) => st.set(k, s.as_str()), + serde_json::Value::Bool(b) => st.set(k, *b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + st.set(k, i) + } else { + continue; + } + } + _ => continue, + }; + } + } + st +} + +/// Split the fuzz input into an optional JSON symbol-table document and the +/// expression source. A NUL byte separates the two halves: everything before +/// the first NUL (if it parses as a JSON object) seeds the symbol table; +/// everything after is the expression. With no NUL, the whole input is the +/// expression evaluated against an empty table. +fn split_input(data: &[u8]) -> (SymbolTable, &[u8]) { + match data.iter().position(|&b| b == 0) { + Some(idx) => { + let (head, tail) = (&data[..idx], &data[idx + 1..]); + let symtab = std::str::from_utf8(head) + .ok() + .and_then(|s| serde_json::from_str::(s).ok()) + .map(|v| symtab_from_json(&v)) + .unwrap_or_default(); + (symtab, tail) + } + None => (SymbolTable::new(), data), + } +} + +fuzz_target!(|data: &[u8]| { + let (symtab, expr_bytes) = split_input(data); + + let Ok(expr) = std::str::from_utf8(expr_bytes) else { + return; + }; + + // Only expressions that parse can be evaluated. A parse error is an + // expected outcome, not a finding. + let Ok(parsed) = ParsedExpression::new(expr) else { + return; + }; + + // Evaluate under BOTH path formats. `path()` operations format and split + // paths differently on POSIX vs Windows (separator handling, drive-letter + // parsing, prefix stripping), and the Windows branch does byte-index work + // on the path string — so char-boundary safety on multibyte paths must + // hold in both. The default `evaluate()` only ever uses POSIX, leaving the + // Windows path code unfuzzed; drive both explicitly here. Errors are fine; + // the invariant under test is "no panic, no abort, always returns". + let _ = parsed + .with_path_format(PathFormat::Posix) + .evaluate(&[&symtab]); + let _ = parsed + .with_path_format(PathFormat::Windows) + .evaluate(&[&symtab]); +}); diff --git a/fuzz/fuzz_targets/expr_parse.rs b/fuzz/fuzz_targets/expr_parse.rs new file mode 100644 index 00000000..1311166e --- /dev/null +++ b/fuzz/fuzz_targets/expr_parse.rs @@ -0,0 +1,31 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the expression parser. +//! +//! `ParsedExpression::new` accepts arbitrary user-supplied expression text +//! (from job templates, `let` bindings, format strings). A malformed or +//! adversarial expression MUST surface as an `Err`, never a panic, an abort, +//! or a hang. The parser has its own input-length and depth caps; this target +//! exercises the paths below those caps where the ruff-based recursive-descent +//! parser and the structural depth walker actually run. + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // The parser operates on &str. Reject non-UTF-8 input cheaply rather than + // lossily transforming it — feeding it lossy data would mask real + // char-boundary behaviour behind U+FFFD replacements. + let Ok(s) = std::str::from_utf8(data) else { + return; + }; + + // `new` uses `ExprProfile::latest()` — the widest syntax surface (every + // extension enabled), which maximizes the grammar the fuzzer can reach. + // A returned error is a valid, expected outcome; only a panic/abort is a + // finding. + let _ = openjd_expr::ParsedExpression::new(s); +}); diff --git a/fuzz/fuzz_targets/expr_type_parse.rs b/fuzz/fuzz_targets/expr_type_parse.rs new file mode 100644 index 00000000..a96351be --- /dev/null +++ b/fuzz/fuzz_targets/expr_type_parse.rs @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the public type-string parser `ExprType::parse`. +//! +//! `ExprType::parse` turns strings like `list[int]`, `union[int, string]`, or +//! `unresolved[T]` into `ExprType` values. It is a public, recursive parser +//! over untrusted text (type annotations can originate from function-signature +//! DSL strings), so it must reject malformed input with `Err` and must not +//! recurse into a stack overflow on deeply nested type strings. + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let Ok(s) = std::str::from_utf8(data) else { + return; + }; + // `parse` has its own depth guard; a returned `Err(String)` is the expected + // outcome for malformed or too-deep input. Only a panic/abort is a finding. + let _ = openjd_expr::ExprType::parse(s); +}); diff --git a/fuzz/fuzz_targets/format_string.rs b/fuzz/fuzz_targets/format_string.rs new file mode 100644 index 00000000..26d4163c --- /dev/null +++ b/fuzz/fuzz_targets/format_string.rs @@ -0,0 +1,73 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz format-string parsing and resolution. +//! +//! Format strings (`"{{ Param.Foo }}/output"`) embed expressions in literal +//! text and are pervasive in job templates. Parsing splits literal vs. +//! expression segments; resolution evaluates each embedded expression and +//! concatenates. The contextual-keyword rewriting the format-string layer +//! performs was the source of the "string literal silently rewritten" +//! divergence, and any embedded expression can reach the same arithmetic / +//! path / coercion code as the standalone evaluator. Neither parse nor +//! resolve may panic on arbitrary input. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_expr::{FormatString, FormatStringOptions, SymbolTable}; + +/// Build a symbol table from a JSON object via `SymbolTable::set`, mapping +/// scalar values onto `ExprValue`. Dotted keys build nested scopes. See +/// `expr_evaluate.rs` for the rationale on wiring variables into the fuzzer. +fn symtab_from_json(v: &serde_json::Value) -> SymbolTable { + let mut st = SymbolTable::new(); + if let Some(obj) = v.as_object() { + for (k, val) in obj { + let _ = match val { + serde_json::Value::String(s) => st.set(k, s.as_str()), + serde_json::Value::Bool(b) => st.set(k, *b), + serde_json::Value::Number(n) => match n.as_i64() { + Some(i) => st.set(k, i), + None => continue, + }, + _ => continue, + }; + } + } + st +} + +fn split_input(data: &[u8]) -> (SymbolTable, &[u8]) { + match data.iter().position(|&b| b == 0) { + Some(idx) => { + let (head, tail) = (&data[..idx], &data[idx + 1..]); + let symtab = std::str::from_utf8(head) + .ok() + .and_then(|s| serde_json::from_str::(s).ok()) + .map(|v| symtab_from_json(&v)) + .unwrap_or_default(); + (symtab, tail) + } + None => (SymbolTable::new(), data), + } +} + +fuzz_target!(|data: &[u8]| { + let (symtab, fmt_bytes) = split_input(data); + + let Ok(fmt) = std::str::from_utf8(fmt_bytes) else { + return; + }; + + let Ok(parsed) = FormatString::new(fmt) else { + return; + }; + + // Resolve to a String. Default options use the POSIX path format and the + // default function library, matching template-context resolution. Errors + // are expected; a panic is a finding. + let opts = FormatStringOptions::new(); + let _ = parsed.resolve_string_with(&symtab, &opts); +}); diff --git a/fuzz/fuzz_targets/int_range_new.rs b/fuzz/fuzz_targets/int_range_new.rs new file mode 100644 index 00000000..05db278d --- /dev/null +++ b/fuzz/fuzz_targets/int_range_new.rs @@ -0,0 +1,41 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the public `IntRange::new` constructor with arbitrary `(start, end, +//! step)` triples. +//! +//! `RangeExpr::from_str` can only ever hand `IntRange::new` a step whose +//! magnitude came from parsing a decimal literal, so the text parser cannot +//! reach values like `i64::MIN`. But `IntRange` is a public type and `new` is +//! a public constructor callable directly with *any* `i64` — so the range +//! normalization arithmetic (including the `-step` negation on the descending +//! branch) must stay panic-free across the full `i64` domain, not just the +//! subset the text parser can produce. `new` MUST return `Ok` or `Err` for +//! every input — never panic or abort under the overflow-checked fuzz build. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_expr::range_expr::IntRange; + +// Take three i64s directly from the fuzzer via `arbitrary`. This gives the +// mutator full control over each field (including the i64 boundary values) far +// more efficiently than decoding raw bytes by hand. +fuzz_target!(|triple: (i64, i64, i64)| { + let (start, end, step) = triple; + if let Ok(range) = IntRange::new(start, end, step) { + // A successfully constructed range must also materialize without + // panicking: len/get/iter do their own index arithmetic over the + // normalized bounds. Bound the walk so a valid-but-enormous range + // doesn't stall the fuzzer. + let len = range.len(); + let _ = range.is_empty(); + for i in 0..len.min(256) { + let _ = range.get(i); + } + for v in range.iter().take(256) { + let _ = range.contains(v); + } + } +}); diff --git a/fuzz/fuzz_targets/model_create_job.rs b/fuzz/fuzz_targets/model_create_job.rs new file mode 100644 index 00000000..ec0e2cd4 --- /dev/null +++ b/fuzz/fuzz_targets/model_create_job.rs @@ -0,0 +1,102 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the full template → job instantiation pipeline. +//! +//! `model_decode` stops at `decode_job_template` (parse + validate). The two +//! documented model panics (finding #1 `json_to_expr_value`, finding #2 float +//! parameter-space construction) live one layer past that, in +//! `preprocess_job_parameters` + `create_job`: parameter-value coercion, +//! format-string evaluation, parameter-space iteration, and chunk-count +//! arithmetic. This target drives that pipeline exactly as `openjd run` does. +//! +//! Both stages MUST return `Ok` or `Err` for any decoded template and any +//! supplied parameter values — never panic, abort, or hang. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_expr::path_mapping::PathFormat; +use openjd_expr::ExprValue; +use openjd_model::template::parse::{decode_job_template, document_string_to_object, DocumentType}; +use openjd_model::types::{CallerLimits, ModelExtension}; +use openjd_model::{ + create_job, preprocess_job_parameters, JobParameterInputValues, PathParameterOptions, +}; + +/// Split the input at the first NUL: the head is an optional JSON object of +/// parameter name → value (fed to the job as user-supplied parameter values), +/// the tail is the template document text. Wiring parameter values in lets the +/// fuzzer reach coercion and format-string-resolution code that a parameterless +/// template never exercises. +fn split(data: &[u8]) -> (JobParameterInputValues, &[u8]) { + match data.iter().position(|&b| b == 0) { + Some(i) => { + let mut values = JobParameterInputValues::new(); + if let Some(obj) = std::str::from_utf8(&data[..i]) + .ok() + .and_then(|s| serde_json::from_str::(s).ok()) + .and_then(|v| v.as_object().cloned()) + { + for (k, v) in obj { + // CLI callers pass everything as a string and let + // preprocess coerce; mirror that for string values, and + // pass through ints/bools directly for typed coverage. + let ev = match v { + serde_json::Value::String(s) => ExprValue::String(s), + serde_json::Value::Bool(b) => ExprValue::Bool(b), + serde_json::Value::Number(n) => match n.as_i64() { + Some(x) => ExprValue::Int(x), + None => continue, + }, + _ => continue, + }; + values.insert(k, ev); + } + } + (values, &data[i + 1..]) + } + None => (JobParameterInputValues::new(), data), + } +} + +fuzz_target!(|data: &[u8]| { + let (input_values, doc_bytes) = split(data); + let Ok(doc) = std::str::from_utf8(doc_bytes) else { + return; + }; + + let limits = CallerLimits::default(); + let Ok(value) = document_string_to_object(doc, DocumentType::Yaml, &limits) else { + return; + }; + // Enable every recognized extension so the widest set of template features + // (and thus the most instantiation code) is reachable. `supported_extensions` + // is an allowlist: `None` / `Some(&[])` would reject any template declaring + // an `extensions:` list, narrowing this target to extension-free templates. + // Pass the full ModelExtension::ALL name list instead. + let all_extensions: Vec<&str> = ModelExtension::ALL.iter().map(|e| e.as_str()).collect(); + let Ok(template) = decode_job_template(value, Some(&all_extensions), &limits) else { + return; + }; + + // Absolute dirs + walk-up allowed so PATH-parameter handling doesn't reject + // the fuzzer's synthetic paths before the interesting code runs. + let path_options = PathParameterOptions { + job_template_dir: "/tmpl", + current_working_dir: "/cwd", + path_format: PathFormat::Posix, + allow_template_dir_walk_up: true, + allow_uri_path_values: true, + }; + + let Ok(param_values) = + preprocess_job_parameters(&template, &input_values, &[], &path_options) + else { + return; + }; + + let ctx = template.default_validation_context(); + let _ = create_job(&template, ¶m_values, &ctx); +}); diff --git a/fuzz/fuzz_targets/model_decode.rs b/fuzz/fuzz_targets/model_decode.rs new file mode 100644 index 00000000..c3e11dec --- /dev/null +++ b/fuzz/fuzz_targets/model_decode.rs @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz job/environment template decoding. +//! +//! `document_string_to_object` + `decode_job_template` / `decode_environment_template` +//! is exactly the path `openjd check` runs on an untrusted YAML/JSON template +//! file. It parses YAML (via serde-saphyr, with a depth budget), then validates +//! structure, extensions, format strings, and parameter spaces. This is where +//! the `json_to_expr_value` panic and float-parameter-space panic lived. Decode +//! MUST reject malformed templates with an `Err`, never panic or abort. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_model::template::parse::{ + decode_environment_template, decode_job_template, document_string_to_object, DocumentType, +}; +use openjd_model::types::CallerLimits; + +fuzz_target!(|data: &[u8]| { + let Ok(doc) = std::str::from_utf8(data) else { + return; + }; + + let limits = CallerLimits::default(); + + // Try both document types. YAML is the common case; JSON exercises the + // serde_json branch. Each returns a serde_json::Value on success. + for doc_type in [DocumentType::Yaml, DocumentType::Json] { + let Ok(value) = document_string_to_object(doc, doc_type, &limits) else { + continue; + }; + + // Feed the decoded value into both decoders. `None` for supported + // extensions means "no extensions"; the decoders still parse the + // structure and report unknown-extension / validation errors as `Err`. + // Clone because each decoder takes the value by move. + let _ = decode_job_template(value.clone(), None, &limits); + let _ = decode_environment_template(value, None); + } +}); diff --git a/fuzz/fuzz_targets/range_expr.rs b/fuzz/fuzz_targets/range_expr.rs new file mode 100644 index 00000000..ed6bf78b --- /dev/null +++ b/fuzz/fuzz_targets/range_expr.rs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz `RangeExpr` parsing and materialization. +//! +//! `RangeExpr` parses strings like `"1-10:2"` into (start, end, step) integer +//! ranges used by step parameter spaces. The documented crash +//! `RangeExpr::from_str("-9223372036854775807-9223372036854775807")` was an +//! `attempt to subtract with overflow` in the parser; range length/indexing +//! also does integer arithmetic that must not overflow. Parsing MUST return +//! `Err` on malformed or out-of-range input, and materializing a parsed range +//! MUST NOT panic. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use std::str::FromStr; + +fuzz_target!(|data: &[u8]| { + let Ok(s) = std::str::from_utf8(data) else { + return; + }; + + if let Ok(range) = openjd_expr::RangeExpr::from_str(s) { + // A successfully parsed range must report a consistent length and be + // safely indexable/iterable without overflow. `len()` and `get()` both + // do index arithmetic over i64 bounds. Bound the materialization so a + // legitimately huge (but valid) range doesn't turn the fuzzer into a + // multi-second loop; the arithmetic we care about is exercised by the + // first handful of elements and the length computation. + let len = range.len(); + let _ = range.is_empty(); + for i in 0..len.min(256) { + let _ = range.get(i as i64); + } + // Also drive the iterator adaptor a bounded distance. + for v in range.iter().take(256) { + let _ = range.contains(v); + } + } +}); diff --git a/fuzz/fuzz_targets/range_expr_slice.rs b/fuzz/fuzz_targets/range_expr_slice.rs new file mode 100644 index 00000000..047f54cb --- /dev/null +++ b/fuzz/fuzz_targets/range_expr_slice.rs @@ -0,0 +1,42 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz the public `RangeExpr::slice` constructor. +//! +//! `slice(start, stop, step)` is the structural twin of `IntRange::new`: a +//! public method that takes raw `i64` indices and does a lot of unchecked index +//! arithmetic (cumulative-length sums, ceil-division, per-sub-range stride +//! remapping). The evaluator only ever calls it with parser-derived, in-bounds +//! indices, so the adversarial `i64` domain — negative, `i64::MAX`, `i64::MIN`, +//! strides that overflow when multiplied — is not otherwise exercised. +//! +//! `slice` MUST return `Ok` or `Err` for any indices — never panic, abort, or +//! hang — and any returned `RangeExpr` MUST materialize without panicking. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use std::str::FromStr; + +// The fuzzer supplies the source range text plus the three slice indices. +// Splitting the input this way lets the mutator explore both the base range +// shape and the slice arguments independently. +fuzz_target!(|input: (&str, i64, i64, i64)| { + let (text, start, stop, step) = input; + + let Ok(range) = openjd_expr::RangeExpr::from_str(text) else { + return; + }; + + if let Ok(sliced) = range.slice(start, stop, step) { + // A returned slice must be safe to walk: len/get do index arithmetic + // over the remapped sub-ranges. Bound the walk so a valid-but-large + // result doesn't stall the fuzzer. + let len = sliced.len(); + let _ = sliced.is_empty(); + for i in 0..len.min(256) { + let _ = sliced.get(i as i64); + } + } +}); diff --git a/fuzz/fuzz_targets/snapshot_decode.rs b/fuzz/fuzz_targets/snapshot_decode.rs new file mode 100644 index 00000000..5068cf2c --- /dev/null +++ b/fuzz/fuzz_targets/snapshot_decode.rs @@ -0,0 +1,45 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz snapshot manifest decoding + validation. +//! +//! `decode_manifest` auto-detects the manifest format (v2023 / v2025 absolute +//! or relative, snapshot or diff) from an untrusted JSON string and +//! deserializes it into a `Manifest`. The `file_chunk_size_bytes` guard +//! (reject zero / invalid negative) and the `total_size` / path invariants are +//! enforced by `Manifest::validate()`. Decoding attacker-controlled manifest +//! JSON MUST NOT panic (e.g. a div-by-zero or overflow in chunk math), and +//! validating a decoded manifest MUST return `Err` on invariant violations +//! rather than aborting. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_snapshots::{decode_manifest, DecodedManifest}; + +fuzz_target!(|data: &[u8]| { + let Ok(json) = std::str::from_utf8(data) else { + return; + }; + + // decode_manifest returns the version/kind-appropriate decoded form, each + // of which is a `Manifest` type alias exposing `validate()`. A parse + // or schema error is an expected outcome; only a panic/abort is a finding. + let result = decode_manifest(json); + match result { + Ok(DecodedManifest::AbsSnapshot(m)) => { + let _ = m.validate(); + } + Ok(DecodedManifest::AbsSnapshotDiff(m)) => { + let _ = m.validate(); + } + Ok(DecodedManifest::Snapshot(m)) => { + let _ = m.validate(); + } + Ok(DecodedManifest::SnapshotDiff(m)) => { + let _ = m.validate(); + } + Err(_) => {} + } +}); diff --git a/fuzz/fuzz_targets/snapshot_ops.rs b/fuzz/fuzz_targets/snapshot_ops.rs new file mode 100644 index 00000000..d8919be7 --- /dev/null +++ b/fuzz/fuzz_targets/snapshot_ops.rs @@ -0,0 +1,62 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright by contributors to this project. +// SPDX-License-Identifier: (Apache-2.0 OR MIT) + +//! Fuzz snapshot manifest operations on attacker-controlled decoded manifests. +//! +//! `snapshot_decode` only exercises decode + `validate`. The manifest +//! *operations* — diffing, composing, partitioning, subtree extraction, +//! filtering — consume decoded manifests and do their own merging, size +//! accounting, and path arithmetic over that data. A malformed-but-decodable +//! manifest could drive those into a panic that `validate()` alone wouldn't +//! catch. This target decodes two absolute snapshots from the fuzz input and +//! runs the operations typed for them. +//! +//! Every operation MUST return `Ok`/`Err` (or a value) — never panic, abort, +//! or hang. + +#![no_main] + +use libfuzzer_sys::fuzz_target; +use openjd_snapshots::{ + compose_diffs, decode_manifest, diff_snapshots, filter_manifest, partition_manifest, + subtree_manifest, AbsManifest, DecodedManifest, DiffOptions, PartitionOptions, SymlinkPolicy, +}; + +// Two JSON manifest documents separated by a NUL, so the binary op (diff) gets +// two independently-mutated inputs. +fuzz_target!(|data: &[u8]| { + let (a_bytes, b_bytes) = match data.iter().position(|&b| b == 0) { + Some(i) => (&data[..i], &data[i + 1..]), + None => (data, &b""[..]), + }; + let (Ok(a_str), Ok(b_str)) = (std::str::from_utf8(a_bytes), std::str::from_utf8(b_bytes)) + else { + return; + }; + + // The ops below are typed for absolute full snapshots (`AbsSnapshot`). + // Other decoded shapes still exercised the decoder; here we keep only the + // variant the ops accept. + let decode_abs = |s: &str| match decode_manifest(s) { + Ok(DecodedManifest::AbsSnapshot(m)) => Some(m), + _ => None, + }; + + if let Some(a) = decode_abs(a_str) { + // Unary ops on a single decoded snapshot. + let _ = partition_manifest(&a, &PartitionOptions::default()); + let _ = filter_manifest(&a, &|_entry| true); + // subtree_manifest takes an `AbsManifest` wrapper enum. Cloning is + // fine — the point is to exercise the traversal/path arithmetic. + let _ = subtree_manifest(&AbsManifest::Snapshot(a.clone()), "some/dir", SymlinkPolicy::Preserve); + + if let Some(b) = decode_abs(b_str) { + // Binary op: diff two decoded snapshots, then compose the result + // back — a round-trip through the diff/compose arithmetic. + if let Ok(d) = diff_snapshots(&a, &b, &DiffOptions::default()) { + let _ = compose_diffs(&[&d]); + } + } + } +}); diff --git a/fuzz/seeds/copy_symbol_value/basic b/fuzz/seeds/copy_symbol_value/basic new file mode 100644 index 0000000000000000000000000000000000000000..3cb06dc71a4006a33c1081b2453c804c397e4251 GIT binary patch literal 46 icmbzU%++%P5~)g7raDl*ADF9Dp;XI&qyPX}2@b{p literal 0 HcmV?d00001 diff --git a/fuzz/seeds/copy_symbol_value/empty_symbol b/fuzz/seeds/copy_symbol_value/empty_symbol new file mode 100644 index 0000000000000000000000000000000000000000..84feee5c326b56aedef2dd3857eb6d83319a2707 GIT binary patch literal 3 Kcmb=fWdHyIZUFfJ literal 0 HcmV?d00001 diff --git a/fuzz/seeds/copy_symbol_value/nested b/fuzz/seeds/copy_symbol_value/nested new file mode 100644 index 0000000000000000000000000000000000000000..3a4c25ce3a26389cff05948c67063132271e2e30 GIT binary patch literal 17 Ucmb;M1& literal 0 HcmV?d00001 diff --git a/fuzz/seeds/copy_symbol_value/property b/fuzz/seeds/copy_symbol_value/property new file mode 100644 index 0000000000000000000000000000000000000000..716160ee4b047dfc9f964522d09b9224c53eaefe GIT binary patch literal 21 ccmb literal 0 HcmV?d00001 diff --git a/fuzz/seeds/expr_evaluate/mb_relto2 b/fuzz/seeds/expr_evaluate/mb_relto2 new file mode 100644 index 0000000000000000000000000000000000000000..d94ce332bba5347bfaa930ab0d1c7e0f9987d901 GIT binary patch literal 50 zcmbA-4_M literal 0 HcmV?d00001 diff --git a/fuzz/seeds/expr_evaluate/with_symtab_1 b/fuzz/seeds/expr_evaluate/with_symtab_1 new file mode 100644 index 0000000000000000000000000000000000000000..86c134fa8420ebdf97144257309f76df72260df7 GIT binary patch literal 37 hcmbzU%++%P5~)g7rnL-6d<889BL!^*LjdG43itp3 literal 0 HcmV?d00001 diff --git a/fuzz/seeds/expr_evaluate/with_symtab_2 b/fuzz/seeds/expr_evaluate/with_symtab_2 new file mode 100644 index 0000000000000000000000000000000000000000..632e35a9a8eb2b8fe97fb0cd133472004d97ae0e GIT binary patch literal 36 ocmbzU%++%P5~)g7rnL;!)e1;L3R((A3bnNW0e%dO literal 0 HcmV?d00001 diff --git a/fuzz/seeds/int_range_new/seed_0 b/fuzz/seeds/int_range_new/seed_0 new file mode 100644 index 0000000000000000000000000000000000000000..8f0456f7ee70ee4fd2a6d8c6ab0768c6af0855c6 GIT binary patch literal 24 PcmZQ&fB;q~4W$|Y0bl@& literal 0 HcmV?d00001 diff --git a/fuzz/seeds/int_range_new/seed_1 b/fuzz/seeds/int_range_new/seed_1 new file mode 100644 index 0000000000000000000000000000000000000000..da92b9e3146f9a3b4c393d50c383b4329e4d0d0b GIT binary patch literal 24 OcmZQzKmiR<= 8 else ('medium' if Param.Quality >= 5 else 'low'))}}" + OUTPUT_BASE: "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + TILE_AREA: "{{str(Param.TileSize * Param.TileSize)}}" + script: + actions: + onEnter: + command: "{{Param.OutputDir}}/setup.sh" + args: + - "--project" + - "{{Param.ProjectName}}" + - "--quality" + - "{{str(Param.Quality)}}" + onExit: + command: /usr/bin/cleanup + args: + - "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + +steps: + - name: Validate + script: + actions: + onRun: + command: python + args: + - "-c" + - "print('Validating {{Param.Shot}} frames={{Param.Frames}} quality={{str(Param.Quality)}} denoiser={{str(Param.UseDenoiser)}}')" + + - name: Render + dependencies: + - dependsOn: Validate + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: "{{Param.Frames}}" + script: + embeddedFiles: + - name: RenderScript + type: TEXT + filename: render_frame.py + runnable: true + data: | + import bpy + import os + + frame = {{Task.Param.Frame}} + output_dir = "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + quality = {{Param.Quality}} + tile_size = {{Param.TileSize}} + motion_blur = {{Param.MotionBlur}} + use_denoiser = {{Param.UseDenoiser}} + + bpy.context.scene.frame_set(frame) + bpy.context.scene.render.resolution_percentage = quality * 10 + bpy.context.scene.render.tile_x = tile_size + bpy.context.scene.render.tile_y = tile_size + + if use_denoiser: + bpy.context.scene.view_layers[0].cycles.use_denoising = True + + padded = str(frame).zfill(5) + bpy.context.scene.render.filepath = os.path.join(output_dir, f"frame_{padded}.exr") + bpy.ops.render.render(write_still=True) + print(f"Rendered frame {frame} to {output_dir}") + actions: + onRun: + command: blender + args: + - "--background" + - "{{Param.OutputDir}}/{{Param.ProjectName}}/scene.blend" + - "--python" + - "{{Task.File.RenderScript}}" + timeout: "{{str(max(300, Param.Quality * 120))}}" + + - name: Composite + dependencies: + - dependsOn: Render + script: + embeddedFiles: + - name: CompositeScript + type: TEXT + filename: composite.py + runnable: true + data: | + import os + import glob + + output_dir = "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + frames = sorted(glob.glob(os.path.join(output_dir, "frame_*.exr"))) + print(f"Compositing {len(frames)} frames from {output_dir}") + + quality_label = "{{('high' if Param.Quality >= 8 else 'standard')}}" + output_file = os.path.join(output_dir, f"{{Param.Shot}}_{quality_label}.mp4") + + cmd = f"ffmpeg -framerate 24 -i {output_dir}/frame_%05d.exr -c:v libx264 -pix_fmt yuv420p {output_file}" + os.system(cmd) + print(f"Output: {output_file}") + actions: + onRun: + command: python + args: + - "{{Task.File.CompositeScript}}" + + - name: Notify + dependencies: + - dependsOn: Composite + script: + actions: + onRun: + command: curl + args: + - "-X" + - "POST" + - "https://hooks.slack.com/render-complete" + - "-d" + - "{\"text\": \"{{Param.ProjectName}}/{{Param.Shot}} render complete. Quality: {{str(Param.Quality)}}/10, Denoiser: {{str(Param.UseDenoiser)}}\"}" diff --git a/fuzz/seeds/model_create_job/tpl_1 b/fuzz/seeds/model_create_job/tpl_1 new file mode 100644 index 00000000..144fbac5 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_1 @@ -0,0 +1,53 @@ +specificationVersion: jobtemplate-2023-09 +name: Job +parameterDefinitions: +- name: J + type: STRING +jobEnvironments: +- name: J1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('J1 Enter') + onExit: + command: python + args: + - -c + - print('J1 Exit') +steps: +- name: First + parameterSpace: + taskParameterDefinitions: + - name: Foo + type: INT + range: '1' + - name: Bar + type: STRING + range: + - Bar1 + - Bar2 + script: + actions: + onRun: + command: python + args: + - -c + - print('J={{Param.J}} Foo={{Task.Param.Foo}}. Bar={{Task.Param.Bar}}') +- name: Second + dependencies: + - dependsOn: First + parameterSpace: + taskParameterDefinitions: + - name: Fuz + type: INT + range: 1-2 + script: + actions: + onRun: + command: python + args: + - -c + - print('J={{Param.J}} Fuz={{Task.Param.Fuz}}.') \ No newline at end of file diff --git a/fuzz/seeds/model_create_job/tpl_10 b/fuzz/seeds/model_create_job/tpl_10 new file mode 100644 index 00000000..107625f4 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_10 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Bash Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: BashStep + bash: + script: | + echo "Hello from Bash!" diff --git a/fuzz/seeds/model_create_job/tpl_11 b/fuzz/seeds/model_create_job/tpl_11 new file mode 100644 index 00000000..d7ffaff7 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_11 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Cmd Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: CmdStep + cmd: + script: | + echo Hello from Cmd! diff --git a/fuzz/seeds/model_create_job/tpl_12 b/fuzz/seeds/model_create_job/tpl_12 new file mode 100644 index 00000000..66023e22 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_12 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine AUTO +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: AUTO + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_create_job/tpl_13 b/fuzz/seeds/model_create_job/tpl_13 new file mode 100644 index 00000000..87dc1f8f --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_13 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine CRLF +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: CRLF + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_create_job/tpl_14 b/fuzz/seeds/model_create_job/tpl_14 new file mode 100644 index 00000000..dcb7072b --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_14 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine LF +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: LF + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_create_job/tpl_15 b/fuzz/seeds/model_create_job/tpl_15 new file mode 100644 index 00000000..2a32adac --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_15 @@ -0,0 +1,11 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Extended Step Name +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: ThisIsAVeryLongStepNameThatExceedsTheSixtyFourCharacterLimitButIsAllowedWithFeatureBundle1Extension + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Long step name works!'"] diff --git a/fuzz/seeds/model_create_job/tpl_16 b/fuzz/seeds/model_create_job/tpl_16 new file mode 100644 index 00000000..f00b4f50 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_16 @@ -0,0 +1,18 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Notify Period +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: NotifyPeriod + type: INT + default: 2 +steps: + - name: TestStep + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Notify period works!'"] + cancelation: + mode: NOTIFY_THEN_TERMINATE + notifyPeriodInSeconds: "{{Param.NotifyPeriod}}" diff --git a/fuzz/seeds/model_create_job/tpl_17 b/fuzz/seeds/model_create_job/tpl_17 new file mode 100644 index 00000000..888819c6 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_17 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 PowerShell Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: PowerShellStep + powershell: + script: | + Write-Host "Hello from PowerShell!" diff --git a/fuzz/seeds/model_create_job/tpl_18 b/fuzz/seeds/model_create_job/tpl_18 new file mode 100644 index 00000000..400ff26a --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_18 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Python Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: PythonStep + python: + script: | + print("Hello from Python!") diff --git a/fuzz/seeds/model_create_job/tpl_19 b/fuzz/seeds/model_create_job/tpl_19 new file mode 100644 index 00000000..3e21a31f --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_19 @@ -0,0 +1,16 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Format String Timeout +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: Timeout + type: INT + default: 5 +steps: + - name: TimeoutStep + script: + actions: + onRun: + command: bash + args: ["-c", "echo Running with timeout {{Param.Timeout}}s; sleep 1"] + timeout: "{{Param.Timeout}}" diff --git a/fuzz/seeds/model_create_job/tpl_2 b/fuzz/seeds/model_create_job/tpl_2 new file mode 100644 index 00000000..706f0a0f --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_2 @@ -0,0 +1,51 @@ +specificationVersion: "jobtemplate-2023-09" +name: Job +parameterDefinitions: + - name: J + type: STRING +jobEnvironments: + - name: J1 + script: + actions: + onEnter: + command: python + args: ["-c", "print('J1 Enter')"] + onExit: + command: python + args: ["-c", "print('J1 Exit')"] + - name: J2 + script: + actions: + onEnter: + command: python + args: ["-c", "print('J2 Enter')"] + onExit: + command: python + args: ["-c", "print('J2 Exit')"] +steps: + - name: First + parameterSpace: + taskParameterDefinitions: + - name: Foo + type: INT + range: "1" + - name: Bar + type: STRING + range: ["Bar1", "Bar2"] + stepEnvironments: + - name: FirstS, + script: + actions: + onEnter: + command: python + args: ["-c", "print('FirstS Enter')"] + onExit: + command: python + args: ["-c", "print('FirstS Exit')"] + script: + actions: + onRun: + command: python + args: + - "-c" + - "print('J={{Param.J}} Foo={{Task.Param.Foo}}. Bar={{Task.Param.Bar}}')" diff --git a/fuzz/seeds/model_create_job/tpl_20 b/fuzz/seeds/model_create_job/tpl_20 new file mode 100644 index 00000000..f0f5e2f5 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_20 @@ -0,0 +1,23 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "TimeoutTest", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "Timeout", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": [ + "-c", + # Obfuscate "EXIT_NORMAL" so it doesn't appear in the log when Windows prints the command that's run to the log. + "import time,sys; print('SLEEP'); sys.stdout.flush(); time.sleep(5); print(chr(69)+'XIT_NORMAL')", + ], + "timeout": 2, + } + } + }, + } + ], + } \ No newline at end of file diff --git a/fuzz/seeds/model_create_job/tpl_21 b/fuzz/seeds/model_create_job/tpl_21 new file mode 100644 index 00000000..7aab3e5f --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_21 @@ -0,0 +1,131 @@ +# Catch-all sample template with different cases per Step +specificationVersion: jobtemplate-2023-09 +name: my-job +parameterDefinitions: +- name: Message + type: STRING + default: Hello, world! +jobEnvironments: +- name: rootEnv + variables: + rootVar: rootVal +steps: +# VALID STEPS +# Basic step; uses Job parameters and has an environment +- name: NormalStep + script: + actions: + onRun: + command: python + args: + - -c + - print('{{Param.Message}}') + stepEnvironments: + - name: env1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnteringEnv') +# Step that will wait for one minute before completing its Task +- name: LongCommand + script: + actions: + onRun: + command: sleep + args: + - '60' +# Step with the bare minimum information, i.e., no Task parameters, environments, or dependencies +- name: BareStep + script: + actions: + onRun: + command: python + args: + - -c + - print('zzz') +# Step with a direct dependency on a previous Step +- name: DependentStep + script: + actions: + onRun: + command: python + args: + - -c + - print('I am dependent!') + dependencies: + - dependsOn: BareStep +# Step with Task parameters +- name: TaskParamStep + parameterSpace: + taskParameterDefinitions: + - name: TaskNumber + type: INT + range: + - 1 + - 2 + - 3 + - name: TaskMessage + type: STRING + range: + - Hi! + - Bye! + - One=Two + script: + actions: + onRun: + command: python + args: + - -c + - print('{{Task.Param.TaskNumber}}.{{Task.Param.TaskMessage}}') +# Step with a transitive dependency and a direct dependency +- name: ExtraDependentStep + script: + actions: + onRun: + command: python + args: + - -c + - print('I am extra dependent!') + dependencies: + - dependsOn: DependentStep + - dependsOn: TaskParamStep +# Step with dependencies and Task parameters +- name: DependentParamStep + parameterSpace: + taskParameterDefinitions: + - name: Adjective + type: STRING + range: + - really + - very + - super + script: + actions: + onRun: + command: python + args: + - -c + - print('I am {{Task.Param.Adjective}} dependent!') + dependencies: + - dependsOn: TaskParamStep +# Step whose dependency has a step environment +- name: StepDepHasStepEnv + script: + actions: + onRun: + command: python + args: + - -c + - print('I have a dependency with a step environment!') + dependencies: + - dependsOn: NormalStep +# ERROR STEPS +# Step with a non-existent command that will throw an error when run +- name: BadCommand + script: + actions: + onRun: + command: aaaaaaaaaa diff --git a/fuzz/seeds/model_create_job/tpl_22 b/fuzz/seeds/model_create_job/tpl_22 new file mode 100644 index 00000000..1dde9252 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_22 @@ -0,0 +1,52 @@ +specificationVersion: "jobtemplate-2023-09" +extensions: + - REDACTED_ENV_VARS +name: Test Redacted Env +description: Test redacted environment variables + +jobEnvironments: + - name: RedactedEnv + script: + actions: + onEnter: + command: python + args: ["{{Env.File.Enter}}"] + onExit: + command: python + args: ["{{Env.File.Exit}}"] + embeddedFiles: + - name: Enter + type: TEXT + data: | + print("Setting redacted vars..") + print(f"openjd_redacted_env: SECRETVAR=SECRETVAL") + print(f"openjd_redacted_env: KEYSPACE =SECRETVAL") + print(f"openjd_redacted_env: VALSPACE= SPACEVAL") + print(f'openjd_redacted_env: "MULTILINE=first_line\\nsecond_line\\nthird_line"') + - name: Exit + type: TEXT + data: | + import os + print(f"SECRETVAR is {os.environ.get('SECRETVAR')}") + print(f"KEYSPACE is {os.environ.get('KEYSPACE')}") + print(f"VALSPACE is {os.environ.get('VALSPACE')}") + print(f"MULTILINE is {os.environ.get('VALSPACE')} END") + print("first_line") + print("second_line") + print("third_line") +steps: + - name: CheckVars + script: + actions: + onRun: + command: python + args: ["{{Task.File.Run}}"] + embeddedFiles: + - name: Run + type: TEXT + data: | + import os + print(f"SECRETVAR is {os.environ.get('SECRETVAR')}") + print(f"KEYSPACE is {os.environ.get('KEYSPACE')}") + print(f"VALSPACE is {os.environ.get('VALSPACE')}") + print(f"MULTILINE is {os.environ.get('VALSPACE')} END") diff --git a/fuzz/seeds/model_create_job/tpl_23 b/fuzz/seeds/model_create_job/tpl_23 new file mode 100644 index 00000000..72e062aa --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_23 @@ -0,0 +1,28 @@ +specificationVersion: jobtemplate-2023-09 +name: ReverseDeps +steps: +- name: StepC + dependencies: + - dependsOn: StepB + script: + actions: + onRun: + command: echo + args: + - "Running StepC" +- name: StepB + dependencies: + - dependsOn: StepA + script: + actions: + onRun: + command: echo + args: + - "Running StepB" +- name: StepA + script: + actions: + onRun: + command: echo + args: + - "Running StepA" diff --git a/fuzz/seeds/model_create_job/tpl_24 b/fuzz/seeds/model_create_job/tpl_24 new file mode 100644 index 00000000..73fbc15e --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_24 @@ -0,0 +1,18 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "Test", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "SimpleStep", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": ["-c", "import sys; print('DoTask'); sys.exit(1)"] + } + } + } + } + ] +} diff --git a/fuzz/seeds/model_create_job/tpl_25 b/fuzz/seeds/model_create_job/tpl_25 new file mode 100644 index 00000000..9f7875a6 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_25 @@ -0,0 +1,18 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "Test", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "SimpleStep", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": ["-c", "print('DoTask {{Param.J}}')"], + } + } + }, + } + ], +} \ No newline at end of file diff --git a/fuzz/seeds/model_create_job/tpl_26 b/fuzz/seeds/model_create_job/tpl_26 new file mode 100644 index 00000000..d8909286 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_26 @@ -0,0 +1,47 @@ +specificationVersion: jobtemplate-2023-09 +name: Step Let In Step Env Test +extensions: + - EXPR +parameterDefinitions: + - name: Base + type: INT + default: 7 +steps: + - name: TestStep + let: + - val = Param.Base * 3 + - label = "item_" + string(val) + stepEnvironments: + - name: VarEnv + variables: + MY_VAL: "{{ val }}" + MY_LABEL: "{{ label }}" + - name: ScriptEnv + script: + actions: + onEnter: + command: python + args: + - -c + - "print('ENTER_VAL:{{val}}')\nprint('ENTER_LABEL:{{label}}')" + onExit: + command: python + args: + - -c + - "print('EXIT_VAL:{{val}}')\nprint('EXIT_LABEL:{{label}}')" + script: + actions: + onRun: + command: python + args: + - -c + - | + import os + print('ENV_VAL:' + os.environ.get('MY_VAL', '')) + print('ENV_LABEL:' + os.environ.get('MY_LABEL', '')) + print('TASK_VAL:{{val}}') + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: [1] diff --git a/fuzz/seeds/model_create_job/tpl_27 b/fuzz/seeds/model_create_job/tpl_27 new file mode 100644 index 00000000..3a063496 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_27 @@ -0,0 +1,24 @@ +# RFC 0008 "Lifecycle and cleanup guarantees": a failed onWrapEnvEnter is an +# inner onEnter failure, but the inner env's onWrapEnvExit and the wrapping +# environment's own onExit must still run before the session ends. +specificationVersion: environment-2023-09 +extensions: [WRAP_ACTIONS, EXPR] +environment: + name: WrapEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('WrapEnv Own Enter')"] + onWrapEnvEnter: + command: python + args: ["-c", "import sys; print('Wrap Enter Failing'); sys.exit(7)"] + onWrapTaskRun: + command: python + args: ["-c", "print('Wrap Task Ran')"] + onWrapEnvExit: + command: python + args: ["-c", "print('Wrap Exit Ran for {{WrappedEnv.Name}}')"] + onExit: + command: python + args: ["-c", "print('WrapEnv Own Exit')"] diff --git a/fuzz/seeds/model_create_job/tpl_28 b/fuzz/seeds/model_create_job/tpl_28 new file mode 100644 index 00000000..071674c9 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_28 @@ -0,0 +1,18 @@ +# RFC 0008: a second wrap-defining environment template. Supplying this +# alongside another wrap env template must be rejected by the session's +# single-layer rule before the second environment is entered. +specificationVersion: environment-2023-09 +extensions: [WRAP_ACTIONS, EXPR] +environment: + name: WrapB + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print('WrapB Enter')"] + onWrapTaskRun: + command: python + args: ["-c", "print('WrapB Task')"] + onWrapEnvExit: + command: python + args: ["-c", "print('WrapB Exit')"] diff --git a/fuzz/seeds/model_create_job/tpl_29 b/fuzz/seeds/model_create_job/tpl_29 new file mode 100644 index 00000000..497ef57c --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_29 @@ -0,0 +1,23 @@ +# RFC 0008: a wrap environment template parameterized via its own +# parameterDefinitions. The hooks reference {{Param.WrapPrefix}}, which must +# resolve from the environment's frozen resolved_symtab even though the +# wrapped step's own symbol table knows nothing about it (audit finding F11). +specificationVersion: environment-2023-09 +extensions: [WRAP_ACTIONS, EXPR] +parameterDefinitions: + - name: WrapPrefix + type: STRING + default: FROM_ENV_PARAM +environment: + name: WrapParam + script: + actions: + onWrapEnvEnter: + command: python + args: ["-c", "print('WrapParam Enter {{Param.WrapPrefix}}')"] + onWrapTaskRun: + command: python + args: ["-c", "print('WrapParam Task {{Param.WrapPrefix}}')"] + onWrapEnvExit: + command: python + args: ["-c", "print('WrapParam Exit {{Param.WrapPrefix}}')"] diff --git a/fuzz/seeds/model_create_job/tpl_3 b/fuzz/seeds/model_create_job/tpl_3 new file mode 100644 index 00000000..d3825465 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_3 @@ -0,0 +1,79 @@ +specificationVersion: 'jobtemplate-2023-09' +extensions: + - TASK_CHUNKING +name: Chunked Job + +parameterDefinitions: +- name: Items + type: STRING + default: 1-40 +- name: ChunkSize + type: INT + default: 10 +- name: TargetRuntime + type: INT + default: 0 +- name: TaskSleepTime + type: FLOAT + default: 0.01 + +steps: +- name: Chunked Step + + parameterSpace: + taskParameterDefinitions: + - name: Item + type: CHUNK[INT] + range: "{{Param.Items}}" + chunks: + defaultTaskCount: "{{Param.ChunkSize}}" + targetRuntimeSeconds: "{{Param.TargetRuntime}}" + rangeConstraint: NONCONTIGUOUS + + script: + actions: + onRun: + command: bash + args: ['{{Task.File.Run}}'] + embeddedFiles: + - name: GetSleepTime + type: TEXT + filename: get_sleep_time.py + data: | + """ + Converts an Open Job Description range expression into a sleep time according to the job parameters. + * https://github.com/OpenJobDescription/openjd-specifications/wiki/2023-09-Template-Schemas#34111-intrangeexpr + """ + + import sys, re + + task_sleep_time = float(sys.argv[2]) + + def range_expr_to_list(range_expr): + # Regex that matches "", "-", or "-:" + int_pat = r"\s*(-?[0-9]+)\s*" + part_re = re.compile(f"^{int_pat}(?:-{int_pat}(?::{int_pat})?)?$") + result = [] + for part in range_expr.split(","): + if m := part_re.match(part): + start, end, step = m.groups() + if step is not None: + # Linear sequence "3-7:2" means the values [3, 5, 7]. + result.extend(range(int(start), int(end) + (int(step)//abs(int(step))), int(step))) + elif end is not None: + # Interval "3-6" means the values [3, 4, 5, 6]. + result.extend(range(int(start), int(end) + 1)) + else: + # Integer "3" means the values [3]. + result.append(int(start)) + else: + raise ValueError(f"Invalid frame range expression: {range_expr}") + return result + + print(task_sleep_time * len(range_expr_to_list(sys.argv[1]))) + - name: Run + type: TEXT + data: | + set -xeuo pipefail + + sleep "$(python '{{Task.File.GetSleepTime}}' '{{Task.Param.Item}}' '{{Param.TaskSleepTime}}')" diff --git a/fuzz/seeds/model_create_job/tpl_30 b/fuzz/seeds/model_create_job/tpl_30 new file mode 100644 index 00000000..246f8e18 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_30 @@ -0,0 +1,21 @@ +# Companion to wrap_env_failing_enter.yaml: a job with one inner environment +# whose onEnter gets intercepted (and failed) by the wrap layer. +specificationVersion: "jobtemplate-2023-09" +name: WrapJobWithInnerEnv +jobEnvironments: + - name: InnerEnv + script: + actions: + onEnter: + command: python + args: ["-c", "print('Inner Enter Body')"] + onExit: + command: python + args: ["-c", "print('Inner Exit Body')"] +steps: + - name: OnlyStep + script: + actions: + onRun: + command: python + args: ["-c", "print('TaskBody Ran')"] diff --git a/fuzz/seeds/model_create_job/tpl_31 b/fuzz/seeds/model_create_job/tpl_31 new file mode 100644 index 00000000..167dc213 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_31 @@ -0,0 +1,9 @@ +specificationVersion: "jobtemplate-2023-09" +name: WrapSimpleJob +steps: + - name: OnlyStep + script: + actions: + onRun: + command: python + args: ["-c", "print('TaskBody Ran')"] diff --git a/fuzz/seeds/model_create_job/tpl_4 b/fuzz/seeds/model_create_job/tpl_4 new file mode 100644 index 00000000..94db7031 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_4 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: Env1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('Env1 Enter') + onExit: + command: python + args: + - -c + - print('Env1 Exit') diff --git a/fuzz/seeds/model_create_job/tpl_5 b/fuzz/seeds/model_create_job/tpl_5 new file mode 100644 index 00000000..975a40a7 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_5 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: Env2 + script: + actions: + onEnter: + command: python + args: + - -c + - print('Env2 Enter') + onExit: + command: python + args: + - -c + - print('Env2 Exit') diff --git a/fuzz/seeds/model_create_job/tpl_6 b/fuzz/seeds/model_create_job/tpl_6 new file mode 100644 index 00000000..e233a202 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_6 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: EnvEnterFail + script: + actions: + onEnter: + command: python + args: + - -c + - import sys; print('EnvEnterFail Enter'); sys.exit(1) + onExit: + command: python + args: + - -c + - print('EnvEnterFail Exit') diff --git a/fuzz/seeds/model_create_job/tpl_7 b/fuzz/seeds/model_create_job/tpl_7 new file mode 100644 index 00000000..47bc416a --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_7 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: EnvExitFail + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnvExitFail Enter') + onExit: + command: python + args: + - -c + - import sys; print('EnvExitFail Exit'); sys.exit(1) diff --git a/fuzz/seeds/model_create_job/tpl_8 b/fuzz/seeds/model_create_job/tpl_8 new file mode 100644 index 00000000..2214c265 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_8 @@ -0,0 +1,19 @@ +specificationVersion: environment-2023-09 +parameterDefinitions: + - name: EnvParam + type: STRING + default: DefaultForEnvParam +environment: + name: EnvWithParam + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnvWithParam Enter {{Param.EnvParam}}') + onExit: + command: python + args: + - -c + - print('EnvWithParam Exit {{Param.EnvParam}}') diff --git a/fuzz/seeds/model_create_job/tpl_9 b/fuzz/seeds/model_create_job/tpl_9 new file mode 100644 index 00000000..2625c8d1 --- /dev/null +++ b/fuzz/seeds/model_create_job/tpl_9 @@ -0,0 +1,23 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Amount Min Max +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: CpuMin + type: INT + default: 1 + - name: CpuMax + type: INT + default: 4 +steps: + - name: TestStep + hostRequirements: + amounts: + - name: amount.worker.vcpu + min: "{{Param.CpuMin}}" + max: "{{Param.CpuMax}}" + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Amount min/max works!'"] diff --git a/fuzz/seeds/model_create_job/with_params_0 b/fuzz/seeds/model_create_job/with_params_0 new file mode 100644 index 0000000000000000000000000000000000000000..f7dee5cc303f7e29edc0f48a04cf4498fb87b4d4 GIT binary patch literal 4707 zcmb_gQIFa<5WZ)Ag%#-pTQz~D-KvMQC$U}H?r9GwuvJgDrzj)_+$K)WcD7J1|NX{x z91;Sh=q?Wnv1dHvnfbmMc5A(5UWB>Tvo7t+j+Ony6YP@(@jXFe`Vlh@A3gLro(mi$ zp(n82?Q~z*onIOh6nn_pZpR*+;fz`Q8T(=gt663UJFd+k-XPH z$VP)t<4Gft`dDCgix-3vX*l23CQ2uR5<+5=fZ^$EayNXZ76QER(oldi6wlMpv((vI zSz11&#aA7;dr3(L<&>I?SVe7=iX;`cgq1YM{n>le?O@^#hVSl%gC2q{S(!=}q48w&+u+UYSQ*RI$mhdD z|Nd_FhkM_@8Qd%3R&zy`t0p1~q{~0g9QySdy+%0X7`2)a4oDgu7P`_x%`pDjytt6q z(56NYv+={sz3ETE*#cZeWo230V5$U*(yBq4K5ce)KXCh#L0_H41#4+xSC%WqmD(w1?Kz=sKg1r*OHE1~xrA}` z{Jb@|z)7XT0oNn|;#H|QnU+aP4<;qqh*eDUbRPBF_G?i~7@?xo%)KCViOl76siB4H zAlGJ9b7z*JJPojMDC{W68K$ydVjf6M^TKL?$A-)_)YTONoJbf`pX`8c!;x~8kDy^y zf>tz`Y2x|VghzP%i$cSJ)}u087*A*FV6|_|!84@wt?!3qfg9keDG3u|5JTd`C8t>fmzbsjt z(xBD_47;C3tgdQhvvvyfcGZSz8##YSLUeQ>H2v-7BI&ju5r$Y4vrOczTQ54D=X)a< z)PA?4XDTQY!>Vs$U^Wh!P^ELSfoLz3>{;Ypm^qifVb;+8{de*PmX_f%PfqE}FrHU& zv#Tqp`~QiQRoQZhMQ9^9Xi2M%{L?xLtQ=R*G@vdH!^6Z?4It3-G|u@&md_vX5l8z_ zse~b?Mj)6u4)uk9V2W>Y0Yk0Ff@Wn_l(EgjC%NPMykWqj2PSu0@g#Gi!Ok8yG*M7MMSr1r7h+-YG`*3{Lv7akNYerU3Ac86ac^-9u>H3qACl zgm!U~3V9XXXI#NgSy)QiVNU&2qq^~EI;$l>eu|hObEo zV`@#$KETwEmYgmgtsZ(TF#WyXib!`W`C~^JaZo-+L7ESa?(IvUT=Nfts!(b20k}L` HP+|W9= 8 else ('medium' if Param.Quality >= 5 else 'low'))}}" + OUTPUT_BASE: "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + TILE_AREA: "{{str(Param.TileSize * Param.TileSize)}}" + script: + actions: + onEnter: + command: "{{Param.OutputDir}}/setup.sh" + args: + - "--project" + - "{{Param.ProjectName}}" + - "--quality" + - "{{str(Param.Quality)}}" + onExit: + command: /usr/bin/cleanup + args: + - "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + +steps: + - name: Validate + script: + actions: + onRun: + command: python + args: + - "-c" + - "print('Validating {{Param.Shot}} frames={{Param.Frames}} quality={{str(Param.Quality)}} denoiser={{str(Param.UseDenoiser)}}')" + + - name: Render + dependencies: + - dependsOn: Validate + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: "{{Param.Frames}}" + script: + embeddedFiles: + - name: RenderScript + type: TEXT + filename: render_frame.py + runnable: true + data: | + import bpy + import os + + frame = {{Task.Param.Frame}} + output_dir = "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + quality = {{Param.Quality}} + tile_size = {{Param.TileSize}} + motion_blur = {{Param.MotionBlur}} + use_denoiser = {{Param.UseDenoiser}} + + bpy.context.scene.frame_set(frame) + bpy.context.scene.render.resolution_percentage = quality * 10 + bpy.context.scene.render.tile_x = tile_size + bpy.context.scene.render.tile_y = tile_size + + if use_denoiser: + bpy.context.scene.view_layers[0].cycles.use_denoising = True + + padded = str(frame).zfill(5) + bpy.context.scene.render.filepath = os.path.join(output_dir, f"frame_{padded}.exr") + bpy.ops.render.render(write_still=True) + print(f"Rendered frame {frame} to {output_dir}") + actions: + onRun: + command: blender + args: + - "--background" + - "{{Param.OutputDir}}/{{Param.ProjectName}}/scene.blend" + - "--python" + - "{{Task.File.RenderScript}}" + timeout: "{{str(max(300, Param.Quality * 120))}}" + + - name: Composite + dependencies: + - dependsOn: Render + script: + embeddedFiles: + - name: CompositeScript + type: TEXT + filename: composite.py + runnable: true + data: | + import os + import glob + + output_dir = "{{Param.OutputDir}}/{{Param.ProjectName}}/{{Param.Shot}}" + frames = sorted(glob.glob(os.path.join(output_dir, "frame_*.exr"))) + print(f"Compositing {len(frames)} frames from {output_dir}") + + quality_label = "{{('high' if Param.Quality >= 8 else 'standard')}}" + output_file = os.path.join(output_dir, f"{{Param.Shot}}_{quality_label}.mp4") + + cmd = f"ffmpeg -framerate 24 -i {output_dir}/frame_%05d.exr -c:v libx264 -pix_fmt yuv420p {output_file}" + os.system(cmd) + print(f"Output: {output_file}") + actions: + onRun: + command: python + args: + - "{{Task.File.CompositeScript}}" + + - name: Notify + dependencies: + - dependsOn: Composite + script: + actions: + onRun: + command: curl + args: + - "-X" + - "POST" + - "https://hooks.slack.com/render-complete" + - "-d" + - "{\"text\": \"{{Param.ProjectName}}/{{Param.Shot}} render complete. Quality: {{str(Param.Quality)}}/10, Denoiser: {{str(Param.UseDenoiser)}}\"}" diff --git a/fuzz/seeds/model_decode/tpl_1 b/fuzz/seeds/model_decode/tpl_1 new file mode 100644 index 00000000..144fbac5 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_1 @@ -0,0 +1,53 @@ +specificationVersion: jobtemplate-2023-09 +name: Job +parameterDefinitions: +- name: J + type: STRING +jobEnvironments: +- name: J1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('J1 Enter') + onExit: + command: python + args: + - -c + - print('J1 Exit') +steps: +- name: First + parameterSpace: + taskParameterDefinitions: + - name: Foo + type: INT + range: '1' + - name: Bar + type: STRING + range: + - Bar1 + - Bar2 + script: + actions: + onRun: + command: python + args: + - -c + - print('J={{Param.J}} Foo={{Task.Param.Foo}}. Bar={{Task.Param.Bar}}') +- name: Second + dependencies: + - dependsOn: First + parameterSpace: + taskParameterDefinitions: + - name: Fuz + type: INT + range: 1-2 + script: + actions: + onRun: + command: python + args: + - -c + - print('J={{Param.J}} Fuz={{Task.Param.Fuz}}.') \ No newline at end of file diff --git a/fuzz/seeds/model_decode/tpl_10 b/fuzz/seeds/model_decode/tpl_10 new file mode 100644 index 00000000..107625f4 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_10 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Bash Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: BashStep + bash: + script: | + echo "Hello from Bash!" diff --git a/fuzz/seeds/model_decode/tpl_11 b/fuzz/seeds/model_decode/tpl_11 new file mode 100644 index 00000000..d7ffaff7 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_11 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Cmd Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: CmdStep + cmd: + script: | + echo Hello from Cmd! diff --git a/fuzz/seeds/model_decode/tpl_12 b/fuzz/seeds/model_decode/tpl_12 new file mode 100644 index 00000000..66023e22 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_12 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine AUTO +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: AUTO + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_decode/tpl_13 b/fuzz/seeds/model_decode/tpl_13 new file mode 100644 index 00000000..87dc1f8f --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_13 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine CRLF +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: CRLF + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_decode/tpl_14 b/fuzz/seeds/model_decode/tpl_14 new file mode 100644 index 00000000..dcb7072b --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_14 @@ -0,0 +1,17 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 EndOfLine LF +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: EOLStep + script: + embeddedFiles: + - name: TestFile + type: TEXT + filename: test_eol.txt + data: "line1\nline2\nline3" + endOfLine: LF + actions: + onRun: + command: bash + args: ["-c", "cat '{{Task.File.TestFile}}' | xxd"] diff --git a/fuzz/seeds/model_decode/tpl_15 b/fuzz/seeds/model_decode/tpl_15 new file mode 100644 index 00000000..2a32adac --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_15 @@ -0,0 +1,11 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Extended Step Name +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: ThisIsAVeryLongStepNameThatExceedsTheSixtyFourCharacterLimitButIsAllowedWithFeatureBundle1Extension + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Long step name works!'"] diff --git a/fuzz/seeds/model_decode/tpl_16 b/fuzz/seeds/model_decode/tpl_16 new file mode 100644 index 00000000..f00b4f50 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_16 @@ -0,0 +1,18 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Notify Period +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: NotifyPeriod + type: INT + default: 2 +steps: + - name: TestStep + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Notify period works!'"] + cancelation: + mode: NOTIFY_THEN_TERMINATE + notifyPeriodInSeconds: "{{Param.NotifyPeriod}}" diff --git a/fuzz/seeds/model_decode/tpl_17 b/fuzz/seeds/model_decode/tpl_17 new file mode 100644 index 00000000..888819c6 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_17 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 PowerShell Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: PowerShellStep + powershell: + script: | + Write-Host "Hello from PowerShell!" diff --git a/fuzz/seeds/model_decode/tpl_18 b/fuzz/seeds/model_decode/tpl_18 new file mode 100644 index 00000000..400ff26a --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_18 @@ -0,0 +1,9 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Python Syntax Sugar +extensions: + - FEATURE_BUNDLE_1 +steps: + - name: PythonStep + python: + script: | + print("Hello from Python!") diff --git a/fuzz/seeds/model_decode/tpl_19 b/fuzz/seeds/model_decode/tpl_19 new file mode 100644 index 00000000..3e21a31f --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_19 @@ -0,0 +1,16 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Format String Timeout +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: Timeout + type: INT + default: 5 +steps: + - name: TimeoutStep + script: + actions: + onRun: + command: bash + args: ["-c", "echo Running with timeout {{Param.Timeout}}s; sleep 1"] + timeout: "{{Param.Timeout}}" diff --git a/fuzz/seeds/model_decode/tpl_2 b/fuzz/seeds/model_decode/tpl_2 new file mode 100644 index 00000000..706f0a0f --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_2 @@ -0,0 +1,51 @@ +specificationVersion: "jobtemplate-2023-09" +name: Job +parameterDefinitions: + - name: J + type: STRING +jobEnvironments: + - name: J1 + script: + actions: + onEnter: + command: python + args: ["-c", "print('J1 Enter')"] + onExit: + command: python + args: ["-c", "print('J1 Exit')"] + - name: J2 + script: + actions: + onEnter: + command: python + args: ["-c", "print('J2 Enter')"] + onExit: + command: python + args: ["-c", "print('J2 Exit')"] +steps: + - name: First + parameterSpace: + taskParameterDefinitions: + - name: Foo + type: INT + range: "1" + - name: Bar + type: STRING + range: ["Bar1", "Bar2"] + stepEnvironments: + - name: FirstS, + script: + actions: + onEnter: + command: python + args: ["-c", "print('FirstS Enter')"] + onExit: + command: python + args: ["-c", "print('FirstS Exit')"] + script: + actions: + onRun: + command: python + args: + - "-c" + - "print('J={{Param.J}} Foo={{Task.Param.Foo}}. Bar={{Task.Param.Bar}}')" diff --git a/fuzz/seeds/model_decode/tpl_20 b/fuzz/seeds/model_decode/tpl_20 new file mode 100644 index 00000000..f0f5e2f5 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_20 @@ -0,0 +1,23 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "TimeoutTest", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "Timeout", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": [ + "-c", + # Obfuscate "EXIT_NORMAL" so it doesn't appear in the log when Windows prints the command that's run to the log. + "import time,sys; print('SLEEP'); sys.stdout.flush(); time.sleep(5); print(chr(69)+'XIT_NORMAL')", + ], + "timeout": 2, + } + } + }, + } + ], + } \ No newline at end of file diff --git a/fuzz/seeds/model_decode/tpl_21 b/fuzz/seeds/model_decode/tpl_21 new file mode 100644 index 00000000..7aab3e5f --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_21 @@ -0,0 +1,131 @@ +# Catch-all sample template with different cases per Step +specificationVersion: jobtemplate-2023-09 +name: my-job +parameterDefinitions: +- name: Message + type: STRING + default: Hello, world! +jobEnvironments: +- name: rootEnv + variables: + rootVar: rootVal +steps: +# VALID STEPS +# Basic step; uses Job parameters and has an environment +- name: NormalStep + script: + actions: + onRun: + command: python + args: + - -c + - print('{{Param.Message}}') + stepEnvironments: + - name: env1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnteringEnv') +# Step that will wait for one minute before completing its Task +- name: LongCommand + script: + actions: + onRun: + command: sleep + args: + - '60' +# Step with the bare minimum information, i.e., no Task parameters, environments, or dependencies +- name: BareStep + script: + actions: + onRun: + command: python + args: + - -c + - print('zzz') +# Step with a direct dependency on a previous Step +- name: DependentStep + script: + actions: + onRun: + command: python + args: + - -c + - print('I am dependent!') + dependencies: + - dependsOn: BareStep +# Step with Task parameters +- name: TaskParamStep + parameterSpace: + taskParameterDefinitions: + - name: TaskNumber + type: INT + range: + - 1 + - 2 + - 3 + - name: TaskMessage + type: STRING + range: + - Hi! + - Bye! + - One=Two + script: + actions: + onRun: + command: python + args: + - -c + - print('{{Task.Param.TaskNumber}}.{{Task.Param.TaskMessage}}') +# Step with a transitive dependency and a direct dependency +- name: ExtraDependentStep + script: + actions: + onRun: + command: python + args: + - -c + - print('I am extra dependent!') + dependencies: + - dependsOn: DependentStep + - dependsOn: TaskParamStep +# Step with dependencies and Task parameters +- name: DependentParamStep + parameterSpace: + taskParameterDefinitions: + - name: Adjective + type: STRING + range: + - really + - very + - super + script: + actions: + onRun: + command: python + args: + - -c + - print('I am {{Task.Param.Adjective}} dependent!') + dependencies: + - dependsOn: TaskParamStep +# Step whose dependency has a step environment +- name: StepDepHasStepEnv + script: + actions: + onRun: + command: python + args: + - -c + - print('I have a dependency with a step environment!') + dependencies: + - dependsOn: NormalStep +# ERROR STEPS +# Step with a non-existent command that will throw an error when run +- name: BadCommand + script: + actions: + onRun: + command: aaaaaaaaaa diff --git a/fuzz/seeds/model_decode/tpl_22 b/fuzz/seeds/model_decode/tpl_22 new file mode 100644 index 00000000..1dde9252 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_22 @@ -0,0 +1,52 @@ +specificationVersion: "jobtemplate-2023-09" +extensions: + - REDACTED_ENV_VARS +name: Test Redacted Env +description: Test redacted environment variables + +jobEnvironments: + - name: RedactedEnv + script: + actions: + onEnter: + command: python + args: ["{{Env.File.Enter}}"] + onExit: + command: python + args: ["{{Env.File.Exit}}"] + embeddedFiles: + - name: Enter + type: TEXT + data: | + print("Setting redacted vars..") + print(f"openjd_redacted_env: SECRETVAR=SECRETVAL") + print(f"openjd_redacted_env: KEYSPACE =SECRETVAL") + print(f"openjd_redacted_env: VALSPACE= SPACEVAL") + print(f'openjd_redacted_env: "MULTILINE=first_line\\nsecond_line\\nthird_line"') + - name: Exit + type: TEXT + data: | + import os + print(f"SECRETVAR is {os.environ.get('SECRETVAR')}") + print(f"KEYSPACE is {os.environ.get('KEYSPACE')}") + print(f"VALSPACE is {os.environ.get('VALSPACE')}") + print(f"MULTILINE is {os.environ.get('VALSPACE')} END") + print("first_line") + print("second_line") + print("third_line") +steps: + - name: CheckVars + script: + actions: + onRun: + command: python + args: ["{{Task.File.Run}}"] + embeddedFiles: + - name: Run + type: TEXT + data: | + import os + print(f"SECRETVAR is {os.environ.get('SECRETVAR')}") + print(f"KEYSPACE is {os.environ.get('KEYSPACE')}") + print(f"VALSPACE is {os.environ.get('VALSPACE')}") + print(f"MULTILINE is {os.environ.get('VALSPACE')} END") diff --git a/fuzz/seeds/model_decode/tpl_23 b/fuzz/seeds/model_decode/tpl_23 new file mode 100644 index 00000000..72e062aa --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_23 @@ -0,0 +1,28 @@ +specificationVersion: jobtemplate-2023-09 +name: ReverseDeps +steps: +- name: StepC + dependencies: + - dependsOn: StepB + script: + actions: + onRun: + command: echo + args: + - "Running StepC" +- name: StepB + dependencies: + - dependsOn: StepA + script: + actions: + onRun: + command: echo + args: + - "Running StepB" +- name: StepA + script: + actions: + onRun: + command: echo + args: + - "Running StepA" diff --git a/fuzz/seeds/model_decode/tpl_24 b/fuzz/seeds/model_decode/tpl_24 new file mode 100644 index 00000000..73fbc15e --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_24 @@ -0,0 +1,18 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "Test", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "SimpleStep", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": ["-c", "import sys; print('DoTask'); sys.exit(1)"] + } + } + } + } + ] +} diff --git a/fuzz/seeds/model_decode/tpl_25 b/fuzz/seeds/model_decode/tpl_25 new file mode 100644 index 00000000..9f7875a6 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_25 @@ -0,0 +1,18 @@ +{ + "specificationVersion": "jobtemplate-2023-09", + "name": "Test", + "parameterDefinitions": [{"name": "J", "type": "STRING"}], + "steps": [ + { + "name": "SimpleStep", + "script": { + "actions": { + "onRun": { + "command": "python", + "args": ["-c", "print('DoTask {{Param.J}}')"], + } + } + }, + } + ], +} \ No newline at end of file diff --git a/fuzz/seeds/model_decode/tpl_26 b/fuzz/seeds/model_decode/tpl_26 new file mode 100644 index 00000000..d8909286 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_26 @@ -0,0 +1,47 @@ +specificationVersion: jobtemplate-2023-09 +name: Step Let In Step Env Test +extensions: + - EXPR +parameterDefinitions: + - name: Base + type: INT + default: 7 +steps: + - name: TestStep + let: + - val = Param.Base * 3 + - label = "item_" + string(val) + stepEnvironments: + - name: VarEnv + variables: + MY_VAL: "{{ val }}" + MY_LABEL: "{{ label }}" + - name: ScriptEnv + script: + actions: + onEnter: + command: python + args: + - -c + - "print('ENTER_VAL:{{val}}')\nprint('ENTER_LABEL:{{label}}')" + onExit: + command: python + args: + - -c + - "print('EXIT_VAL:{{val}}')\nprint('EXIT_LABEL:{{label}}')" + script: + actions: + onRun: + command: python + args: + - -c + - | + import os + print('ENV_VAL:' + os.environ.get('MY_VAL', '')) + print('ENV_LABEL:' + os.environ.get('MY_LABEL', '')) + print('TASK_VAL:{{val}}') + parameterSpace: + taskParameterDefinitions: + - name: Frame + type: INT + range: [1] diff --git a/fuzz/seeds/model_decode/tpl_3 b/fuzz/seeds/model_decode/tpl_3 new file mode 100644 index 00000000..d3825465 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_3 @@ -0,0 +1,79 @@ +specificationVersion: 'jobtemplate-2023-09' +extensions: + - TASK_CHUNKING +name: Chunked Job + +parameterDefinitions: +- name: Items + type: STRING + default: 1-40 +- name: ChunkSize + type: INT + default: 10 +- name: TargetRuntime + type: INT + default: 0 +- name: TaskSleepTime + type: FLOAT + default: 0.01 + +steps: +- name: Chunked Step + + parameterSpace: + taskParameterDefinitions: + - name: Item + type: CHUNK[INT] + range: "{{Param.Items}}" + chunks: + defaultTaskCount: "{{Param.ChunkSize}}" + targetRuntimeSeconds: "{{Param.TargetRuntime}}" + rangeConstraint: NONCONTIGUOUS + + script: + actions: + onRun: + command: bash + args: ['{{Task.File.Run}}'] + embeddedFiles: + - name: GetSleepTime + type: TEXT + filename: get_sleep_time.py + data: | + """ + Converts an Open Job Description range expression into a sleep time according to the job parameters. + * https://github.com/OpenJobDescription/openjd-specifications/wiki/2023-09-Template-Schemas#34111-intrangeexpr + """ + + import sys, re + + task_sleep_time = float(sys.argv[2]) + + def range_expr_to_list(range_expr): + # Regex that matches "", "-", or "-:" + int_pat = r"\s*(-?[0-9]+)\s*" + part_re = re.compile(f"^{int_pat}(?:-{int_pat}(?::{int_pat})?)?$") + result = [] + for part in range_expr.split(","): + if m := part_re.match(part): + start, end, step = m.groups() + if step is not None: + # Linear sequence "3-7:2" means the values [3, 5, 7]. + result.extend(range(int(start), int(end) + (int(step)//abs(int(step))), int(step))) + elif end is not None: + # Interval "3-6" means the values [3, 4, 5, 6]. + result.extend(range(int(start), int(end) + 1)) + else: + # Integer "3" means the values [3]. + result.append(int(start)) + else: + raise ValueError(f"Invalid frame range expression: {range_expr}") + return result + + print(task_sleep_time * len(range_expr_to_list(sys.argv[1]))) + - name: Run + type: TEXT + data: | + set -xeuo pipefail + + sleep "$(python '{{Task.File.GetSleepTime}}' '{{Task.Param.Item}}' '{{Param.TaskSleepTime}}')" diff --git a/fuzz/seeds/model_decode/tpl_4 b/fuzz/seeds/model_decode/tpl_4 new file mode 100644 index 00000000..94db7031 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_4 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: Env1 + script: + actions: + onEnter: + command: python + args: + - -c + - print('Env1 Enter') + onExit: + command: python + args: + - -c + - print('Env1 Exit') diff --git a/fuzz/seeds/model_decode/tpl_5 b/fuzz/seeds/model_decode/tpl_5 new file mode 100644 index 00000000..975a40a7 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_5 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: Env2 + script: + actions: + onEnter: + command: python + args: + - -c + - print('Env2 Enter') + onExit: + command: python + args: + - -c + - print('Env2 Exit') diff --git a/fuzz/seeds/model_decode/tpl_6 b/fuzz/seeds/model_decode/tpl_6 new file mode 100644 index 00000000..e233a202 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_6 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: EnvEnterFail + script: + actions: + onEnter: + command: python + args: + - -c + - import sys; print('EnvEnterFail Enter'); sys.exit(1) + onExit: + command: python + args: + - -c + - print('EnvEnterFail Exit') diff --git a/fuzz/seeds/model_decode/tpl_7 b/fuzz/seeds/model_decode/tpl_7 new file mode 100644 index 00000000..47bc416a --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_7 @@ -0,0 +1,15 @@ +specificationVersion: environment-2023-09 +environment: + name: EnvExitFail + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnvExitFail Enter') + onExit: + command: python + args: + - -c + - import sys; print('EnvExitFail Exit'); sys.exit(1) diff --git a/fuzz/seeds/model_decode/tpl_8 b/fuzz/seeds/model_decode/tpl_8 new file mode 100644 index 00000000..2214c265 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_8 @@ -0,0 +1,19 @@ +specificationVersion: environment-2023-09 +parameterDefinitions: + - name: EnvParam + type: STRING + default: DefaultForEnvParam +environment: + name: EnvWithParam + script: + actions: + onEnter: + command: python + args: + - -c + - print('EnvWithParam Enter {{Param.EnvParam}}') + onExit: + command: python + args: + - -c + - print('EnvWithParam Exit {{Param.EnvParam}}') diff --git a/fuzz/seeds/model_decode/tpl_9 b/fuzz/seeds/model_decode/tpl_9 new file mode 100644 index 00000000..2625c8d1 --- /dev/null +++ b/fuzz/seeds/model_decode/tpl_9 @@ -0,0 +1,23 @@ +specificationVersion: jobtemplate-2023-09 +name: FB1 Amount Min Max +extensions: + - FEATURE_BUNDLE_1 +parameterDefinitions: + - name: CpuMin + type: INT + default: 1 + - name: CpuMax + type: INT + default: 4 +steps: + - name: TestStep + hostRequirements: + amounts: + - name: amount.worker.vcpu + min: "{{Param.CpuMin}}" + max: "{{Param.CpuMax}}" + script: + actions: + onRun: + command: bash + args: ["-c", "echo 'Amount min/max works!'"] diff --git a/fuzz/seeds/range_expr/range_0 b/fuzz/seeds/range_expr/range_0 new file mode 100644 index 00000000..2fc089e7 --- /dev/null +++ b/fuzz/seeds/range_expr/range_0 @@ -0,0 +1 @@ +1-10 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_1 b/fuzz/seeds/range_expr/range_1 new file mode 100644 index 00000000..843e5ff6 --- /dev/null +++ b/fuzz/seeds/range_expr/range_1 @@ -0,0 +1 @@ +1-10:2 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_2 b/fuzz/seeds/range_expr/range_2 new file mode 100644 index 00000000..13a316ba --- /dev/null +++ b/fuzz/seeds/range_expr/range_2 @@ -0,0 +1 @@ +0-100:5 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_3 b/fuzz/seeds/range_expr/range_3 new file mode 100644 index 00000000..3d8a807f --- /dev/null +++ b/fuzz/seeds/range_expr/range_3 @@ -0,0 +1 @@ +-5-5 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_4 b/fuzz/seeds/range_expr/range_4 new file mode 100644 index 00000000..2319ba48 --- /dev/null +++ b/fuzz/seeds/range_expr/range_4 @@ -0,0 +1 @@ +1-1 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_5 b/fuzz/seeds/range_expr/range_5 new file mode 100644 index 00000000..f1af364c --- /dev/null +++ b/fuzz/seeds/range_expr/range_5 @@ -0,0 +1 @@ +5-1 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_6 b/fuzz/seeds/range_expr/range_6 new file mode 100644 index 00000000..c4324b01 --- /dev/null +++ b/fuzz/seeds/range_expr/range_6 @@ -0,0 +1 @@ +1-10:0 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_7 b/fuzz/seeds/range_expr/range_7 new file mode 100644 index 00000000..a30d784b --- /dev/null +++ b/fuzz/seeds/range_expr/range_7 @@ -0,0 +1 @@ +-9223372036854775807-9223372036854775807 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_8 b/fuzz/seeds/range_expr/range_8 new file mode 100644 index 00000000..5d1527c1 --- /dev/null +++ b/fuzz/seeds/range_expr/range_8 @@ -0,0 +1 @@ +9223372036854775806-9223372036854775807:2 \ No newline at end of file diff --git a/fuzz/seeds/range_expr/range_9 b/fuzz/seeds/range_expr/range_9 new file mode 100644 index 00000000..8be8164c --- /dev/null +++ b/fuzz/seeds/range_expr/range_9 @@ -0,0 +1 @@ +0-9223372036854775807 \ No newline at end of file diff --git a/fuzz/seeds/range_expr_slice/seed_0-10000000000001000000000 b/fuzz/seeds/range_expr_slice/seed_0-10000000000001000000000 new file mode 100644 index 00000000..60a51af3 --- /dev/null +++ b/fuzz/seeds/range_expr_slice/seed_0-10000000000001000000000 @@ -0,0 +1 @@ +0-1000000000000:1000000000 \ No newline at end of file diff --git a/fuzz/seeds/range_expr_slice/seed_0-1005 b/fuzz/seeds/range_expr_slice/seed_0-1005 new file mode 100644 index 00000000..13a316ba --- /dev/null +++ b/fuzz/seeds/range_expr_slice/seed_0-1005 @@ -0,0 +1 @@ +0-100:5 \ No newline at end of file diff --git a/fuzz/seeds/range_expr_slice/seed_1-10 b/fuzz/seeds/range_expr_slice/seed_1-10 new file mode 100644 index 00000000..2fc089e7 --- /dev/null +++ b/fuzz/seeds/range_expr_slice/seed_1-10 @@ -0,0 +1 @@ +1-10 \ No newline at end of file diff --git a/fuzz/seeds/range_expr_slice/seed_1-510-15 b/fuzz/seeds/range_expr_slice/seed_1-510-15 new file mode 100644 index 00000000..55f9abac --- /dev/null +++ b/fuzz/seeds/range_expr_slice/seed_1-510-15 @@ -0,0 +1 @@ +1-5,10-15 \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_0 b/fuzz/seeds/snapshot_decode/manifest_0 new file mode 100644 index 00000000..7a319874 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_0 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"1111","mtime":1000,"path":"a/b/c/deep.txt","size":100},{"hash":"2222","mtime":2000,"path":"a/b/shallow.txt","size":200},{"hash":"3333","mtime":3000,"path":"a/top.txt","size":300},{"hash":"4444","mtime":4000,"path":"root.txt","size":400}],"totalSize":1000} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_1 b/fuzz/seeds/snapshot_decode/manifest_1 new file mode 100644 index 00000000..7e6c49b6 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_1 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"c3c3","mtime":3000,"path":"FILE.txt","size":30},{"hash":"c1c1","mtime":1000,"path":"File.txt","size":10},{"hash":"c4c4","mtime":4000,"path":"fILE.txt","size":40},{"hash":"c2c2","mtime":2000,"path":"file.txt","size":20}],"totalSize":100} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_10 b/fuzz/seeds/snapshot_decode/manifest_10 new file mode 100644 index 00000000..5365c758 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_10 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"0000","mtime":1000,"path":"empty_file.txt","size":0}],"totalSize":0} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_2 b/fuzz/seeds/snapshot_decode/manifest_2 new file mode 100644 index 00000000..0e22ebc6 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_2 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"00000000000000000000000000000000","mtime":1700000000000000,"path":"dir0/subdir0/file_0000.dat","size":1024},{"hash":"0000000000000000000000000000000a","mtime":1700000010000000,"path":"dir0/subdir0/file_0010.dat","size":11264},{"hash":"00000000000000000000000000000014","mtime":1700000020000000,"path":"dir0/subdir0/file_0020.dat","size":21504},{"hash":"0000000000000000000000000000001e","mtime":1700000030000000,"path":"dir0/subdir0/file_0030.dat","size":31744},{"hash":"00000000000000000000000000000028","mtime":1700000040000000,"path":"dir0/subdir0/file_0040.dat","size":41984},{"hash":"00000000000000000000000000000032","mtime":1700000050000000,"path":"dir0/subdir0/file_0050.dat","size":52224},{"hash":"0000000000000000000000000000003c","mtime":1700000060000000,"path":"dir0/subdir0/file_0060.dat","size":62464},{"hash":"00000000000000000000000000000046","mtime":1700000070000000,"path":"dir0/subdir0/file_0070.dat","size":72704},{"hash":"00000000000000000000000000000050","mtime":1700000080000000,"path":"dir0/subdir0/file_0080.dat","size":82944},{"hash":"0000000000000000000000000000005a","mtime":1700000090000000,"path":"dir0/subdir0/file_0090.dat","size":93184},{"hash":"00000000000000000000000000000001","mtime":1700000001000000,"path":"dir1/subdir1/file_0001.dat","size":2048},{"hash":"0000000000000000000000000000000b","mtime":1700000011000000,"path":"dir1/subdir1/file_0011.dat","size":12288},{"hash":"00000000000000000000000000000015","mtime":1700000021000000,"path":"dir1/subdir1/file_0021.dat","size":22528},{"hash":"0000000000000000000000000000001f","mtime":1700000031000000,"path":"dir1/subdir1/file_0031.dat","size":32768},{"hash":"00000000000000000000000000000029","mtime":1700000041000000,"path":"dir1/subdir1/file_0041.dat","size":43008},{"hash":"00000000000000000000000000000033","mtime":1700000051000000,"path":"dir1/subdir1/file_0051.dat","size":53248},{"hash":"0000000000000000000000000000003d","mtime":1700000061000000,"path":"dir1/subdir1/file_0061.dat","size":63488},{"hash":"00000000000000000000000000000047","mtime":1700000071000000,"path":"dir1/subdir1/file_0071.dat","size":73728},{"hash":"00000000000000000000000000000051","mtime":1700000081000000,"path":"dir1/subdir1/file_0081.dat","size":83968},{"hash":"0000000000000000000000000000005b","mtime":1700000091000000,"path":"dir1/subdir1/file_0091.dat","size":94208},{"hash":"00000000000000000000000000000002","mtime":1700000002000000,"path":"dir2/subdir2/file_0002.dat","size":3072},{"hash":"0000000000000000000000000000000c","mtime":1700000012000000,"path":"dir2/subdir2/file_0012.dat","size":13312},{"hash":"00000000000000000000000000000016","mtime":1700000022000000,"path":"dir2/subdir2/file_0022.dat","size":23552},{"hash":"00000000000000000000000000000020","mtime":1700000032000000,"path":"dir2/subdir2/file_0032.dat","size":33792},{"hash":"0000000000000000000000000000002a","mtime":1700000042000000,"path":"dir2/subdir2/file_0042.dat","size":44032},{"hash":"00000000000000000000000000000034","mtime":1700000052000000,"path":"dir2/subdir2/file_0052.dat","size":54272},{"hash":"0000000000000000000000000000003e","mtime":1700000062000000,"path":"dir2/subdir2/file_0062.dat","size":64512},{"hash":"00000000000000000000000000000048","mtime":1700000072000000,"path":"dir2/subdir2/file_0072.dat","size":74752},{"hash":"00000000000000000000000000000052","mtime":1700000082000000,"path":"dir2/subdir2/file_0082.dat","size":84992},{"hash":"0000000000000000000000000000005c","mtime":1700000092000000,"path":"dir2/subdir2/file_0092.dat","size":95232},{"hash":"00000000000000000000000000000003","mtime":1700000003000000,"path":"dir3/subdir3/file_0003.dat","size":4096},{"hash":"0000000000000000000000000000000d","mtime":1700000013000000,"path":"dir3/subdir3/file_0013.dat","size":14336},{"hash":"00000000000000000000000000000017","mtime":1700000023000000,"path":"dir3/subdir3/file_0023.dat","size":24576},{"hash":"00000000000000000000000000000021","mtime":1700000033000000,"path":"dir3/subdir3/file_0033.dat","size":34816},{"hash":"0000000000000000000000000000002b","mtime":1700000043000000,"path":"dir3/subdir3/file_0043.dat","size":45056},{"hash":"00000000000000000000000000000035","mtime":1700000053000000,"path":"dir3/subdir3/file_0053.dat","size":55296},{"hash":"0000000000000000000000000000003f","mtime":1700000063000000,"path":"dir3/subdir3/file_0063.dat","size":65536},{"hash":"00000000000000000000000000000049","mtime":1700000073000000,"path":"dir3/subdir3/file_0073.dat","size":75776},{"hash":"00000000000000000000000000000053","mtime":1700000083000000,"path":"dir3/subdir3/file_0083.dat","size":86016},{"hash":"0000000000000000000000000000005d","mtime":1700000093000000,"path":"dir3/subdir3/file_0093.dat","size":96256},{"hash":"00000000000000000000000000000004","mtime":1700000004000000,"path":"dir4/subdir4/file_0004.dat","size":5120},{"hash":"0000000000000000000000000000000e","mtime":1700000014000000,"path":"dir4/subdir4/file_0014.dat","size":15360},{"hash":"00000000000000000000000000000018","mtime":1700000024000000,"path":"dir4/subdir4/file_0024.dat","size":25600},{"hash":"00000000000000000000000000000022","mtime":1700000034000000,"path":"dir4/subdir4/file_0034.dat","size":35840},{"hash":"0000000000000000000000000000002c","mtime":1700000044000000,"path":"dir4/subdir4/file_0044.dat","size":46080},{"hash":"00000000000000000000000000000036","mtime":1700000054000000,"path":"dir4/subdir4/file_0054.dat","size":56320},{"hash":"00000000000000000000000000000040","mtime":1700000064000000,"path":"dir4/subdir4/file_0064.dat","size":66560},{"hash":"0000000000000000000000000000004a","mtime":1700000074000000,"path":"dir4/subdir4/file_0074.dat","size":76800},{"hash":"00000000000000000000000000000054","mtime":1700000084000000,"path":"dir4/subdir4/file_0084.dat","size":87040},{"hash":"0000000000000000000000000000005e","mtime":1700000094000000,"path":"dir4/subdir4/file_0094.dat","size":97280},{"hash":"00000000000000000000000000000005","mtime":1700000005000000,"path":"dir5/subdir0/file_0005.dat","size":6144},{"hash":"0000000000000000000000000000000f","mtime":1700000015000000,"path":"dir5/subdir0/file_0015.dat","size":16384},{"hash":"00000000000000000000000000000019","mtime":1700000025000000,"path":"dir5/subdir0/file_0025.dat","size":26624},{"hash":"00000000000000000000000000000023","mtime":1700000035000000,"path":"dir5/subdir0/file_0035.dat","size":36864},{"hash":"0000000000000000000000000000002d","mtime":1700000045000000,"path":"dir5/subdir0/file_0045.dat","size":47104},{"hash":"00000000000000000000000000000037","mtime":1700000055000000,"path":"dir5/subdir0/file_0055.dat","size":57344},{"hash":"00000000000000000000000000000041","mtime":1700000065000000,"path":"dir5/subdir0/file_0065.dat","size":67584},{"hash":"0000000000000000000000000000004b","mtime":1700000075000000,"path":"dir5/subdir0/file_0075.dat","size":77824},{"hash":"00000000000000000000000000000055","mtime":1700000085000000,"path":"dir5/subdir0/file_0085.dat","size":88064},{"hash":"0000000000000000000000000000005f","mtime":1700000095000000,"path":"dir5/subdir0/file_0095.dat","size":98304},{"hash":"00000000000000000000000000000006","mtime":1700000006000000,"path":"dir6/subdir1/file_0006.dat","size":7168},{"hash":"00000000000000000000000000000010","mtime":1700000016000000,"path":"dir6/subdir1/file_0016.dat","size":17408},{"hash":"0000000000000000000000000000001a","mtime":1700000026000000,"path":"dir6/subdir1/file_0026.dat","size":27648},{"hash":"00000000000000000000000000000024","mtime":1700000036000000,"path":"dir6/subdir1/file_0036.dat","size":37888},{"hash":"0000000000000000000000000000002e","mtime":1700000046000000,"path":"dir6/subdir1/file_0046.dat","size":48128},{"hash":"00000000000000000000000000000038","mtime":1700000056000000,"path":"dir6/subdir1/file_0056.dat","size":58368},{"hash":"00000000000000000000000000000042","mtime":1700000066000000,"path":"dir6/subdir1/file_0066.dat","size":68608},{"hash":"0000000000000000000000000000004c","mtime":1700000076000000,"path":"dir6/subdir1/file_0076.dat","size":78848},{"hash":"00000000000000000000000000000056","mtime":1700000086000000,"path":"dir6/subdir1/file_0086.dat","size":89088},{"hash":"00000000000000000000000000000060","mtime":1700000096000000,"path":"dir6/subdir1/file_0096.dat","size":99328},{"hash":"00000000000000000000000000000007","mtime":1700000007000000,"path":"dir7/subdir2/file_0007.dat","size":8192},{"hash":"00000000000000000000000000000011","mtime":1700000017000000,"path":"dir7/subdir2/file_0017.dat","size":18432},{"hash":"0000000000000000000000000000001b","mtime":1700000027000000,"path":"dir7/subdir2/file_0027.dat","size":28672},{"hash":"00000000000000000000000000000025","mtime":1700000037000000,"path":"dir7/subdir2/file_0037.dat","size":38912},{"hash":"0000000000000000000000000000002f","mtime":1700000047000000,"path":"dir7/subdir2/file_0047.dat","size":49152},{"hash":"00000000000000000000000000000039","mtime":1700000057000000,"path":"dir7/subdir2/file_0057.dat","size":59392},{"hash":"00000000000000000000000000000043","mtime":1700000067000000,"path":"dir7/subdir2/file_0067.dat","size":69632},{"hash":"0000000000000000000000000000004d","mtime":1700000077000000,"path":"dir7/subdir2/file_0077.dat","size":79872},{"hash":"00000000000000000000000000000057","mtime":1700000087000000,"path":"dir7/subdir2/file_0087.dat","size":90112},{"hash":"00000000000000000000000000000061","mtime":1700000097000000,"path":"dir7/subdir2/file_0097.dat","size":100352},{"hash":"00000000000000000000000000000008","mtime":1700000008000000,"path":"dir8/subdir3/file_0008.dat","size":9216},{"hash":"00000000000000000000000000000012","mtime":1700000018000000,"path":"dir8/subdir3/file_0018.dat","size":19456},{"hash":"0000000000000000000000000000001c","mtime":1700000028000000,"path":"dir8/subdir3/file_0028.dat","size":29696},{"hash":"00000000000000000000000000000026","mtime":1700000038000000,"path":"dir8/subdir3/file_0038.dat","size":39936},{"hash":"00000000000000000000000000000030","mtime":1700000048000000,"path":"dir8/subdir3/file_0048.dat","size":50176},{"hash":"0000000000000000000000000000003a","mtime":1700000058000000,"path":"dir8/subdir3/file_0058.dat","size":60416},{"hash":"00000000000000000000000000000044","mtime":1700000068000000,"path":"dir8/subdir3/file_0068.dat","size":70656},{"hash":"0000000000000000000000000000004e","mtime":1700000078000000,"path":"dir8/subdir3/file_0078.dat","size":80896},{"hash":"00000000000000000000000000000058","mtime":1700000088000000,"path":"dir8/subdir3/file_0088.dat","size":91136},{"hash":"00000000000000000000000000000062","mtime":1700000098000000,"path":"dir8/subdir3/file_0098.dat","size":101376},{"hash":"00000000000000000000000000000009","mtime":1700000009000000,"path":"dir9/subdir4/file_0009.dat","size":10240},{"hash":"00000000000000000000000000000013","mtime":1700000019000000,"path":"dir9/subdir4/file_0019.dat","size":20480},{"hash":"0000000000000000000000000000001d","mtime":1700000029000000,"path":"dir9/subdir4/file_0029.dat","size":30720},{"hash":"00000000000000000000000000000027","mtime":1700000039000000,"path":"dir9/subdir4/file_0039.dat","size":40960},{"hash":"00000000000000000000000000000031","mtime":1700000049000000,"path":"dir9/subdir4/file_0049.dat","size":51200},{"hash":"0000000000000000000000000000003b","mtime":1700000059000000,"path":"dir9/subdir4/file_0059.dat","size":61440},{"hash":"00000000000000000000000000000045","mtime":1700000069000000,"path":"dir9/subdir4/file_0069.dat","size":71680},{"hash":"0000000000000000000000000000004f","mtime":1700000079000000,"path":"dir9/subdir4/file_0079.dat","size":81920},{"hash":"00000000000000000000000000000059","mtime":1700000089000000,"path":"dir9/subdir4/file_0089.dat","size":92160},{"hash":"00000000000000000000000000000063","mtime":1700000099000000,"path":"dir9/subdir4/file_0099.dat","size":102400}],"totalSize":5171200} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_3 b/fuzz/seeds/snapshot_decode/manifest_3 new file mode 100644 index 00000000..1b60d73d --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_3 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"sp5sp5","mtime":5000,"path":"UPPERCASE.TXT","size":50},{"hash":"sp1sp1","mtime":1000,"path":"file with spaces.txt","size":10},{"hash":"sp2sp2","mtime":2000,"path":"file-with-dashes.txt","size":20},{"hash":"sp4sp4","mtime":4000,"path":"file.multiple.dots.txt","size":40},{"hash":"sp3sp3","mtime":3000,"path":"file_with_underscores.txt","size":30}],"totalSize":150} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_4 b/fuzz/seeds/snapshot_decode/manifest_4 new file mode 100644 index 00000000..6d698275 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_4 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"s2s2","mtime":2000,"path":"Atop.txt","size":20},{"hash":"s3s3","mtime":3000,"path":"ztop.txt","size":30},{"hash":"s1s1","mtime":1000,"path":"~tilde.txt","size":10},{"hash":"s4s4","mtime":4000,"path":"\u00e9accent.txt","size":40},{"hash":"s5s5","mtime":5000,"path":"\u0100macron.txt","size":50}],"totalSize":150} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_5 b/fuzz/seeds/snapshot_decode/manifest_5 new file mode 100644 index 00000000..44857213 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_5 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4","mtime":1700000000000000,"path":"hello.txt","size":11}],"totalSize":11} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_6 b/fuzz/seeds/snapshot_decode/manifest_6 new file mode 100644 index 00000000..31da224f --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_6 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"t2t2","mtime":9999999999999999,"path":"huge.dat","size":1099511627776},{"hash":"t3t3","mtime":1,"path":"normal.txt","size":1},{"hash":"t1t1","mtime":0,"path":"tiny.txt","size":0}],"totalSize":1099511627777} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_7 b/fuzz/seeds/snapshot_decode/manifest_7 new file mode 100644 index 00000000..c71a2d0e --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_7 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"tgt1","mtime":1000,"path":"link.txt","size":100},{"hash":"tgt1","mtime":1000,"path":"target.txt","size":100}],"totalSize":200} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_8 b/fuzz/seeds/snapshot_decode/manifest_8 new file mode 100644 index 00000000..4012821e --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_8 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"u5u5","mtime":5000,"path":"abc.txt","size":50},{"hash":"u1u1","mtime":1000,"path":"caf\u00e9.txt","size":10},{"hash":"u2u2","mtime":2000,"path":"na\u00efve.txt","size":20},{"hash":"u4u4","mtime":4000,"path":"\u00c5ngstr\u00f6m.txt","size":40},{"hash":"u3u3","mtime":3000,"path":"\u65e5\u672c\u8a9e.txt","size":30}],"totalSize":150} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_decode/manifest_9 b/fuzz/seeds/snapshot_decode/manifest_9 new file mode 100644 index 00000000..cecf1566 --- /dev/null +++ b/fuzz/seeds/snapshot_decode/manifest_9 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"bbbb","mtime":2000,"path":"alpha.txt","size":20},{"hash":"cccc","mtime":3000,"path":"middle.txt","size":30},{"hash":"aaaa","mtime":1000,"path":"zebra.txt","size":10}],"totalSize":60} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_0 b/fuzz/seeds/snapshot_ops/m_0 new file mode 100644 index 00000000..7a319874 --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_0 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"1111","mtime":1000,"path":"a/b/c/deep.txt","size":100},{"hash":"2222","mtime":2000,"path":"a/b/shallow.txt","size":200},{"hash":"3333","mtime":3000,"path":"a/top.txt","size":300},{"hash":"4444","mtime":4000,"path":"root.txt","size":400}],"totalSize":1000} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_1 b/fuzz/seeds/snapshot_ops/m_1 new file mode 100644 index 00000000..7e6c49b6 --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_1 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"c3c3","mtime":3000,"path":"FILE.txt","size":30},{"hash":"c1c1","mtime":1000,"path":"File.txt","size":10},{"hash":"c4c4","mtime":4000,"path":"fILE.txt","size":40},{"hash":"c2c2","mtime":2000,"path":"file.txt","size":20}],"totalSize":100} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_2 b/fuzz/seeds/snapshot_ops/m_2 new file mode 100644 index 00000000..0e22ebc6 --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_2 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"00000000000000000000000000000000","mtime":1700000000000000,"path":"dir0/subdir0/file_0000.dat","size":1024},{"hash":"0000000000000000000000000000000a","mtime":1700000010000000,"path":"dir0/subdir0/file_0010.dat","size":11264},{"hash":"00000000000000000000000000000014","mtime":1700000020000000,"path":"dir0/subdir0/file_0020.dat","size":21504},{"hash":"0000000000000000000000000000001e","mtime":1700000030000000,"path":"dir0/subdir0/file_0030.dat","size":31744},{"hash":"00000000000000000000000000000028","mtime":1700000040000000,"path":"dir0/subdir0/file_0040.dat","size":41984},{"hash":"00000000000000000000000000000032","mtime":1700000050000000,"path":"dir0/subdir0/file_0050.dat","size":52224},{"hash":"0000000000000000000000000000003c","mtime":1700000060000000,"path":"dir0/subdir0/file_0060.dat","size":62464},{"hash":"00000000000000000000000000000046","mtime":1700000070000000,"path":"dir0/subdir0/file_0070.dat","size":72704},{"hash":"00000000000000000000000000000050","mtime":1700000080000000,"path":"dir0/subdir0/file_0080.dat","size":82944},{"hash":"0000000000000000000000000000005a","mtime":1700000090000000,"path":"dir0/subdir0/file_0090.dat","size":93184},{"hash":"00000000000000000000000000000001","mtime":1700000001000000,"path":"dir1/subdir1/file_0001.dat","size":2048},{"hash":"0000000000000000000000000000000b","mtime":1700000011000000,"path":"dir1/subdir1/file_0011.dat","size":12288},{"hash":"00000000000000000000000000000015","mtime":1700000021000000,"path":"dir1/subdir1/file_0021.dat","size":22528},{"hash":"0000000000000000000000000000001f","mtime":1700000031000000,"path":"dir1/subdir1/file_0031.dat","size":32768},{"hash":"00000000000000000000000000000029","mtime":1700000041000000,"path":"dir1/subdir1/file_0041.dat","size":43008},{"hash":"00000000000000000000000000000033","mtime":1700000051000000,"path":"dir1/subdir1/file_0051.dat","size":53248},{"hash":"0000000000000000000000000000003d","mtime":1700000061000000,"path":"dir1/subdir1/file_0061.dat","size":63488},{"hash":"00000000000000000000000000000047","mtime":1700000071000000,"path":"dir1/subdir1/file_0071.dat","size":73728},{"hash":"00000000000000000000000000000051","mtime":1700000081000000,"path":"dir1/subdir1/file_0081.dat","size":83968},{"hash":"0000000000000000000000000000005b","mtime":1700000091000000,"path":"dir1/subdir1/file_0091.dat","size":94208},{"hash":"00000000000000000000000000000002","mtime":1700000002000000,"path":"dir2/subdir2/file_0002.dat","size":3072},{"hash":"0000000000000000000000000000000c","mtime":1700000012000000,"path":"dir2/subdir2/file_0012.dat","size":13312},{"hash":"00000000000000000000000000000016","mtime":1700000022000000,"path":"dir2/subdir2/file_0022.dat","size":23552},{"hash":"00000000000000000000000000000020","mtime":1700000032000000,"path":"dir2/subdir2/file_0032.dat","size":33792},{"hash":"0000000000000000000000000000002a","mtime":1700000042000000,"path":"dir2/subdir2/file_0042.dat","size":44032},{"hash":"00000000000000000000000000000034","mtime":1700000052000000,"path":"dir2/subdir2/file_0052.dat","size":54272},{"hash":"0000000000000000000000000000003e","mtime":1700000062000000,"path":"dir2/subdir2/file_0062.dat","size":64512},{"hash":"00000000000000000000000000000048","mtime":1700000072000000,"path":"dir2/subdir2/file_0072.dat","size":74752},{"hash":"00000000000000000000000000000052","mtime":1700000082000000,"path":"dir2/subdir2/file_0082.dat","size":84992},{"hash":"0000000000000000000000000000005c","mtime":1700000092000000,"path":"dir2/subdir2/file_0092.dat","size":95232},{"hash":"00000000000000000000000000000003","mtime":1700000003000000,"path":"dir3/subdir3/file_0003.dat","size":4096},{"hash":"0000000000000000000000000000000d","mtime":1700000013000000,"path":"dir3/subdir3/file_0013.dat","size":14336},{"hash":"00000000000000000000000000000017","mtime":1700000023000000,"path":"dir3/subdir3/file_0023.dat","size":24576},{"hash":"00000000000000000000000000000021","mtime":1700000033000000,"path":"dir3/subdir3/file_0033.dat","size":34816},{"hash":"0000000000000000000000000000002b","mtime":1700000043000000,"path":"dir3/subdir3/file_0043.dat","size":45056},{"hash":"00000000000000000000000000000035","mtime":1700000053000000,"path":"dir3/subdir3/file_0053.dat","size":55296},{"hash":"0000000000000000000000000000003f","mtime":1700000063000000,"path":"dir3/subdir3/file_0063.dat","size":65536},{"hash":"00000000000000000000000000000049","mtime":1700000073000000,"path":"dir3/subdir3/file_0073.dat","size":75776},{"hash":"00000000000000000000000000000053","mtime":1700000083000000,"path":"dir3/subdir3/file_0083.dat","size":86016},{"hash":"0000000000000000000000000000005d","mtime":1700000093000000,"path":"dir3/subdir3/file_0093.dat","size":96256},{"hash":"00000000000000000000000000000004","mtime":1700000004000000,"path":"dir4/subdir4/file_0004.dat","size":5120},{"hash":"0000000000000000000000000000000e","mtime":1700000014000000,"path":"dir4/subdir4/file_0014.dat","size":15360},{"hash":"00000000000000000000000000000018","mtime":1700000024000000,"path":"dir4/subdir4/file_0024.dat","size":25600},{"hash":"00000000000000000000000000000022","mtime":1700000034000000,"path":"dir4/subdir4/file_0034.dat","size":35840},{"hash":"0000000000000000000000000000002c","mtime":1700000044000000,"path":"dir4/subdir4/file_0044.dat","size":46080},{"hash":"00000000000000000000000000000036","mtime":1700000054000000,"path":"dir4/subdir4/file_0054.dat","size":56320},{"hash":"00000000000000000000000000000040","mtime":1700000064000000,"path":"dir4/subdir4/file_0064.dat","size":66560},{"hash":"0000000000000000000000000000004a","mtime":1700000074000000,"path":"dir4/subdir4/file_0074.dat","size":76800},{"hash":"00000000000000000000000000000054","mtime":1700000084000000,"path":"dir4/subdir4/file_0084.dat","size":87040},{"hash":"0000000000000000000000000000005e","mtime":1700000094000000,"path":"dir4/subdir4/file_0094.dat","size":97280},{"hash":"00000000000000000000000000000005","mtime":1700000005000000,"path":"dir5/subdir0/file_0005.dat","size":6144},{"hash":"0000000000000000000000000000000f","mtime":1700000015000000,"path":"dir5/subdir0/file_0015.dat","size":16384},{"hash":"00000000000000000000000000000019","mtime":1700000025000000,"path":"dir5/subdir0/file_0025.dat","size":26624},{"hash":"00000000000000000000000000000023","mtime":1700000035000000,"path":"dir5/subdir0/file_0035.dat","size":36864},{"hash":"0000000000000000000000000000002d","mtime":1700000045000000,"path":"dir5/subdir0/file_0045.dat","size":47104},{"hash":"00000000000000000000000000000037","mtime":1700000055000000,"path":"dir5/subdir0/file_0055.dat","size":57344},{"hash":"00000000000000000000000000000041","mtime":1700000065000000,"path":"dir5/subdir0/file_0065.dat","size":67584},{"hash":"0000000000000000000000000000004b","mtime":1700000075000000,"path":"dir5/subdir0/file_0075.dat","size":77824},{"hash":"00000000000000000000000000000055","mtime":1700000085000000,"path":"dir5/subdir0/file_0085.dat","size":88064},{"hash":"0000000000000000000000000000005f","mtime":1700000095000000,"path":"dir5/subdir0/file_0095.dat","size":98304},{"hash":"00000000000000000000000000000006","mtime":1700000006000000,"path":"dir6/subdir1/file_0006.dat","size":7168},{"hash":"00000000000000000000000000000010","mtime":1700000016000000,"path":"dir6/subdir1/file_0016.dat","size":17408},{"hash":"0000000000000000000000000000001a","mtime":1700000026000000,"path":"dir6/subdir1/file_0026.dat","size":27648},{"hash":"00000000000000000000000000000024","mtime":1700000036000000,"path":"dir6/subdir1/file_0036.dat","size":37888},{"hash":"0000000000000000000000000000002e","mtime":1700000046000000,"path":"dir6/subdir1/file_0046.dat","size":48128},{"hash":"00000000000000000000000000000038","mtime":1700000056000000,"path":"dir6/subdir1/file_0056.dat","size":58368},{"hash":"00000000000000000000000000000042","mtime":1700000066000000,"path":"dir6/subdir1/file_0066.dat","size":68608},{"hash":"0000000000000000000000000000004c","mtime":1700000076000000,"path":"dir6/subdir1/file_0076.dat","size":78848},{"hash":"00000000000000000000000000000056","mtime":1700000086000000,"path":"dir6/subdir1/file_0086.dat","size":89088},{"hash":"00000000000000000000000000000060","mtime":1700000096000000,"path":"dir6/subdir1/file_0096.dat","size":99328},{"hash":"00000000000000000000000000000007","mtime":1700000007000000,"path":"dir7/subdir2/file_0007.dat","size":8192},{"hash":"00000000000000000000000000000011","mtime":1700000017000000,"path":"dir7/subdir2/file_0017.dat","size":18432},{"hash":"0000000000000000000000000000001b","mtime":1700000027000000,"path":"dir7/subdir2/file_0027.dat","size":28672},{"hash":"00000000000000000000000000000025","mtime":1700000037000000,"path":"dir7/subdir2/file_0037.dat","size":38912},{"hash":"0000000000000000000000000000002f","mtime":1700000047000000,"path":"dir7/subdir2/file_0047.dat","size":49152},{"hash":"00000000000000000000000000000039","mtime":1700000057000000,"path":"dir7/subdir2/file_0057.dat","size":59392},{"hash":"00000000000000000000000000000043","mtime":1700000067000000,"path":"dir7/subdir2/file_0067.dat","size":69632},{"hash":"0000000000000000000000000000004d","mtime":1700000077000000,"path":"dir7/subdir2/file_0077.dat","size":79872},{"hash":"00000000000000000000000000000057","mtime":1700000087000000,"path":"dir7/subdir2/file_0087.dat","size":90112},{"hash":"00000000000000000000000000000061","mtime":1700000097000000,"path":"dir7/subdir2/file_0097.dat","size":100352},{"hash":"00000000000000000000000000000008","mtime":1700000008000000,"path":"dir8/subdir3/file_0008.dat","size":9216},{"hash":"00000000000000000000000000000012","mtime":1700000018000000,"path":"dir8/subdir3/file_0018.dat","size":19456},{"hash":"0000000000000000000000000000001c","mtime":1700000028000000,"path":"dir8/subdir3/file_0028.dat","size":29696},{"hash":"00000000000000000000000000000026","mtime":1700000038000000,"path":"dir8/subdir3/file_0038.dat","size":39936},{"hash":"00000000000000000000000000000030","mtime":1700000048000000,"path":"dir8/subdir3/file_0048.dat","size":50176},{"hash":"0000000000000000000000000000003a","mtime":1700000058000000,"path":"dir8/subdir3/file_0058.dat","size":60416},{"hash":"00000000000000000000000000000044","mtime":1700000068000000,"path":"dir8/subdir3/file_0068.dat","size":70656},{"hash":"0000000000000000000000000000004e","mtime":1700000078000000,"path":"dir8/subdir3/file_0078.dat","size":80896},{"hash":"00000000000000000000000000000058","mtime":1700000088000000,"path":"dir8/subdir3/file_0088.dat","size":91136},{"hash":"00000000000000000000000000000062","mtime":1700000098000000,"path":"dir8/subdir3/file_0098.dat","size":101376},{"hash":"00000000000000000000000000000009","mtime":1700000009000000,"path":"dir9/subdir4/file_0009.dat","size":10240},{"hash":"00000000000000000000000000000013","mtime":1700000019000000,"path":"dir9/subdir4/file_0019.dat","size":20480},{"hash":"0000000000000000000000000000001d","mtime":1700000029000000,"path":"dir9/subdir4/file_0029.dat","size":30720},{"hash":"00000000000000000000000000000027","mtime":1700000039000000,"path":"dir9/subdir4/file_0039.dat","size":40960},{"hash":"00000000000000000000000000000031","mtime":1700000049000000,"path":"dir9/subdir4/file_0049.dat","size":51200},{"hash":"0000000000000000000000000000003b","mtime":1700000059000000,"path":"dir9/subdir4/file_0059.dat","size":61440},{"hash":"00000000000000000000000000000045","mtime":1700000069000000,"path":"dir9/subdir4/file_0069.dat","size":71680},{"hash":"0000000000000000000000000000004f","mtime":1700000079000000,"path":"dir9/subdir4/file_0079.dat","size":81920},{"hash":"00000000000000000000000000000059","mtime":1700000089000000,"path":"dir9/subdir4/file_0089.dat","size":92160},{"hash":"00000000000000000000000000000063","mtime":1700000099000000,"path":"dir9/subdir4/file_0099.dat","size":102400}],"totalSize":5171200} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_3 b/fuzz/seeds/snapshot_ops/m_3 new file mode 100644 index 00000000..1b60d73d --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_3 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"sp5sp5","mtime":5000,"path":"UPPERCASE.TXT","size":50},{"hash":"sp1sp1","mtime":1000,"path":"file with spaces.txt","size":10},{"hash":"sp2sp2","mtime":2000,"path":"file-with-dashes.txt","size":20},{"hash":"sp4sp4","mtime":4000,"path":"file.multiple.dots.txt","size":40},{"hash":"sp3sp3","mtime":3000,"path":"file_with_underscores.txt","size":30}],"totalSize":150} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_4 b/fuzz/seeds/snapshot_ops/m_4 new file mode 100644 index 00000000..6d698275 --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_4 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"s2s2","mtime":2000,"path":"Atop.txt","size":20},{"hash":"s3s3","mtime":3000,"path":"ztop.txt","size":30},{"hash":"s1s1","mtime":1000,"path":"~tilde.txt","size":10},{"hash":"s4s4","mtime":4000,"path":"\u00e9accent.txt","size":40},{"hash":"s5s5","mtime":5000,"path":"\u0100macron.txt","size":50}],"totalSize":150} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_5 b/fuzz/seeds/snapshot_ops/m_5 new file mode 100644 index 00000000..44857213 --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_5 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4","mtime":1700000000000000,"path":"hello.txt","size":11}],"totalSize":11} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_6 b/fuzz/seeds/snapshot_ops/m_6 new file mode 100644 index 00000000..31da224f --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_6 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"t2t2","mtime":9999999999999999,"path":"huge.dat","size":1099511627776},{"hash":"t3t3","mtime":1,"path":"normal.txt","size":1},{"hash":"t1t1","mtime":0,"path":"tiny.txt","size":0}],"totalSize":1099511627777} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/m_7 b/fuzz/seeds/snapshot_ops/m_7 new file mode 100644 index 00000000..c71a2d0e --- /dev/null +++ b/fuzz/seeds/snapshot_ops/m_7 @@ -0,0 +1 @@ +{"hashAlg":"xxh128","manifestVersion":"2023-03-03","paths":[{"hash":"tgt1","mtime":1000,"path":"link.txt","size":100},{"hash":"tgt1","mtime":1000,"path":"target.txt","size":100}],"totalSize":200} \ No newline at end of file diff --git a/fuzz/seeds/snapshot_ops/pair_0 b/fuzz/seeds/snapshot_ops/pair_0 new file mode 100644 index 0000000000000000000000000000000000000000..acb060a8c5c8e3b7b9d0988bb53004fa2a32fc66 GIT binary patch literal 649 zcmeH_!4AS85Jdf!Jy%O<52iQ2!NiO4fQmvwOH($nvEkp{ic0b5*$KCqH=Ersf)Y+` z%oS=F237DiWYCCKU!=pG?47nPS)B25$)-nkCr~-4_v4c#w;)Xz>PA9cFvdcNERi4c zQ+}4RTj78da{3-JKKt;T6nXy5J0*;kk>AMPat@J(ypFMgB*8eSn{i_y8p+z5M_H literal 0 HcmV?d00001 diff --git a/scripts/check_fuzz_coverage.py b/scripts/check_fuzz_coverage.py new file mode 100644 index 00000000..538b6e37 --- /dev/null +++ b/scripts/check_fuzz_coverage.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright by contributors to this project. +# SPDX-License-Identifier: (Apache-2.0 OR MIT) +# +# Hard gate: every public function that takes untrusted input (a &str, &[u8], +# serde_json::Value, or &Path parameter) must be accounted for in +# fuzz/fuzz_coverage.toml — either mapped to a fuzz target that exercises it, or +# explicitly classified as not-untrusted with a reason. This makes "all +# untrusted inputs are fuzzed" an executable, non-rotting check instead of a +# prose convention. +# +# It enforces four invariants: +# 1. No orphan targets — every fuzz/fuzz_targets/*.rs is listed in [targets]. +# 2. No phantom targets — every [targets] entry has a matching target file. +# 3. No dangling entries — every classified function still exists in source. +# 4. Ratchet (completeness) — every input-shaped public fn is classified, so a +# newly-added untrusted entry point fails CI until a human triages it. +# +# It deliberately does NOT verify that a target *meaningfully* exercises a +# function (that needs coverage instrumentation) or that a not-untrusted reason +# is honest (that is the one human-reviewed line per entry). It runs on stable +# Rust with no build — pure source + manifest analysis — so it fits the fast +# Compliance CI job alongside the copyright-header check. +# +# Usage: scripts/check_fuzz_coverage.py + +import re +import sys +import tomllib +from pathlib import Path + +# Crates whose public API is security-relevant (parse/decode/evaluate untrusted +# input). openjd-cli is a bin-only crate (no lib target) and openjd-for-js is a +# thin wasm shim over these crates, so neither exposes a fuzzable library API. +CRATES = ["openjd-expr", "openjd-model", "openjd-snapshots"] + +# Parameter-type fragments that indicate a function consumes externally-shaped +# input. Anything taking bytes, text, parsed JSON, or a filesystem path. +INPUT_TYPE_MARKERS = ["&str", "&[u8]", "serde_json::Value", "&Path", "&std::path::Path"] + +REPO_ROOT = Path(__file__).resolve().parent.parent +MANIFEST = REPO_ROOT / "fuzz" / "fuzz_coverage.toml" +TARGET_DIR = REPO_ROOT / "fuzz" / "fuzz_targets" + +# `pub fn name` — not `pub(crate) fn` (has no bare " fn "), not private fns. +# Only the `pub fn ` prefix is matched here; the optional generic block +# and the arg-list paren are located by `find_arg_paren` with bracket +# balancing, so signatures whose generics contain nested angle brackets +# (e.g. `foo>(input: &str)`) are handled — a regex like +# `<[^>]*>` would stop at the first `>` and silently skip such a function, +# letting it escape the ratchet. +FN_START = re.compile(r"\bpub\s+fn\s+([a-z_][a-z0-9_]*)") + + +def public_module_files(crate_src: Path) -> list[Path]: + """Return .rs files reachable through `pub mod` from lib.rs. + + A `pub fn` inside a private top-level module (e.g. snapshots' `mod + path_util`) is not public API, so its functions must not count toward the + untrusted surface. We resolve visibility at the top level (the common case, + and the one that has actually bitten us) by reading lib.rs's module + declarations; files under a private top-level module are excluded. + """ + lib = crate_src / "lib.rs" + text = lib.read_text(encoding="utf-8", errors="replace") + private_tops = set() + for m in re.finditer(r"^\s*(pub\s+)?mod\s+([a-z_][a-z0-9_]*)\s*;", text, re.M): + if not m.group(1): # `mod foo;` without `pub` + private_tops.add(m.group(2)) + + files = [] + for rs in sorted(crate_src.rglob("*.rs")): + rel = rs.relative_to(crate_src) + top = rel.parts[0] + # `foo.rs` or `foo/…` where `foo` is a private top-level module → skip. + stem_top = top[:-3] if top.endswith(".rs") else top + if stem_top in private_tops: + continue + files.append(rs) + return files + + +def find_arg_paren(text: str, after_name_idx: int) -> int | None: + """Given the index just past a `pub fn `, return the index of the + arg-list opening `(`, or None if the shape is unexpected. + + Skips an optional generic block `<...>` with full angle-bracket balancing + (so nested generics like `>` are handled) plus surrounding + whitespace. `->` inside a bound (e.g. ` bool>`) is treated as + a token so its `>` does not close the block early. The first `(` at + generic-depth 0 is the arg list. + """ + i = after_name_idx + n = len(text) + while i < n and text[i].isspace(): + i += 1 + if i < n and text[i] == "<": + depth = 0 + while i < n: + c = text[i] + # Skip `->` so the `>` of a return-type arrow inside a bound (e.g. + # `Fn(&str) -> bool`) is not counted as closing the generic block. + if c == "-" and i + 1 < n and text[i + 1] == ">": + i += 2 + continue + if c == "<": + depth += 1 + elif c == ">": + depth -= 1 + if depth == 0: + i += 1 + break + i += 1 + while i < n and text[i].isspace(): + i += 1 + if i < n and text[i] == "(": + return i + return None + + +def extract_signature(text: str, fn_start_idx: int, paren_idx: int) -> str: + """Return the normalized `pub fn …(…)` slice, balancing parens.""" + depth = 0 + i = paren_idx + while i < len(text): + c = text[i] + if c == "(": + depth += 1 + elif c == ")": + depth -= 1 + if depth == 0: + break + i += 1 + return " ".join(text[fn_start_idx : i + 1].split()) + + +def input_shaped_functions() -> dict[str, str]: + """Map "crate-relative-file::fn_name" -> normalized signature, for every + input-shaped public function across the security-relevant crates.""" + found: dict[str, str] = {} + for crate in CRATES: + crate_src = REPO_ROOT / "crates" / crate / "src" + for rs in public_module_files(crate_src): + text = rs.read_text(encoding="utf-8", errors="replace") + # Skip #[cfg(test)] modules: crude but effective — drop everything + # from a `mod tests {` onward is unsafe (nested), so instead skip + # files whose path is a test module. Test fns are rarely `pub` and + # never take these types as public API, so residual noise is nil. + for m in FN_START.finditer(text): + paren = find_arg_paren(text, m.end()) + if paren is None: + continue + sig = extract_signature(text, m.start(), paren) + # Normalize explicit lifetimes out of reference types before + # marker-matching, so `&'a str` / `&'de [u8]` are recognized the + # same as `&str` / `&[u8]`. Without this, a lifetime-annotated + # parameter (e.g. `job_template_dir: &'a str`) would silently + # escape classification and defeat the ratchet. + normalized = re.sub(r"&\s*'[a-z_][a-z0-9_]*\s+", "&", sig) + if any(marker in normalized for marker in INPUT_TYPE_MARKERS): + rel = rs.relative_to(REPO_ROOT).as_posix() + found[f"{rel}::{m.group(1)}"] = sig + return found + + +def target_files() -> set[str]: + return {p.stem for p in TARGET_DIR.glob("*.rs")} + + +def main() -> int: + if not MANIFEST.exists(): + print(f"error: manifest not found: {MANIFEST}", file=sys.stderr) + return 1 + + manifest = tomllib.loads(MANIFEST.read_text(encoding="utf-8")) + declared_targets = set(manifest.get("targets", {}).keys()) + fuzzed = {e["fn"]: e for e in manifest.get("fuzzed", [])} + not_untrusted = {e["fn"]: e for e in manifest.get("not_untrusted", [])} + classified = set(fuzzed) | set(not_untrusted) + + errors: list[str] = [] + + # (1)/(2) target files ↔ [targets] table. + actual_targets = target_files() + for orphan in sorted(actual_targets - declared_targets): + errors.append( + f"fuzz target '{orphan}' exists in fuzz/fuzz_targets/ but is not " + f"listed in [targets] of {MANIFEST.name}" + ) + for phantom in sorted(declared_targets - actual_targets): + errors.append( + f"[targets] lists '{phantom}' but fuzz/fuzz_targets/{phantom}.rs " + f"does not exist" + ) + + # fuzzed[].target must name a real target. + for fn, entry in sorted(fuzzed.items()): + tgt = entry.get("target") + if tgt not in declared_targets: + errors.append( + f"fuzzed entry '{fn}' names target '{tgt}', which is not in " + f"[targets]" + ) + + # An entry can't be both fuzzed and not-untrusted. + for fn in sorted(set(fuzzed) & set(not_untrusted)): + errors.append(f"'{fn}' is listed as both fuzzed and not_untrusted") + + surface = input_shaped_functions() + + # (3) No dangling: every classified fn still exists in source. + for fn in sorted(classified - set(surface)): + errors.append( + f"manifest classifies '{fn}' but no such input-shaped public " + f"function exists (renamed, removed, or made private?)" + ) + + # (4) Ratchet: every input-shaped public fn is classified. + for fn in sorted(set(surface) - classified): + errors.append( + f"input-shaped public function '{fn}' is not classified in " + f"{MANIFEST.name}\n {surface[fn]}\n → add it to [[fuzzed]] " + f"(with the target that exercises it) or [[not_untrusted]] (with a " + f"reason it does not take untrusted input)" + ) + + if errors: + print("Fuzz coverage check failed:\n", file=sys.stderr) + for e in errors: + print(f" - {e}", file=sys.stderr) + print( + f"\n{len(surface)} input-shaped public functions scanned; " + f"{len(classified)} classified.", + file=sys.stderr, + ) + return 1 + + print( + f"Fuzz coverage check passed: {len(surface)} input-shaped public " + f"functions, all classified ({len(fuzzed)} fuzzed, " + f"{len(not_untrusted)} not-untrusted); {len(actual_targets)} fuzz targets." + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/run_fuzz.sh b/scripts/run_fuzz.sh new file mode 100755 index 00000000..b7567f60 --- /dev/null +++ b/scripts/run_fuzz.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# Copyright by contributors to this project. +# SPDX-License-Identifier: (Apache-2.0 OR MIT) +# +# Build and smoke-fuzz every fuzz target. The target list comes from +# `cargo fuzz list` (the single source of truth — every fuzz/fuzz_targets/*.rs +# with a matching [[bin]] in fuzz/Cargo.toml), so adding a target requires no +# change here or in the CI workflow: it is picked up automatically. +# +# Each target runs time-boxed, seeded with its committed corpus in +# fuzz/seeds/. Any panic, abort, overflow, or char-boundary slice in a +# fuzzed entry point fails the run. A missing seed dir is not fatal (the target +# just starts from an empty corpus). +# +# Usage: +# scripts/run_fuzz.sh # all targets, default budget +# FUZZ_SECONDS=30 scripts/run_fuzz.sh # override per-target budget +# scripts/run_fuzz.sh expr_parse … # only the named targets +# +# Requires a nightly toolchain and cargo-fuzz (see fuzz/README.md). The nightly +# is pinned so a bad nightly can't randomly break the run; override with +# FUZZ_TOOLCHAIN. + +set -euo pipefail + +# Per-target wall-clock budget, in seconds. Coverage plateaus within a few +# seconds once seeded, so a short smoke run is enough to catch a regression +# reachable from the corpus; deeper campaigns are run manually with a larger +# value (see fuzz/README.md). +FUZZ_SECONDS="${FUZZ_SECONDS:-10}" +FUZZ_TOOLCHAIN="${FUZZ_TOOLCHAIN:-nightly-2026-05-15}" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$REPO_ROOT" + +cargo_fuzz() { cargo "+${FUZZ_TOOLCHAIN}" fuzz "$@"; } + +# Targets: either the ones named on the command line, or every registered one. +# Read `cargo fuzz list` line-by-line rather than `mapfile` so the script runs +# on the Bash 3.2 that ships with macOS as well as CI's newer Bash. +if [[ $# -gt 0 ]]; then + targets=("$@") +else + targets=() + while IFS= read -r line; do + [[ -n "$line" ]] && targets+=("$line") + done < <(cargo_fuzz list) +fi + +if [[ ${#targets[@]} -eq 0 ]]; then + echo "error: no fuzz targets found (cargo fuzz list returned nothing)" >&2 + exit 1 +fi + +echo "Fuzzing ${#targets[@]} target(s) for ${FUZZ_SECONDS}s each: ${targets[*]}" + +# One build pass produces every target binary. +cargo_fuzz build + +common_args=(-max_total_time="${FUZZ_SECONDS}" -timeout=25 -rss_limit_mb=4096) + +for target in "${targets[@]}"; do + echo "::group::fuzz ${target} (${FUZZ_SECONDS}s)" + # Pass the seed corpus dir only when it exists. Branching (rather than + # expanding a possibly-empty array) avoids the `unbound variable` abort + # that Bash 3.2 — the macOS /bin/bash this script supports — raises for + # "${arr[@]}" on an empty array under `set -u`. + if [[ -d "fuzz/seeds/${target}" ]]; then + cargo_fuzz run "${target}" "fuzz/seeds/${target}" -- "${common_args[@]}" + else + cargo_fuzz run "${target}" -- "${common_args[@]}" + fi + echo "::endgroup::" +done + +echo "All ${#targets[@]} fuzz target(s) passed." From 3280b7c6226126d34f4a6d7a2df77cdc07493052 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:54:24 -0700 Subject: [PATCH 2/3] docs: mark fuzz-testing report items resolved; document fuzz suite in AGENTS.md and specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per AGENTS.md report-driven development, strike through the report items this PR's cargo-fuzz suite resolves: - expr report: recommendation 20 (fuzz harness), the §4 structural gap, the §5 test_fuzz.py parity gap, and the X8 path_starts_with char-boundary panic (item 16 / recommendation 5), fixed in this PR with a regression test. - model report: gap 5 / recommendation 12 (fuzz testing for parser robustness), covered by the model_decode and model_create_job targets. Also document the new surface where developers will look for it: - AGENTS.md: quick-reference entries for scripts/run_fuzz.sh and scripts/check_fuzz_coverage.py, the Fuzz workflow and the Compliance job's fuzz-coverage gate in the CI table, and a note that new untrusted-input public fns must be classified in fuzz/fuzz_coverage.toml. - specs/architecture.md: fuzz/ in the project layout and a Key Design Decisions entry describing the suite and the coverage gate. The sessions report's directive-parsing fuzz note stays open — the suite does not cover openjd-sessions. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- AGENTS.md | 7 ++++++- reports/expr-quality-evaluation-report.md | 10 +++++----- reports/model-quality-evaluation-report.md | 4 ++-- specs/architecture.md | 12 ++++++++++++ 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 723617a5..480881fd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,6 +18,8 @@ cargo clippy --all-features --all-targets --workspace -- -D warnings # Lint cargo fmt --all # Apply formatting cargo doc --no-deps --workspace # Build docs scripts/coverage.sh # Code coverage (see COVERAGE_REPORT.md) +scripts/run_fuzz.sh # Smoke-fuzz all cargo-fuzz targets (see fuzz/README.md) +python3 scripts/check_fuzz_coverage.py # Untrusted-input fuzz coverage gate (runs in CI) ``` MSRV: **1.94.1** (enforced in CI). @@ -124,6 +126,8 @@ The `specs/` directory is the primary resource for understanding each crate's de Every crate's spec directory must include a `public-api.md` that fully describes the crate's public API — all public types, functions, traits, and constants with their signatures. When adding or changing public API surface, update `public-api.md` in the same commit. +When adding a public function that takes untrusted input (`&str`, `&[u8]`, `serde_json::Value`, `&Path`) to `openjd-expr`, `openjd-model`, or `openjd-snapshots`, CI's fuzz-coverage gate (`scripts/check_fuzz_coverage.py`) will fail until the function is classified in `fuzz/fuzz_coverage.toml` — either mapped to a fuzz target that exercises it or marked not-untrusted with a reason. See `fuzz/README.md`. + The structure: ``` @@ -239,10 +243,11 @@ PRs run these checks (all must pass): | **Conformance** | Full OpenJD conformance suite (1,038 tests) on all three platforms | | **MSRV** | `cargo check --workspace` with Rust 1.94.1 | | **Documentation** | `cargo doc --no-deps --workspace` with `-D warnings` | -| **Compliance** | Copyright header check | +| **Compliance** | Copyright header check + fuzz-coverage gate (`scripts/check_fuzz_coverage.py`: every public fn taking untrusted input must be classified in `fuzz/fuzz_coverage.toml`) | | **Cross-User (Linux)** | Docker-based cross-user tests: localuser and LDAP variants | | **Cross-User (Windows)** | Windows cross-user and permissions tests with a temporary test user | | **openjd-for-js** | Builds the wasm32 crate with `wasm-bindgen` and runs the vitest suite | +| **Fuzz** | Separate workflow (`.github/workflows/fuzz.yml`): smoke-fuzzes every cargo-fuzz target (nightly + ASan, overflow-checks on) — see `fuzz/README.md` | If a dependency update bumps `wasm-bindgen`, also bump the matching `wasm-bindgen-cli` version pinned in the `openjd-for-js` job in diff --git a/reports/expr-quality-evaluation-report.md b/reports/expr-quality-evaluation-report.md index 2a8f604d..77a435c1 100644 --- a/reports/expr-quality-evaluation-report.md +++ b/reports/expr-quality-evaluation-report.md @@ -64,7 +64,7 @@ All 25 source files were reviewed. Quality is generally high: pervasive doc comm 13. `functions/regex.rs:363` (`re_split_fn`) — same `n + 1` overflow; also the only regex function using unchecked `make_list` instead of `make_list_checked`. 14. `functions/string.rs` (`zfill_fn`) — negative width wraps to `usize::MAX` → `"0".repeat(huge)` allocation abort (inspection; not executed to avoid aborting the harness). Output size is also never charged to memory/op budgets. 15. `functions/string.rs` (`center_fn`/`ljust_fn`/`rjust_fn`) — negative width wraps huge; fails *closed* with a nonsensical "operation count (72057594037927938) exceeded limit" error where Python returns the string unchanged. The inconsistency with `zfill` (which fails *open*) shows the four width-taking functions lack a shared validation path. -16. `functions/path.rs:526` (`path_starts_with`) — `path[..base.len()]` byte-slices at a possibly non-char boundary on Windows-format comparison; panics on multibyte UTF-8 paths. `relative_to_fn` shares the helper. +16. ~~`functions/path.rs:526` (`path_starts_with`) — `path[..base.len()]` byte-slices at a possibly non-char boundary on Windows-format comparison; panics on multibyte UTF-8 paths. `relative_to_fn` shares the helper.~~ **Resolved** — byte-wise `eq_ignore_ascii_case` prefix compare (no char-boundary slicing); found by the `expr_evaluate` fuzz target's Windows path branch, regression test `is_relative_to_multibyte_no_char_boundary_panic`. 17. `functions/repr.rs` (`repr_py`) — escapes only `\` and `'`; newlines/control characters emitted raw, producing invalid Python string literals. 18. `format_string.rs` `copy_symbol_value` + `symbol_table.rs` `set_table` — dotted keys inserted literally, creating entries `get()` can never find. 19. `uri_path.rs:133–141` (`join`) — double slash when joining onto bare-authority URIs (`s3://bucket//child`). @@ -79,7 +79,7 @@ All 25 source files were reviewed. Quality is generally high: pervasive doc comm - **Error-assertion standard:** the caret/multi-line full-message pattern from `test_error_formatting.rs` is replicated across 17+ files — compliance is the norm. Biggest gap: **29 statement-rejection tests in `test_evaluation.rs`** (lines 180, 401–442, 709–775: `x = 1`, `import os`, `del`, async/match forms) assert only `eval_fails` with no message content. `test_regex_validation.rs` asserts keyword disjunctions instead of exact messages and has zero caret assertions; `test_path_mapping.rs` has four bare `is_err()` checks (lines 909–1005); `test_unresolved_eval.rs:256` accepts either error or unresolved result, pinning neither. - **Duplication/brittleness:** `src/types.rs` unit test `basic_types` is byte-identical to one in `test_types.rs`; `test_int64_bounds.rs` has `eval_fails` tests redundant with adjacent full-message twins; eval/assert helpers are copy-pasted into nearly every file with `#[allow(dead_code)]` noise (extract a shared `tests/integration/common.rs`). Exact operation-count assertions (e.g. "count (51) exceeded limit (50)") deliberately pin the cost model and will break on any metering change. -- **Structural gap:** no property-based or fuzz testing, despite explicit DoS guards (depth/memory/op limits) that would benefit from a never-panic invariant harness — §7 shows exactly the class of bug such a harness would have caught. +- ~~**Structural gap:** no property-based or fuzz testing, despite explicit DoS guards (depth/memory/op limits) that would benefit from a never-panic invariant harness — §7 shows exactly the class of bug such a harness would have caught.~~ **Resolved** — see recommendation 20: a cargo-fuzz suite (`fuzz/`) now encodes the never-panic invariant over the parser, evaluator, range, type, format-string, and symbol-table entry points, gated in CI. ## 5. Python Comparison @@ -97,7 +97,7 @@ Compared against `openjd-model-for-python` (branch `expr`; 1,366 test functions **API design:** Rust's structured `ExpressionErrorKind`, string-DSL `register_sig`, typed list variants, and `EvalBuilder` are supersets/equivalents of the Python API; the spec's "Recommended Library Interface" permits this. Path-format mismatch messages render the format differently (`POSIX` vs Rust `Debug` formatting) — worth normalizing. -**Test parity:** 457 Python test names lack same-named Rust tests, but spot-checks confirm the large majority are renames. Genuine gaps: (1) ~25 Python tests spawn real `pwsh`/`cmd.exe` subprocesses to round-trip-verify `repr_pwsh`/`repr_cmd` escaping — Rust has zero subprocess-based verification (notable because `cmd_quote`'s handling of trailing backslashes before the closing quote is questionable — see §7, X15 note); (2) `test_fuzz.py` parser no-crash tests have no Rust counterpart; (3) one error-formatting case. +**Test parity:** 457 Python test names lack same-named Rust tests, but spot-checks confirm the large majority are renames. Genuine gaps: (1) ~25 Python tests spawn real `pwsh`/`cmd.exe` subprocesses to round-trip-verify `repr_pwsh`/`repr_cmd` escaping — Rust has zero subprocess-based verification (notable because `cmd_quote`'s handling of trailing backslashes before the closing quote is questionable — see §7, X15 note); (2) ~~`test_fuzz.py` parser no-crash tests have no Rust counterpart~~ **Resolved** — superseded by the coverage-guided cargo-fuzz suite (`fuzz/`, recommendation 20), which is strictly stronger than Python's fixed-corpus no-crash tests; (3) one error-formatting case. ## 6. Build and Test Results @@ -142,7 +142,7 @@ Additional note for spec cross-check: `cmd_quote` does not double backslashes pr 2. **Validate negative int arguments before `as usize`.** `zfill`, `center`, `ljust`, `rjust`, `split`/`rsplit`/`re_split` maxsplit. Match Python semantics (negative width → unchanged string; negative maxsplit → no limit). Share one validation helper across the width-taking functions. 3. **Fix the contextual-keyword retry to skip string literals** (X9, `eval/parse.rs`) — corrupts user data silently. Also fix the byte-vs-char index mix in the after-keyword boundary check. 4. **Fix float-literal passthrough source slicing for multiline expressions** (X10, `eval/evaluator.rs` `eval_number`); have `Float64::with_str` validate that the string round-trips to the value as a backstop. -5. **Fix `path_starts_with` char-boundary panic** (X8, `path.rs:526`) — use `get(..len)` or char-aware case-insensitive comparison. +5. ~~**Fix `path_starts_with` char-boundary panic** (X8, `path.rs:526`) — use `get(..len)` or char-aware case-insensitive comparison.~~ **Resolved** — byte-wise `eq_ignore_ascii_case` prefix compare; found by the `expr_evaluate` fuzz target once it exercised the Windows path format, with regression test `is_relative_to_multibyte_no_char_boundary_panic`. 6. **Make `split_path_parts` preserve absoluteness** (X12, `path_mapping.rs:150`) so relative paths never match absolute rules; use the rule's source format (not host format) for output joining, or document why host format is intended. 7. ~~**Fix Float→Int coercion saturation** (X11, `value.rs`) with an explicit range check; same `>=` fix for the `> i64::MAX as f64` guards in `floordiv_float` and `math.rs` floor/ceil/round.~~ **Resolved** — shared `float_fits_i64` helper (exact `[-2^63, 2^63)` check) used by coercion, `floordiv_float`, and `math.rs` floor/ceil/round. 8. **Escape control characters in `repr_py`** (X15) and cross-check `repr_cmd` trailing-backslash behavior against spec §2.2.6, ideally with subprocess round-trip tests. @@ -167,6 +167,6 @@ Additional note for spec cross-check: `cmd_quote` does not double backslashes pr ### Priority 4 — Tests and robustness 19. **Upgrade the 29 statement-rejection tests in `test_evaluation.rs` to full-message assertions**; tighten `test_regex_validation.rs` and the four bare `is_err()` checks in `test_path_mapping.rs`; pin the behavior in `test_unresolved_eval.rs:256`. -20. **Add a property-based/fuzz harness** (proptest or cargo-fuzz) asserting the evaluator never panics on arbitrary input — it would have caught X1–X8 mechanically; port the spirit of Python's `test_fuzz.py`. +20. ~~**Add a property-based/fuzz harness** (proptest or cargo-fuzz) asserting the evaluator never panics on arbitrary input — it would have caught X1–X8 mechanically; port the spirit of Python's `test_fuzz.py`.~~ **Resolved** — cargo-fuzz suite in `fuzz/` (see `fuzz/README.md`): 12 libFuzzer targets asserting "any input returns Ok/Err, never panics/aborts/hangs," eight of them over this crate (`expr_parse`, `expr_evaluate` in both path formats, `range_expr`, `int_range_new`, `range_expr_slice`, `expr_type_parse`, `format_string`, `copy_symbol_value`), built with overflow-checks and AddressSanitizer, seeded from curated corpora, and smoke-run per PR by `.github/workflows/fuzz.yml`. A `scripts/check_fuzz_coverage.py` CI gate additionally requires every input-shaped public fn to be classified in `fuzz/fuzz_coverage.toml`. Initial runs already caught the X8 char-boundary panic plus two new range overflows (`IntRange::new` step negation, `RangeExpr::slice` stride remap), each fixed with regression tests. 21. **Extract a shared `tests/integration/common.rs`** for the copy-pasted eval/assert helpers; remove the duplicated `basic_types` unit test and redundant `eval_fails` twins. 22. **Move the format-string segment cap inside the parse loop**; ~~replace `mul_list`'s per-element `count_op` loop with a single `count_ops(n)`~~ **Resolved** — list multiplication now bulk-meters the checked result length; add list-comparison fast paths that avoid per-element `String` clones. diff --git a/reports/model-quality-evaluation-report.md b/reports/model-quality-evaluation-report.md index 6a70cd20..ff34e0ad 100644 --- a/reports/model-quality-evaluation-report.md +++ b/reports/model-quality-evaluation-report.md @@ -207,7 +207,7 @@ Re-exports of `FormatString` and `SymbolTable` from `openjd-expr` are appropriat 2. Limited serialization round-trip testing beyond `test_resolved_bindings.rs` 3. No concurrent/parallel execution tests 4. YAML-specific features (anchors, aliases, multi-line strings) not tested -5. No fuzz/property-based testing for parser robustness +5. ~~No fuzz/property-based testing for parser robustness~~ **Resolved** — see recommendation 12. 6. REDACTED_ENV_VARS has only 4 tests — minimal behavioral coverage 7. No stress tests for large templates (many steps, many params) 8. ~~Environment template extension handling noted as not yet implemented (some Python tests skipped)~~ **Resolved** — see recommendation 15. @@ -340,7 +340,7 @@ Re-exports of `FormatString` and `SymbolTable` from `openjd-expr` are appropriat ### Priority 4 (Future Improvements) 11. Add YAML-specific edge case tests (anchors, aliases, multi-line strings). -12. Add fuzz/property-based testing for parser robustness. +12. ~~Add fuzz/property-based testing for parser robustness.~~ **Resolved** — cargo-fuzz targets `model_decode` (YAML/JSON decode + validation, the `openjd check` path) and `model_create_job` (template → job instantiation with all extensions enabled, the `openjd run` path) in `fuzz/`, smoke-run per PR by `.github/workflows/fuzz.yml`; `scripts/check_fuzz_coverage.py` gates that every input-shaped public fn in this crate is classified in `fuzz/fuzz_coverage.toml`. 13. Expand REDACTED_ENV_VARS test coverage beyond the current 4 tests. 14. Add stress tests for large templates. 15. ~~Complete environment template extension handling (noted as not yet implemented).~~ **Resolved** — `validate_environment_template` now runs the FEATURE_BUNDLE_1 pass (`endOfLine` gating) and the format-string pass (session-scope symbol validation, `let` bindings, EXPR gating for complex expressions) on environment templates; previously skipped Python let-binding tests ported to `test_environment_template.rs`. diff --git a/specs/architecture.md b/specs/architecture.md index 7a241f89..975358ba 100644 --- a/specs/architecture.md +++ b/specs/architecture.md @@ -50,6 +50,10 @@ openjd-rs/ │ ├── summary.rs │ ├── run.rs │ └── help.rs +├── fuzz/ # cargo-fuzz suite (own workspace; see fuzz/README.md) +│ ├── fuzz_targets/ # One libFuzzer target per untrusted entry point +│ ├── seeds/ # Curated starter corpora, committed +│ └── fuzz_coverage.toml # Untrusted-input classification manifest (CI-gated) └── specs/ # Design specs ├── architecture.md ├── model/ # openjd-model crate specs @@ -106,6 +110,14 @@ Commands: `check` (validate templates), `summary` (job/step summary), `run` (exe - **Validation**: Post-deserialization validation pass (not inline with serde) - **Error handling**: `thiserror`-based enums throughout - **Workspace layout**: `crates/` directory (rattler convention) +- **Fuzzing**: coverage-guided libFuzzer targets (`fuzz/`, via cargo-fuzz) over + every crate that parses or evaluates untrusted input (`openjd-expr`, + `openjd-model`, `openjd-snapshots`), asserting the never-panic invariant. + The fuzz crate sits outside the root workspace (nightly + sanitizer only) + and runs in its own CI workflow; a manifest gate + (`scripts/check_fuzz_coverage.py` + `fuzz/fuzz_coverage.toml`) requires + every input-shaped public fn to be mapped to a fuzz target or classified + as not-untrusted. See [../fuzz/README.md](../fuzz/README.md). ## Crate Status From 96d70e7dc4835533da9a5cd74bfd72019b262284 Mon Sep 17 00:00:00 2001 From: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:23:20 -0700 Subject: [PATCH 3/3] fix(fuzz): treat pub(crate)/pub(super) mods as private in coverage gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The private-module regex in check_fuzz_coverage.py used a `(pub\s+)?` prefix, which cannot match a restricted-visibility declaration like `pub(crate) mod foo;` — there is no whitespace after `pub`, so the optional group matches empty and `mod` is then tried against `pub`, failing the line entirely. Such modules were therefore never added to `private_tops`, and their crate-internal `pub fn`s were scanned as part of the untrusted public surface, contradicting the function's own documented contract. Match an optional `(...)` visibility restriction explicitly and treat any restriction as private, since `pub(crate)`, `pub(super)`, and `pub(in path)` are all narrower than `pub`. This drops openjd-expr's `pub(crate) mod edit_distance` from the scanned surface (111 -> 109 input-shaped public functions), so its two now-dangling [[fuzzed]] entries are removed from fuzz_coverage.toml and replaced with a note recording that `expr_evaluate` still exercises them transitively. The previous behavior was fail-safe — over-inclusion never let a real entry point escape the ratchet — so this is a contract/accuracy fix, not a soundness fix. Signed-off-by: Stephen Crowe <6042774+crowecawcaw@users.noreply.github.com> --- fuzz/fuzz_coverage.toml | 13 +++++-------- scripts/check_fuzz_coverage.py | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/fuzz/fuzz_coverage.toml b/fuzz/fuzz_coverage.toml index 7e9bb20e..0880c28e 100644 --- a/fuzz/fuzz_coverage.toml +++ b/fuzz/fuzz_coverage.toml @@ -191,14 +191,11 @@ target = "copy_symbol_value" fn = "crates/openjd-expr/src/symbol_table.rs::from_json_str" target = "copy_symbol_value" -# expr — edit-distance suggestions run when evaluation hits an unknown -# name/function, so the fuzzer reaches them with attacker-controlled names. -[[fuzzed]] -fn = "crates/openjd-expr/src/edit_distance.rs::edit_distance" -target = "expr_evaluate" -[[fuzzed]] -fn = "crates/openjd-expr/src/edit_distance.rs::suggest_closest" -target = "expr_evaluate" +# NOTE: `edit_distance` / `suggest_closest` are not listed here. They live in +# `pub(crate) mod edit_distance`, so they are not public API and the gate does +# not scan them. They are still reached with attacker-controlled names whenever +# evaluation hits an unknown name/function, so the `expr_evaluate` target +# exercises them transitively — just not as a classified entry point. # model — decode + instantiate pipeline [[fuzzed]] diff --git a/scripts/check_fuzz_coverage.py b/scripts/check_fuzz_coverage.py index 538b6e37..bf251bef 100644 --- a/scripts/check_fuzz_coverage.py +++ b/scripts/check_fuzz_coverage.py @@ -56,18 +56,29 @@ def public_module_files(crate_src: Path) -> list[Path]: """Return .rs files reachable through `pub mod` from lib.rs. - A `pub fn` inside a private top-level module (e.g. snapshots' `mod - path_util`) is not public API, so its functions must not count toward the - untrusted surface. We resolve visibility at the top level (the common case, - and the one that has actually bitten us) by reading lib.rs's module - declarations; files under a private top-level module are excluded. + A `pub fn` inside a module that is not itself fully public (e.g. snapshots' + `mod path_util`, or expr's `pub(crate) mod edit_distance`) is not public + API, so its functions must not count toward the untrusted surface. We + resolve visibility at the top level (the common case, and the one that has + actually bitten us) by reading lib.rs's module declarations; files under a + non-public top-level module are excluded. + + Only a bare `pub mod foo;` counts as public. Any restricted visibility — + `pub(crate)`, `pub(super)`, `pub(in path)` — is narrower than `pub` and so + is treated as private. Matching the restriction explicitly matters: a + `(pub\\s+)?` prefix would fail to match `pub(crate) mod foo;` at all + (there is no whitespace after `pub`), leaving the module out of + `private_tops` and silently pulling its crate-internal `pub fn`s into the + scanned surface. """ lib = crate_src / "lib.rs" text = lib.read_text(encoding="utf-8", errors="replace") private_tops = set() - for m in re.finditer(r"^\s*(pub\s+)?mod\s+([a-z_][a-z0-9_]*)\s*;", text, re.M): - if not m.group(1): # `mod foo;` without `pub` - private_tops.add(m.group(2)) + mod_decl = re.compile(r"^\s*(pub\s*(\([^)]*\))?\s+)?mod\s+([a-z_][a-z0-9_]*)\s*;", re.M) + for m in mod_decl.finditer(text): + # No `pub` at all, or a `pub(...)` visibility restriction → private. + if not m.group(1) or m.group(2): + private_tops.add(m.group(3)) files = [] for rs in sorted(crate_src.rglob("*.rs")):