diff --git a/.github/workflows/wai-byte-equality.yml b/.github/workflows/wai-byte-equality.yml index 6f0f8ec05..34ef56ee6 100644 --- a/.github/workflows/wai-byte-equality.yml +++ b/.github/workflows/wai-byte-equality.yml @@ -15,7 +15,9 @@ name: wai cross-architecture byte-equality # and the MLIC++-topology codec (`wai.neural.int_mlicpp`), whose entropy network runs # before every checkerboard pass and must give every platform the encoder's scale # bucket for every element. The binary payloads SPEC §5 pins (WIV1, WIH1) are parsed -# with the wire parser a sink runs and decoded to the same digests. Video is the strictest leg — inter-frame decode is a +# with the wire parser a sink runs and decoded to the same digests, and the staged-delivery +# residual layers (WIR1) of the staged corpus are applied stage by stage to pinned digests. +# Video is the strictest leg — inter-frame decode is a # feedback loop, so one divergent half-pel rounding does not stay one byte, it # predicts the next frame and compounds for the rest of the clip. # @@ -40,6 +42,10 @@ on: - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_mlicpp.rs' - 'wai/wai-rs/src/int_payload/wire.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' + - 'wai/staged-conformance/**' # The fast kernels int_synth, int_transform and int_hyper_synth dispatch to # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' @@ -65,6 +71,10 @@ on: - 'wai/wai-rs/src/int_hyper_synth.rs' - 'wai/wai-rs/src/int_mlicpp.rs' - 'wai/wai-rs/src/int_payload/wire.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' + - 'wai/staged-conformance/**' # The fast kernels int_synth, int_transform and int_hyper_synth dispatch to # by default: the goldens here decode through them. - 'wai/wai-rs/src/int_kernels.rs' diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 4dbad796a..823da2153 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -33,6 +33,9 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + # A library test (staged::tests::staged_delivery_text_lists_the_codes) checks + # that this extension's §4.2 and A.6 list the codes the sink reports. + - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' pull_request: @@ -55,6 +58,9 @@ on: # A library test checks SPEC §5's pinned integer payloads against this # extension's sections, so an edit of it is checked here too. - 'wai/extensions/integer-payloads.md' + # A library test (staged::tests::staged_delivery_text_lists_the_codes) checks + # that this extension's §4.2 and A.6 list the codes the sink reports. + - 'wai/extensions/staged-delivery.md' - 'wai/wai-web/**' - '.github/workflows/wai-conformance.yml' @@ -114,11 +120,12 @@ jobs: # and is checkable on this matrix with no ONNX Runtime present. receipt_claims # is in the set because the reference meters (wai_meter, wai_world_meter) write a # measurement report by default and require it: without it they would drop out - # of this build on every leg of this matrix, without a word. - run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims + # of this build on every leg of this matrix, without a word. staged and the integer + # media features build the staged-delivery core, its corpus tool and `wai stage`. + run: cargo build --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,receipt_claims,staged,video_int,audio_int,splat_int - name: Library unit tests (the fixed-point determinism floors) working-directory: wai/wai-rs - run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery + run: cargo test --lib --no-default-features --features world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,oer,receipt_claims,key_discovery,staged,video_int,audio_int,splat_int # The JPEG AI exact decoder reproduces across platforms only if no platform math # library is on its path. Two guards, each failing the step: clippy's # disallowed-methods (wai-rs/clippy.toml), which each module on the decode path @@ -168,7 +175,11 @@ jobs: # allocator) checks that a header past the caps is refused before anything sized from it # is allocated; tests/int_payload_cost.rs (its own binary, a live-bytes allocator) checks # that int_payload::declared_cost bounds what each decode holds, and, in the release - # step only, decodes a 2^30-byte clip a 3 KB payload codes. Cargo.toml declares them, and the other + # step only, decodes a 2^30-byte clip a 3 KB payload codes. tests/staged_resources.rs (its + # own binary, a counting allocator) checks that the staged sink's work and memory per + # layer grow linearly in the layer's units, that a layer past the chain budget is + # refused before any stream is decoded, that what no unit holds is dropped, and that a + # WIR1 parse holds no more than its declared working bytes. Cargo.toml declares them, and the other # feature-gated targets named here, with required-features, so if this step ever loses a # feature, cargo stops with an error instead of compiling an empty binary that reports ok. # Each committed vector's decoded output is also checked against the SHA-256 in @@ -178,10 +189,10 @@ jobs: # wai.generate.spectral vector, which `generate` enables. - name: Fast integer kernels ≡ reference kernels (overflow checks on) working-directory: wai/wai-rs - run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost + run: cargo test --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources - name: Fast integer kernels ≡ reference kernels (release, overflow checks off) working-directory: wai/wai-rs - run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost + run: cargo test --release --no-default-features --features neural_int,video_int,audio_int,world_int,splat_int,generate,staged --lib --test int_kernels_vectors --test int_kernels_geometry --test int_mlicpp --test int_mlicpp_preflight --test int_audio --test int_splat --test generate --test int_payload --test int_payload_preflight --test int_payload_cost --test cli_stage --test staged_resources # The integer decode path builds under each of its features alone, so a sink can take # one medium without the others, and for wasm32 (a browser sink); `int_payload` arms a # medium only under its feature, and none needs `neural`. @@ -191,11 +202,11 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - for f in neural_int video_int audio_int splat_int; do + for f in neural_int video_int audio_int splat_int staged; do cargo check --lib --no-default-features --features "$f" done rustup target add --toolchain 1.98.0 wasm32-unknown-unknown - cargo check --lib --target wasm32-unknown-unknown --no-default-features --features video_int,audio_int,splat_int + cargo check --lib --target wasm32-unknown-unknown --no-default-features --features video_int,audio_int,splat_int,staged # carriage: every registered capability round-trips byte-exact. cli_wrap_intent and # cli_registry run the `wai` binary, which builds without the codecs feature: the # intent rule of `wai wrap`, its refusal of candidate capabilities (SPEC §5), its media @@ -266,7 +277,7 @@ jobs: working-directory: wai/wai-rs run: | set -euo pipefail - F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int + F=world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged v() { echo "::group::$1"; cargo run -q --no-default-features --features "$F" --bin "$2" -- verify "$3"; echo "::endgroup::"; } v worlds wai_world_vectors ../world-conformance v spatial-audio wai_audio_vectors ../audio-conformance @@ -285,6 +296,7 @@ jobs: v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance v int-payload wai_int_payload_vectors ../int-payload-conformance + v staged wai_staged_vectors ../staged-conformance # The pin corpus's second implementation: Python standard library only, sharing no # code with wai-rs. One leg is enough — it checks digests and listing rules, which # do not depend on the architecture — and the macOS host has python3. @@ -313,7 +325,7 @@ jobs: shell: pwsh working-directory: wai/wai-rs run: | - $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int" + $F = "world,audio_scene,haptic,splat4d,score,provenance,neural_int,film,quantum,quantum_ops,oer,receipt_claims,key_discovery,video_int,audio_int,splat_int,staged" function v($grp, $bin, $path) { Write-Output "::group::$grp" cargo run -q --no-default-features --features $F --bin $bin -- verify $path @@ -337,6 +349,7 @@ jobs: v pin wai_pin_vectors ../pin-conformance v signers wai_signer_vectors ../signer-conformance v int-payload wai_int_payload_vectors ../int-payload-conformance + v staged wai_staged_vectors ../staged-conformance # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the @@ -367,7 +380,8 @@ jobs: # under a fixed seed, so a run is reproducible), jwp_verify.py (every JWP vector), and # signer_verify.py (every key-discovery vector and the canonical fixture, with its # verdicts required to equal wai_signer_vectors' line for line), and - # int_payload_verify.py (the integer payloads and their refusal fixtures). + # int_payload_verify.py (the integer payloads and their refusal fixtures), and + # staged_verify.py (the staged-delivery corpus). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -412,6 +426,15 @@ jobs: working-directory: wai shell: bash run: python3 int-payload-conformance/int_payload_verify.py int-payload-conformance + # The staged-delivery corpus's second implementation (staged-conformance/README): it + # replays every case from its stage-0 buffers through its own reading of the + # extension (refines, WIR1, the sink's steps, holds, batches, conflicts, deadlines, + # tiers, stage hashes), decodes the WIV1 keyframe-0, PNG and FLAC bases, explodes the + # WAI2 files, and replays the schedule. Standard library only. + - name: staged_verify.py + working-directory: wai + shell: bash + run: python3 staged-conformance/staged_verify.py staged-conformance - name: claim_verify.py, differential.py, jwp_verify.py, signer_verify.py working-directory: wai shell: bash diff --git a/.github/workflows/wai-formal-verification.yml b/.github/workflows/wai-formal-verification.yml index cc95ff47d..dfb7cadbb 100644 --- a/.github/workflows/wai-formal-verification.yml +++ b/.github/workflows/wai-formal-verification.yml @@ -33,6 +33,8 @@ on: - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/wai_video_policy.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' # Not verified by any harness, but #[path]-included because int_transform's # IGDN dispatches to it, so it must still compile under the model checker # (its cfg(kani) branches are compiled nowhere else). @@ -47,6 +49,8 @@ on: - 'wai/wai-rs/src/int_entropy.rs' - 'wai/wai-rs/src/int_transform.rs' - 'wai/wai-rs/src/wai_video_policy.rs' + - 'wai/wai-rs/src/staged/predict.rs' + - 'wai/wai-rs/src/staged/wir1_arith.rs' - 'wai/wai-rs/src/int_kernels.rs' - '.github/workflows/wai-formal-verification.yml' diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index cd648b6c9..6470617c8 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -11,7 +11,8 @@ name: wai-web wasm32 # capability, none is a candidate (SPEC §5), and the wasm build's own # registry agrees with the export on every candidate; # - demo/verify_pack.mjs: the browser writers `pack` and `packMulti` refuse -# every candidate the export lists, in every place an envelope can name one; +# every candidate the export lists, in every place an envelope can name one, +# and a manifest that names a member twice in one object; # - a byte comparison of the demo's staged int_synth copies with the corpus; # - the crate's wasm_tests under wasm-bindgen-test-runner, and its native unit # tests on the host. @@ -90,7 +91,7 @@ jobs: # bindings, which wasm_tests does not run. They decode the committed integer # payloads to their sidecars' SHA-256 (int_payload::tests) and check that the # writers `pack` and `packMulti` refuse a payload that is not the format SPEC §5 - # pins its integer capability to (tests). The count check turns a test that + # pins its integer capability to, and a manifest that names a member twice (tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests shell: bash @@ -98,7 +99,7 @@ jobs: run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" - grep -q "test result: ok. 8 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" + grep -q "test result: ok. 9 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index bd77e07a8..c7bea8e52 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,107 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Staged delivery: refinement layers, base admission, and WAI2 refinement components + +- **The extension** [staged-delivery](extensions/staged-delivery.md): a stage set is a + base and layers, each layer its own `WAI1` envelope naming, by content hash, the + base and the producer each unit it covers is expected to have (`refines`: `base`, + `prev` as one producer or runs, `layer`, `span`). A sink holds per unit a stage, a + producer and a tier, and applies a layer per unit by one algorithm with a closed + list of reasons (§4.1, §4.2): every envelope-scoped step (well-formed `refines`, a + refinement capability with no fallback, the base present, not through a fallback, + admitted, attested, a well-formed payload within the sink's cost limit and its chain + budget) before the unit steps (conflict, readiness, geometry, deadline). Layers a unit is not ready for are held + to a declared bound and released when its stage advances; layers released together + form a batch, so two conflicting layers released together are both refused and the + unit frozen, whatever their order. A layer that differs from one that already passed + for the same unit freezes it at its stage. A unit's tier is the weakest of its base + and layers, `entropy-consistency` does not pass through a layer, and a stage hash is + stated only at `decode-equivalence`. Deferral, live deadlines (a sink never delays a + unit for a layer, and counts the units presented below the stage their layers would + have reached on time), attestation (a receipt verified without the layer's bytes + never authorizes an application) and byte counting (envelope bytes; receipts, + framing and parameter sets separately; never JSON or base64) are stated. +- **Resource bounds** (§4.5, §14, A.5): a sink declares a chain budget per base, over + its units' output, 32 bytes per stage, and the payload and declared working bytes of + every layer it retains with one unit's output and 32 bytes for each unit it covers + and has not been applied to; a layer that would pass it is refused + (`chain-over-limit`) before any stream is decoded, and a layer no unit is held for is + dropped. The declared working bytes count the stream index. A sink's work per layer + is linear in its payload and declared symbols up to a logarithmic factor in its + units, and §14 states how. A sink that drops a base's chain discards the layers held + for it (`base-absent`) and keeps each unit's record that is not empty (its + producers, a pending producer, its frozen flag), within a declared record budget + whose oldest bases are forgotten first; the base decoded again advances only along + that record, and a frozen unit replays only its own record, in order. + Decoding a base again keeps its chain, and a receipt verified for the base, before or + after it was decoded, makes it attested from then on, across dropped chains; a layer held for its base and offered + again keeps the strongest receipt evidence, and over an attested base counts as + arriving when its receipt was verified; a set split across bases shares one live + clock (`first_unit`). +- **The residual payload `WIR1`** (Appendix A): one rANS stream per unit; + `clamp(pred + r·q)` with the identity, up2 (the half-pel arithmetic, odd sizes + included) and rate (linear interpolation, `rdiv`'s halves away from zero, channels + interleaved) predictors. It follows the integer payloads' rules: a parse in a + stated order with the closed list of their codes, caps, a declared cost a sink reads + before decoding, refusal at the first symbol a stream runs out on, canonical stream + ends; and it reads no parameter set. +- **Refinement capabilities** (SPEC §5, *Refinement capabilities*): + `wai.image.int_refine`, `wai.video.int_refine`, `wai.audio.int_refine` and + `wai.splat.int_refine`, each refinement-only, royalty-free, `StandardDefined` and + exact given the base it refines. SPEC §8 adds the refinement-only role; SPEC §4 never + dispatches one, and a sink that does not implement the extension finds it + unsupported. The registry gains the `refinement` kind and role, and the export is + regenerated. *Envelopes this makes non-conforming* (they still parse): a `WAI1` + carrying a refinement capability without `refines`, with a fallback, or as a + fallback; `refines` on any other capability; a refinement capability as a `WAI2` + rendition or companion; a companion bound to one. +- **Base admission is one table** (SPEC §5, *Base admission*; + `codecs::BASE_ADMISSION`): the bases of each refinement and of the LCEVC enhancement + layer, whose bases are unchanged. `enhancement_bases`, `companion_applies_to` and + `admits` read it. +- **WAI2 refinement components** (SPEC §7.1), on the component identity (`id`, + `sha256`, `prior`; no other digest is defined): an entry with + `role: "refinement"` carries `refines` naming entries by `id`, follows every + selectable entry, is never selected, and is checked by `check_component_roles` in a + stated order. *Exploding a component* writes its `WAI1` envelope, from + `envelope_manifest` verbatim or from the `WAI2` manifest's members as written, with + ids replaced by the content hashes of the named entries' exploded envelopes, so a + component and the layer carried alone are the same object; `WaiMulti::pack` packs + `WAI1` envelopes with their manifests verbatim, and they explode back to the same + bytes; it refuses an envelope it could not give back (bytes around a manifest's JSON + value or after a payload, or a manifest that inside a `WAI2` passes the depth limit) + and checks its output before returning it. Each entry explodes on its own, so one + that cannot leaves the others intact. A refinement's `envelope_manifest` names no + fallback. A malformed `refines` or a mismatched `envelope_manifest` does not make a + manifest unreadable. +- **SPEC §2**: member names are unique within each object of a manifest, at any depth, + and an integer member is written as an integer (`-0`, `1.0` and `1e0` are not); both + readers refuse a manifest that breaks either. `span: null` is `span` absent. +- **Reference implementation**: `staged::{StageChain, StageSink, wir1, predict, + wir1_arith, tier, schedule}` (feature `staged`); `container::{Refines, Prev}`; + `WaiMulti::{explode, explode_each, explode_all, pack, check_envelope_manifests, + refinements_of}`; `container::check_unique_members`; `wai stage + refine|apply|inspect|pack|explode` (`refine --out-dims` gives a layer's shape); + `StageSink::{remove_base, forget_base, mark_attested}`; `wai wrap` refuses a refinement + capability, and `wai inspect` shows `refines`. `RansEncoder` writes the bypass + escape, and `int_entropy::hist_to_cdf` builds a table from a histogram in integer + arithmetic. In `ternary::rans_encode_trits`, the encode call sat inside + `debug_assert!`, so a release build coded no symbol; it now runs in every build. +- **Conformance**: the corpus [staged-conformance](staged-conformance/) (image, video, + audio and splat sets; cases for every reason of §4.2 and every `WIR1` refusal code, + in the order the steps check them, and for §3's rules, holds and their bound, + batches, frozen units, the chain budget, a base decoded again, receipt evidence and + live deadlines, one clock across bases included; `WAI2` cases for each refinement + rule of SPEC §7.1, for exploding entry by entry and for what a packer refuses; the + live schedule), written and verified by `wai_staged_vectors` and verified + independently by `staged_verify.py`, which shares no code with `wai-rs`; the pure- + Python BLAKE3 moves to `claim-conformance/_blake3.py`, shared by both Python + verifiers. Cross-architecture goldens pin every stage of an image, a video and an + audio set; Kani proves, over bounded windows, the arithmetic step, the up2 bounds, + `rdiv` and the rate indices. `rdiv` and the rate indices are computed in checked + 64-bit arithmetic. + ### The integer codecs' payloads pinned in place: `WIV1`, `WIH1`, `WIA1`, `WIS1` - **Pinned in place** (SPEC §5, *Payloads of the WAI-native integer codecs*, and the diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 3f7f47bbd..5ff7d8222 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -27,6 +27,13 @@ Every registered string has one kind. - **companion** — a companion-only capability (SPEC §5, §7.1): its payload is applied with a base rendition and never on its own, so it is only ever a `WAI2` companion. +- **refinement** — a refinement-only capability (SPEC §5, *Refinement + capabilities*): its payload is applied to the output of the stage below it, + bound to that output's producer by content hash, and never decoded on its own. + It is a `WAI1` `capability` with `refines`, or a `WAI2` component with + `role: "refinement"` ([staged-delivery](extensions/staged-delivery.md)). The + bases each one applies over are SPEC §5's *Base admission* table, which also + holds the bases of the enhancement layer `wai.video.lcevc`. - **derivation** — an operation of ISO/IEC 23001-16 (`wai.derive.*`, [derived-tracks](extensions/derived-tracks.md)). It acts on samples another capability decoded and names no payload format. @@ -36,7 +43,8 @@ Every registered string has one kind. A derivation or record string is not a payload capability, so it is not dispatchable (SPEC §4): dispatch never takes an envelope's capability that names -one, and step 5 may still take a usable fallback. +one, and step 5 may still take a usable fallback. A refinement is not dispatchable +either; staged-delivery §4 processes it. Two further tables hold strings that look like capabilities and are not: @@ -64,7 +72,8 @@ Two further tables hold strings that look like capabilities and are not: Every dispatch and selection function uses one predicate, `codecs::is_dispatchable(cap)`, SPEC §4's *dispatchable*: false for a -companion-only capability, a candidate, a record and a derivation; true for a +companion-only capability, a refinement-only capability, a candidate, a record and +a derivation; true for a registered payload capability that is not a candidate, and for a string this revision does not register, which a later MINOR revision may register (SPEC §8) and which a sink decodes only if it advertises it. A new dispatch or selection @@ -84,7 +93,7 @@ then carries that date). ## Licence classes -Every payload and companion capability carries a licence class: +Every payload, companion and refinement capability carries a licence class: `royalty-free`, `patent-pool`, `proprietary` or `non-commercial` (`codecs::capability_license`). It is a property of the capability, not a per-file claim. The classification has no default: a capability registered @@ -100,8 +109,8 @@ gives it; a record carries none. ## How to register a string -1. Add the capability constant to `wai-rs/src/codecs/mod.rs` and, for a payload or - companion capability, list it in `known_capabilities()` and give it an arm in +1. Add the capability constant to `wai-rs/src/codecs/mod.rs` and, for a payload, + companion or refinement capability, list it in `known_capabilities()` and give it an arm in `capability_tier`, `capability_license` and `capability_replicate_class` (each function has no default, and the tests reject a registered capability without one). A record goes in `RECORDS`, a derivation in `codecs::derivation`. @@ -133,6 +142,7 @@ and a role are fixed once registered. | `wai.audio.flac` | payload | registered | audio | standalone | royalty-free | standard-defined | floor | §5 Mandatory floor | | `wai.audio.iamf` | payload | registered | audio | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.audio.int_codec` | payload | registered | audio | standalone | royalty-free | neural-int-exact | — | §5 Interactive, temporal & computed content | +| `wai.audio.int_refine` | refinement | registered | audio | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.audio.ivas` | payload | registered | audio | standalone | patent-pool | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.audio.mpegh` | payload | registered | audio | standalone | patent-pool | standard-unclassified | — | §5 Broadcast and streaming codecs | | `wai.audio.opus` | payload | registered | audio | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | @@ -156,6 +166,7 @@ and a role are fixed once registered. | `wai.haptic.mihs` | payload | registered | haptic | standalone | patent-pool | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.haptic.signal` | payload | registered | haptic | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.image.avif` | payload | registered | image | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | +| `wai.image.int_refine` | refinement | registered | image | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.image.jp2` | payload | registered | image | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.image.jpeg` | payload | registered | image | standalone | royalty-free | standard-tolerance | recommended | §5 Recommended modern set | | `wai.image.jpegai` | payload | registered | image | standalone | royalty-free | neural-entropy-exact | — | §5 Neural capabilities | @@ -194,6 +205,7 @@ and a role are fixed once registered. | `wai.semantic.perco2` | payload | registered | image | standalone | non-commercial | behavioral | — | §5 Neural capabilities | | `wai.splat.4d` | payload | registered | volumetric | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.splat.int_codec` | payload | registered | volumetric | standalone | royalty-free | neural-int-exact | — | §5 Interactive, temporal & computed content | +| `wai.splat.int_refine` | refinement | registered | volumetric | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.splat.sog` | payload | candidate | volumetric | standalone | patent-pool | standard-unclassified | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.splat.spz` | payload | registered | volumetric | standalone | royalty-free | standard-unclassified | — | §5 Interactive, temporal & computed content | | `wai.tensor.nnc` | payload | candidate | binary | standalone | patent-pool | standard-unclassified | — | §5 Prior artifacts, general tensors and derivation operations | @@ -211,6 +223,7 @@ and a role are fixed once registered. | `wai.video.hevc` | payload | registered | video | standalone | patent-pool | standard-defined | — | §5 Broadcast and streaming codecs | | `wai.video.int_hyper` | payload | registered | video | standalone | royalty-free | neural-int-exact | — | §5 Neural capabilities | | `wai.video.int_motion` | payload | registered | video | standalone | royalty-free | standard-defined | — | §5 Neural capabilities | +| `wai.video.int_refine` | refinement | registered | video | refinement | royalty-free | standard-defined | — | §5 Refinement capabilities (staged delivery) | | `wai.video.j2k` | payload | registered | video | standalone | royalty-free | standard-tolerance | — | §5 Immersive audio, cinema, volumetric, haptics, HDR | | `wai.video.keys` | record | registered | — | — | — | — | — | §5 Neural capabilities | | `wai.video.lcevc` | companion | registered | video | enhancement | patent-pool | standard-unclassified | — | §5 Broadcast and streaming codecs | diff --git a/wai/SPEC.md b/wai/SPEC.md index 582086957..7f0831d27 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -69,6 +69,16 @@ OPTIONAL; §5 says which are RECOMMENDED. whitespace, preserving key insertion order. Readers MUST NOT depend on key order for *semantics*; encoders SHOULD preserve order so files round-trip byte-identically. +- Member names are unique within each object of a manifest, at any + depth: a manifest that names a member twice in one object is malformed, + and a reader rejects it as it rejects any manifest that does not parse. + Names compare after their escapes are read. +- An integer member of a manifest, or of any JSON this specification or + its extensions define, is a JSON number written as an integer: digits, + with a minus sign only before a non-zero value, and no fraction or + exponent. `-0`, `1.0` and `1e0` are not integers, whatever their value, + and a member that must be an integer and is written so has the wrong + type. - The payload is opaque codec bytes whose interpretation is fully determined by the manifest's `model_requirement.capability` and, for a capability that decodes against a parameter set, by that set, which the @@ -90,6 +100,7 @@ All WAI multi-byte integers are little-endian unless stated otherwise. "fallback": "" | null }, "conditioning": { "kind": "", "prior": { ... } }, "security": { "encryption": "" | "none", "kid": }, + "refines": { ... }, "target": { ... } } ``` @@ -143,6 +154,10 @@ All WAI multi-byte integers are little-endian unless stated otherwise. payload is plaintext (the default). Otherwise the payload is encrypted and MUST be recovered before dispatch; see §6. The capability, media, and conditioning fields always describe the **plaintext** codec. +- `refines` — OPTIONAL. Carried exactly when `model_requirement.capability` is + refinement-only (§5, *Refinement capabilities*): what the payload refines, by + content hash ([staged-delivery](extensions/staged-delivery.md) §3). A manifest + that carries it has a `null` `fallback`. - `target` — codec-specific reconstruction parameters (dimensions, sample rate, duration, frame count). Informational; the payload is self-describing per the codec's own format. @@ -390,8 +405,9 @@ pinned rendition per set in a `WAI2` envelope (§7.1). A capability is **dispatchable** unless this revision registers it as something a sink does not decode on its own: a candidate (§5, *Candidate capabilities*), a -companion-only capability (§5, *Companion-only capabilities*), a record or a -derivation (§5, *Kinds of registered string*). A sink MUST NOT take step 3 or +companion-only capability (§5, *Companion-only capabilities*), a refinement-only +capability (§5, *Refinement capabilities*), a record or a derivation (§5, *Kinds +of registered string*). A sink MUST NOT take step 3 or step 5 with a capability that is not dispatchable, whatever it advertises. A string this revision does not register is dispatchable: a later MINOR revision may register it (§8), and a sink that has a codec for it says so by advertising @@ -401,6 +417,13 @@ payload is companion data, so step 5 is not taken either (§5). A WAI **payload always corresponds to exactly one capability**. v1.0 does not support multi-codec multiplexed payloads. +A sink never takes step 3 or step 5 with a capability §5 registers as +**refinement-only**, and a `WAI1` whose `cap` is refinement-only takes neither: +its payload refines the output of a base, and +[staged-delivery](extensions/staged-delivery.md) §4 processes it. A sink that does +not implement that extension finds the capability unsupported, and the envelope is +inert. + If the manifest declares `security.encryption` other than `"none"` (§6), the sink MUST recover the plaintext payload before step 3 and run this algorithm on the plaintext; see §6.4. @@ -475,6 +498,9 @@ kind: rendition names the codec its payload is decoded with; - a **companion** capability: companion-only (*Companion-only capabilities*, below), carried only as a `WAI2` companion; +- a **refinement** capability: refinement-only (*Refinement capabilities*, + below), applied to the output of the stage below it and never decoded on its + own; - a **derivation** operation (`wai.derive.*`): an operation on samples another capability decoded, which names no payload format; - a **record**: a signed record a WAI tool emits and verifies (a receipt kind — @@ -482,8 +508,8 @@ kind: delivery records `wai.video.manifest`, `wai.video.ssai`, `wai.video.keys`, `wai.video.package` and `wai.video.channel`), which names no payload format. -Only payload and companion capabilities have a media class, a fidelity tier, a -replicate class and a role. A derivation or record string is not a payload +Only payload, companion and refinement capabilities have a media class, a +fidelity tier, a replicate class and a role. A derivation or record string is not a payload capability and is not dispatchable (§4): step 3 does not take an envelope's `capability` that names one, and step 5 may still take a usable fallback. A row of the tables below that registers a record says what the record attests, not a payload format. Named @@ -801,6 +827,45 @@ capability `codecs::is_dispatchable` accepts (§4's dispatchable). Its `wai wrap envelope packers (`wai_envelope_pack`, `wai_envelope_pack_multi`) and its browser writers (`pack`, `packMulti`) refuse to write one. +### Refinement capabilities (staged delivery) + +| capability | staged form | payload | registry class | +|---|---|---|---| +| `wai.image.int_refine` | residual | `WIR1` ([staged-delivery](extensions/staged-delivery.md) Appendix A.1), units RGB8 | StandardDefined | +| `wai.video.int_refine` | residual | `WIR1`, units RGB8 frames | StandardDefined | +| `wai.audio.int_refine` | residual | `WIR1`, units `i16` PCM | StandardDefined | +| `wai.splat.int_refine` | residual | `WIR1`, units attribute tensors | StandardDefined | + +Each is **refinement-only** (§8): its payload is applied, in a declared order, to +the output of the stage below it, bound to that output's producer by content hash, +and never decoded on its own. It is carried only as a `WAI1` with `refines` (§3), +or as a `WAI2` component with `role: "refinement"` (§7.1). It is never a +`capability` or `fallback` that §4 dispatches, a selectable rendition or a +companion. Each is royalty-free and WAI-native, its payload format is pinned by +this registration, it reads no parameter set, and its tier is `Lossy`: its residual +is quantised. Its next stage is exact given the stage below, so its class is +StandardDefined and its output is as exact as the base it refines +([staged-delivery](extensions/staged-delivery.md) §5). + +### Base admission + +A capability applied to the output of a base — a refinement-only capability, or an +enhancement layer — applies only over the bases listed for it here. A later +revision MAY add a base, which widens the capability (§8). + +| capability | admitted bases | +|---|---| +| `wai.image.int_refine` | `wai.neural.int_hyper`, `wai.neural.int_mlicpp`, `wai.neural.int_synth`, `wai.image.png` | +| `wai.video.int_refine` | `wai.video.int_motion`, `wai.video.int_hyper` | +| `wai.audio.int_refine` | `wai.audio.int_codec`, `wai.audio.flac` | +| `wai.splat.int_refine` | `wai.splat.int_codec` | +| `wai.video.lcevc` | `wai.video.avc`, `wai.video.hevc`, `wai.video.vvc`, `wai.video.evc`, `wai.video.evc.baseline`, `wai.video.vp9`, `wai.video.av1`, `wai.video.av1.lossless` (*Enhancement layers (LCEVC)*, below) | + +A refinement is admitted over a base when its `refines.base` names an envelope that +a sink decoded through one of these capabilities. The reference implementation +holds this table once (`codecs::BASE_ADMISSION`), and every admission check reads +it. + ### Broadcast and streaming codecs (registered, sink-supplied) The elementary video and audio bitstreams that terrestrial and satellite @@ -1625,6 +1690,38 @@ by which capability happens to be installed. `companion_of_id` is not a string (`null` is the same as absent) makes the manifest malformed, as a mistyped pin member does (§3.1), and the reader rejects the envelope. +- **Exploding a component.** The exploded envelope of entry *i* is the `WAI1` + envelope (§2) whose payload is entry *i*'s rendition bytes. Its manifest is the + entry's `envelope_manifest` when it carries one, written exactly as it is in the + `WAI2` manifest; otherwise it is + `{"wai":W,"media":M,"intent":I,"model_requirement":{"capability":C,"fallback":null},"conditioning":{"kind":K[,"prior":P]}[,"security":S][,"refines":R][,"target":T]}`, + with no other bytes, where `W`, `M`, `I`, `S` and `T` are the `WAI2` manifest's + `wai`, `media`, `intent`, `security` and `target`, and `C`, `K` and `P` the entry's + `capability`, `kind` and `prior`, each written exactly as it is in the `WAI2` + manifest; `R` is the entry's `refines` (*Refinement components*, below) with each + component id replaced by the content hash of that entry's exploded envelope, written + as [staged-delivery](extensions/staged-delivery.md) §3 orders its members (`base`, + `prev`, `layer`, `span`; a run's `producer`, then `count`) with no whitespace. A + bracketed part is present exactly when its member is present and not `null`. A + component and its exploded + envelope are the same object for every rule that names an object by content hash + ([jwp-receipts](extensions/jwp-receipts.md) §2.1). An entry MAY carry + `envelope_manifest`, a JSON object: the exact manifest of the entry as a `WAI1` + envelope. Its `model_requirement.capability`, `conditioning.kind` and + `conditioning.prior` MUST equal the entry's, its `refines` MUST be the entry's + with each id replaced as above, or both absent, and, where the entry's capability is + refinement-only, its `model_requirement.fallback` MUST be `null` + (`component-manifest-mismatch`). A `WAI1` envelope packed into a `WAI2` with its + manifest copied verbatim into `envelope_manifest` explodes back to the same bytes, + and so keeps its content hash, when its manifest has no bytes before or after its + JSON value (whitespace around a member's value is not part of it, so an explode + cannot write it back) and nothing follows its payload; a packer refuses any other + envelope, and refuses an output that does not explode back to its sources (one + whose manifest, nested in the `WAI2` manifest, passes a reader's depth limit). Entry + *i*'s exploded envelope is defined by entry *i* and the entries its `refines` names, + so an entry that cannot be exploded leaves every entry that does not name it intact. + A tool that re-serialises a `WAI2` manifest may change these bytes, and with them + the content hashes of the exploded envelopes. - Rendition table: `u16 n_renditions | n × (u32 offset, u32 length)`, offsets relative to the start of the payloads block. The table's i-th entry corresponds to `renditions[i]` in the manifest. @@ -1738,6 +1835,37 @@ does not recognise; a companion that breaks rule 3 binds to nothing, or, for an enhancement layer, is handled as §5 states. The sink MAY still present the rendition it picked. +### Refinement components + +A component with `role: "refinement"` is a layer of a stage set +([staged-delivery](extensions/staged-delivery.md)). It carries `refines` (§3), whose +`base` and producers are the `id`s of entries in the same envelope (§7.1, *Component +identity*); its exploded envelope carries the same `refines` with those ids replaced +by the content hashes of the named entries' exploded envelopes (*Exploding a +component*, above), so it is the same object as the layer carried on its own. + +- **Placement.** A refinement component MUST follow every selectable entry in + `renditions`. +- **Selection.** Selection never picks one (step 2: it is not selectable). A sink + picks a rendition as above, then offers to staged-delivery §4, in `renditions` + order, the exploded envelopes of the refinements whose `refines.base` is the picked + entry's `id`, up to the stage its deployer policy chooses. A refinement whose + payload does not match its `sha256` is not offered. +- **Conformance.** A refinement component conforms only if, in this order: its + capability is refinement-only (§5); it carries `id` and `sha256`; its `refines` + is well formed (staged-delivery §3, with ids in place of hashes); `refines.base` + names a selectable entry, which carries `id` and `sha256`; its capability admits + that entry's (§5, *Base admission*); every producer in `prev` names a selectable + entry or a refinement before it, each carrying `id` and `sha256`; and no selectable + entry follows it. No other component carries `refines`, and a refinement-only + capability is carried with no other role. The reference + `WaiMulti::check_component_roles` reports the first break. + +A sink of an earlier revision does not advertise a refinement-only capability, so it +never picks a refinement by capability; and since refinements follow every selectable +entry, a sink that picks the first entry it can decode picks a base. If it picks one +by another policy, it finds the capability unsupported and the component inert. + ### v1.0 compatibility v1.0 envelopes (`WAI1`) remain valid and their format is unchanged, except that @@ -1828,6 +1956,13 @@ envelope that uses any string a later revision registers. then, does not parse now; §3.1 lists it. Nothing in the envelope signals the change: its `wai` value is the same, so a reader of this revision rejects the whole envelope where an earlier reader ignored the member. +- This revision also defines the manifest's `refines` (§3) and a rendition entry's + `refines` and `envelope_manifest` (§7.1) as OPTIONAL members under the MINOR rule + above; envelopes that use them keep their `wai` value. The exception does not cover + them: an envelope whose `refines` is malformed still parses, and + [staged-delivery](extensions/staged-delivery.md) §4 reports it + (`refines-malformed`); one whose `envelope_manifest` does not match its entry still + parses, and §7.1 reports it (`component-manifest-mismatch`). - New capabilities (§5) MAY be added in any MINOR revision. Existing capabilities MUST NOT be redefined — once a capability string is registered for a codec, its payload format is fixed forever; for a registration @@ -1864,6 +1999,18 @@ envelope that uses any string a later revision registers. envelope stops conforming, and a reader of an earlier revision that reports the new binding as not conforming does not apply that companion and can still present its base. +- This revision adds a third role, **refinement-only**: a payload applied, in a + declared order, to the output of the stage below it, bound to that output's + producer by content hash ([staged-delivery](extensions/staged-delivery.md)), and + never decoded on its own. It differs from companion-only, which is unordered, + names its base by capability and travels only inside a `WAI2`: a refinement is + ordered, names its base by content and travels as its own object. Changing a + capability into or out of either role redefines it; admitting another base (§5, + *Base admission*) widens it. The manifest member `refines` (§3) is an OPTIONAL + member defined under the rule above for MINOR revisions: an envelope that does + not carry it keeps its bytes and its meaning. A reader of an earlier revision + does not advertise a refinement-only capability, so it finds such an envelope's + capability unsupported and the envelope inert. - A registration of a capability that decodes against a parameter set also fixes that set's shape (§5, §3.1). Changing one file to several narrows it and requires a new MAJOR; several to one widens it, which a MINOR revision MAY do. @@ -1897,7 +2044,10 @@ envelope that uses any string a later revision registers. a `WAI1` `capability` or `fallback` or as a selectable rendition, that bound it to a base or a number of bases its registration does not admit, or that bound another companion to it (no companion applies to a companion-only - capability). Such envelopes still parse. + capability); registering a refinement-only capability makes non-conforming an + envelope that carried it as a `WAI1` `capability` without `refines`, as a + `fallback`, or as a selectable rendition or a companion, and one that bound a + companion to it. Such envelopes still parse. --- diff --git a/wai/byte-exact-conformance/src/lib.rs b/wai/byte-exact-conformance/src/lib.rs index b442bccc9..3ff907d42 100644 --- a/wai/byte-exact-conformance/src/lib.rs +++ b/wai/byte-exact-conformance/src/lib.rs @@ -75,9 +75,16 @@ pub mod int_mlicpp; #[path = "../../wai-rs/src/int_payload/wire.rs"] pub mod int_payload_wire; +// The staged-delivery residual (`WIR1`, extensions/staged-delivery.md Appendix A): its +// parser, predictors and arithmetic step. Self-contained beside the rANS coder and the +// histogram table already included here; the encoder is not used by the goldens. +#[allow(dead_code)] +pub mod staged; + mod golden; mod int_payload_golden; mod int_video_golden; mod neural_keyframe_golden; mod jpegai_golden; mod int_mlicpp_golden; +mod staged_golden; diff --git a/wai/byte-exact-conformance/src/staged.rs b/wai/byte-exact-conformance/src/staged.rs new file mode 100644 index 000000000..7d2832099 --- /dev/null +++ b/wai/byte-exact-conformance/src/staged.rs @@ -0,0 +1,9 @@ +//! The staged-delivery residual's sources, `#[path]`-included from wai-rs as siblings, +//! so `wir1.rs` finds `super::predict` and `super::wir1_arith` here as it does there. + +#[path = "../../wai-rs/src/staged/predict.rs"] +pub mod predict; +#[path = "../../wai-rs/src/staged/wir1_arith.rs"] +pub mod wir1_arith; +#[path = "../../wai-rs/src/staged/wir1.rs"] +pub mod wir1; diff --git a/wai/byte-exact-conformance/src/staged_golden.rs b/wai/byte-exact-conformance/src/staged_golden.rs new file mode 100644 index 000000000..62b73a939 --- /dev/null +++ b/wai/byte-exact-conformance/src/staged_golden.rs @@ -0,0 +1,166 @@ +//! Cross-architecture byte-equality for staged delivery +//! (`extensions/staged-delivery.md`): the `WIR1` residual layers of three sets of the +//! corpus (`staged-conformance/`), decoded with the parser, predictors and arithmetic +//! step a sink runs (`staged/{wir1,predict,wir1_arith}.rs`, `#[path]`-included). +//! +//! - video: a `wai.video.int_motion` GOP (`WIV1`, decoded here by `int_video.rs`), a +//! layer 1 per frame, then a layer 2 over the GOP; +//! - image: the pinned 16×16 integer image's stage 0, up2 to 31×31, then up2 to 61×61 +//! (odd sizes, clamped neighbours); +//! - audio: a stereo stage 0 at 8 kHz, the rate predictor to 16 kHz, then to 48 kHz +//! (`rdiv`'s halves). +//! +//! Each stage's SHA-256 (the clip digest over its units, SPEC §7 *Digests*) is pinned +//! below, and the last stage's must also be the clip hash the case states. + +#![cfg(test)] + +use std::path::PathBuf; + +use sha2::{Digest, Sha256}; + +use crate::int_payload_wire::parse_wiv1; +use crate::int_video::{decode_typed, VideoStream}; +use crate::staged::wir1::{apply_unit, parse, Below, Next, UnitKind}; + +const VIDEO_STAGES: [&str; 3] = [ + "877fda1b49177f47d55298f28a280560555d8cace1dc6e9e6d370f2ce834b471", + "63ec8c064fcb36c3ab6e73854da46eace11eabf1aa010c0990b36d911ee408a4", + "4423cde72c6a314df503f5698532daa9917673ab871867b0ab5aa07c6d933694", +]; +const IMAGE_STAGES: [&str; 3] = [ + "4a501b0ee2468e4db07eb5f20e3408330604a231a1cd375a794adc5309197463", + "4695991ba30ab7d1129726f2c2cf743e4d892013b173a21c80fd4d67165e0239", + "64dba25135e5965fd511247477f911b85aa5a9b2fae263b64e791b72c9c43397", +]; +const AUDIO_STAGES: [&str; 3] = [ + "aab0d06a5d8fc29f47bd107940bd88dfd7d588980bbb72df54d5b8e9a3222006", + "e36b0ab5ed1f874edf0670cffd961133e7d9602cbc610fc2edf766a0d620c877", + "0408de38e23d42bd6af8064e109c73ab90582eaad588c68fbcf8b25fe9cf4c29", +]; + +fn case_dir(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../staged-conformance/cases").join(name) +} + +fn read(name: &str, file: &str) -> Vec { + let p = case_dir(name).join(file); + std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display())) +} + +/// A `WAI1` envelope's payload. +fn payload(env: &[u8]) -> Vec { + let ml = u32::from_le_bytes(env[4..8].try_into().unwrap()) as usize; + let at = 8 + ml; + let pl = u32::from_le_bytes(env[at..at + 4].try_into().unwrap()) as usize; + env[at + 4..at + 4 + pl].to_vec() +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +#[derive(Clone)] +enum Buf { + U8(Vec), + I16(Vec), +} + +impl Buf { + fn bytes(&self) -> Vec { + match self { + Buf::U8(v) => v.clone(), + Buf::I16(v) => v.iter().flat_map(|s| s.to_le_bytes()).collect(), + } + } + fn below(&self) -> Below<'_> { + match self { + Buf::U8(v) => Below::U8(v), + Buf::I16(v) => Below::I16(v), + } + } +} + +/// The clip digest of `units`: SHA-256 of their SHA-256 digests in order. +fn clip(units: &[Buf]) -> String { + let mut all = Vec::new(); + for u in units { + all.extend_from_slice(&Sha256::digest(u.bytes())); + } + hex(&Sha256::digest(&all)) +} + +/// Apply `file` (a `WIR1` layer of `kind`) to `units[from..]`, unit by unit. +fn apply(name: &str, file: &str, kind: UnitKind, units: &mut [Buf], shapes: &mut [(u32, u32, u32)], from: usize) { + let p = payload(&read(name, file)); + let w = parse(&p, kind).unwrap_or_else(|e| panic!("{file}: {e}")); + for i in 0..w.n_units as usize { + let next = apply_unit(&w, &p, i as u32, units[from + i].below(), shapes[from + i]).unwrap_or_else(|e| panic!("{file} unit {i}: {e}")); + units[from + i] = match next { + Next::U8(v) => Buf::U8(v), + Next::I16(v) => Buf::I16(v), + }; + shapes[from + i] = (w.d0, w.d1, w.d2); + } +} + +fn case_clip(name: &str, base: usize) -> String { + let c: serde_json::Value = serde_json::from_slice(&read(name, "case.json")).unwrap(); + c["expect"]["bases"][base]["clip_sha256"].as_str().unwrap().to_string() +} + +#[test] +fn staged_video_gop_is_byte_exact() { + let name = "video_int_motion_gop_perframe"; + let base = payload(&read(name, "base_0.wai")); + let v = parse_wiv1(&base, Some(0)).expect("WIV1"); + let s = VideoStream { + n_frames: v.n_frames as usize, h: v.h as usize, w: v.w as usize, block: v.block as usize, + q: v.q as i64, offset: v.offset, mv_precision: v.mv_precision, cdf: &v.cdf, mvs: &v.mvs, rans: v.rans, + }; + let mut units: Vec = decode_typed(&s).expect("decode").frames.into_iter().map(Buf::U8).collect(); + let mut shapes = vec![(v.h, v.w, 3); units.len()]; + let mut got = vec![clip(&units)]; + for f in 0..4 { + apply(name, &format!("g0_L1_f{f}.wai"), UnitKind::Rgb8, &mut units, &mut shapes, f); + } + got.push(clip(&units)); + apply(name, "g0_L2.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("video stages {got:?}"); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, VIDEO_STAGES, "a stage of the video set diverged"); +} + +#[test] +fn staged_image_up2_odd_is_byte_exact() { + let name = "image_up2_odd"; + let mut units = vec![Buf::U8(read(name, "stage0.bin"))]; + let mut shapes = vec![(16, 16, 3)]; + let mut got = vec![clip(&units)]; + apply(name, "L1.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + apply(name, "L2.wai", UnitKind::Rgb8, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("image stages {got:?}"); + assert_eq!(shapes[0], (61, 61, 3)); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, IMAGE_STAGES, "a stage of the image set diverged"); +} + +#[test] +fn staged_audio_rate_ladder_is_byte_exact() { + let name = "audio_flac_rate_ladder"; + let s0 = read(name, "stage0.bin"); + let mut units = vec![Buf::I16(s0.chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect())]; + let mut shapes = vec![(400, 1, 2)]; + let mut got = vec![clip(&units)]; + apply(name, "L1.wai", UnitKind::Pcm, &mut units, &mut shapes, 0); + got.push(clip(&units)); + apply(name, "L2.wai", UnitKind::Pcm, &mut units, &mut shapes, 0); + got.push(clip(&units)); + println!("audio stages {got:?}"); + assert_eq!(shapes[0], (2400, 1, 2)); + assert_eq!(got[2], case_clip(name, 0), "the final stage is the case's"); + assert_eq!(got, AUDIO_STAGES, "a stage of the audio set diverged"); +} diff --git a/wai/claim-conformance/_blake3.py b/wai/claim-conformance/_blake3.py new file mode 100644 index 000000000..89865deb7 --- /dev/null +++ b/wai/claim-conformance/_blake3.py @@ -0,0 +1,75 @@ +"""BLAKE3 in hash mode, written from its specification in pure Python. + +Shared by the claim-conformance verifier (`claim_verify.py`) and the staged-delivery +verifier (`../staged-conformance/staged_verify.py`), which import `blake3` from here. It +shares no code with wai-rs, which computes the same hashes with the `blake3` crate. +""" + +_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 +_M32 = 0xFFFFFFFF + + +def _rotr(x, n): + return ((x >> n) | (x << (32 - n))) & _M32 + + +def _g(s, a, b, c, d, x, y): + s[a] = (s[a] + s[b] + x) & _M32 + s[d] = _rotr(s[d] ^ s[a], 16) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 12) + s[a] = (s[a] + s[b] + y) & _M32 + s[d] = _rotr(s[d] ^ s[a], 8) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 7) + + +def _compress(cv, m, counter, blen, flags): + s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, blen, flags] + m = list(m) + for r in range(7): + _g(s, 0, 4, 8, 12, m[0], m[1]); _g(s, 1, 5, 9, 13, m[2], m[3]) + _g(s, 2, 6, 10, 14, m[4], m[5]); _g(s, 3, 7, 11, 15, m[6], m[7]) + _g(s, 0, 5, 10, 15, m[8], m[9]); _g(s, 1, 6, 11, 12, m[10], m[11]) + _g(s, 2, 7, 8, 13, m[12], m[13]); _g(s, 3, 4, 9, 14, m[14], m[15]) + if r < 6: + m = [m[i] for i in _PERM] + return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] + + +def _words(block): + block = block + b"\0" * (64 - len(block)) + return [int.from_bytes(block[i:i + 4], "little") for i in range(0, 64, 4)] + + +def _chunk_output(chunk, counter): + blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""] + cv = _IV + for j, b in enumerate(blocks[:-1]): + cv = _compress(cv, _words(b), counter, 64, _CHUNK_START if j == 0 else 0)[:8] + last = blocks[-1] + return (cv, _words(last), counter, len(last), _CHUNK_END | (_CHUNK_START if len(blocks) == 1 else 0)) + + +def _cv(out): + cv, w, c, bl, f = out + return _compress(cv, w, c, bl, f)[:8] + + +def blake3(data: bytes) -> bytes: + chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""] + stack = [] + for idx, ch in enumerate(chunks[:-1]): + cv = _cv(_chunk_output(ch, idx)) + total = idx + 1 + while total & 1 == 0: + cv = _cv((_IV, stack.pop() + cv, 0, 64, _PARENT)) + total >>= 1 + stack.append(cv) + out = _chunk_output(chunks[-1], len(chunks) - 1) + while stack: + out = (_IV, stack.pop() + _cv(out), 0, 64, _PARENT) + cv, w, _, bl, f = out + return b"".join(x.to_bytes(4, "little") for x in _compress(cv, w, 0, bl, f | _ROOT)[:8]) diff --git a/wai/claim-conformance/claim_verify.py b/wai/claim-conformance/claim_verify.py index 0ccfd8930..f12577aa8 100644 --- a/wai/claim-conformance/claim_verify.py +++ b/wai/claim-conformance/claim_verify.py @@ -36,6 +36,7 @@ from decimal import localcontext HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, os.path.join(HERE, "..", "oer-conformance")) import oer2_verify as OER # noqa: E402 (the OER reference verifier: grades + receipt checks) +sys.path.insert(0, HERE) from cryptography.exceptions import InvalidSignature # noqa: E402 from cryptography.hazmat.primitives import serialization # noqa: E402 @@ -43,74 +44,7 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 # noqa: E402 # ---------------------------------------------------------------- BLAKE3 (hash mode) -_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] -_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] -_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 -_M32 = 0xFFFFFFFF - - -def _rotr(x, n): - return ((x >> n) | (x << (32 - n))) & _M32 - - -def _g(s, a, b, c, d, x, y): - s[a] = (s[a] + s[b] + x) & _M32 - s[d] = _rotr(s[d] ^ s[a], 16) - s[c] = (s[c] + s[d]) & _M32 - s[b] = _rotr(s[b] ^ s[c], 12) - s[a] = (s[a] + s[b] + y) & _M32 - s[d] = _rotr(s[d] ^ s[a], 8) - s[c] = (s[c] + s[d]) & _M32 - s[b] = _rotr(s[b] ^ s[c], 7) - - -def _compress(cv, m, counter, blen, flags): - s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, blen, flags] - m = list(m) - for r in range(7): - _g(s, 0, 4, 8, 12, m[0], m[1]); _g(s, 1, 5, 9, 13, m[2], m[3]) - _g(s, 2, 6, 10, 14, m[4], m[5]); _g(s, 3, 7, 11, 15, m[6], m[7]) - _g(s, 0, 5, 10, 15, m[8], m[9]); _g(s, 1, 6, 11, 12, m[10], m[11]) - _g(s, 2, 7, 8, 13, m[12], m[13]); _g(s, 3, 4, 9, 14, m[14], m[15]) - if r < 6: - m = [m[i] for i in _PERM] - return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] - - -def _words(block): - block = block + b"\0" * (64 - len(block)) - return [int.from_bytes(block[i:i + 4], "little") for i in range(0, 64, 4)] - - -def _chunk_output(chunk, counter): - blocks = [chunk[i:i + 64] for i in range(0, len(chunk), 64)] or [b""] - cv = _IV - for j, b in enumerate(blocks[:-1]): - cv = _compress(cv, _words(b), counter, 64, _CHUNK_START if j == 0 else 0)[:8] - last = blocks[-1] - return (cv, _words(last), counter, len(last), _CHUNK_END | (_CHUNK_START if len(blocks) == 1 else 0)) - - -def _cv(out): - cv, w, c, bl, f = out - return _compress(cv, w, c, bl, f)[:8] - - -def blake3(data: bytes) -> bytes: - chunks = [data[i:i + 1024] for i in range(0, len(data), 1024)] or [b""] - stack = [] - for idx, ch in enumerate(chunks[:-1]): - cv = _cv(_chunk_output(ch, idx)) - total = idx + 1 - while total & 1 == 0: - cv = _cv((_IV, stack.pop() + cv, 0, 64, _PARENT)) - total >>= 1 - stack.append(cv) - out = _chunk_output(chunks[-1], len(chunks) - 1) - while stack: - out = (_IV, stack.pop() + _cv(out), 0, 64, _PARENT) - cv, w, _, bl, f = out - return b"".join(x.to_bytes(4, "little") for x in _compress(cv, w, 0, bl, f | _ROOT)[:8]) +from _blake3 import blake3 # noqa: E402 (written from its specification; shared with staged_verify.py) # ------------------------------------------------ Ed25519 (RFC 8032), §3.1 step 3's rule diff --git a/wai/extensions/determinism-tiers.md b/wai/extensions/determinism-tiers.md index 66616e558..9297817b4 100644 --- a/wai/extensions/determinism-tiers.md +++ b/wai/extensions/determinism-tiers.md @@ -329,6 +329,9 @@ thing. binding over `wai.image.jpegai` as a binding over its exact decoder's pinned reconstruction; that binding is checkable only by that decoder under the conditions in §5–§6. +- **Staged delivery.** A unit refined in stages holds the weakest tier of its base + and every layer applied to it, and `entropy-consistency` does not pass through a + layer ([staged-delivery](staged-delivery.md) §5). ## 4. Framing diff --git a/wai/extensions/staged-delivery.md b/wai/extensions/staged-delivery.md new file mode 100644 index 000000000..3b5feda14 --- /dev/null +++ b/wai/extensions/staged-delivery.md @@ -0,0 +1,553 @@ +# WAI Extension: Staged Delivery + +> Status: Draft. A base first, refinements after, each its own object, bound to what +> it refines by content hash. Every stage is presentable, every prefix of a stage set +> states its determinism tier, and a refinement that never arrives leaves the stages +> below it intact. Reference impl: `wai-rs` — `staged::{StageChain, StageSink}`, +> `staged::{wir1, predict, wir1_arith, tier, schedule}`, `container::Refines`, +> `codecs::BASE_ADMISSION`; the `wai stage` tool; the corpus +> [`staged-conformance/`](../staged-conformance/). Keywords **MUST**, **MUST NOT**, +> **SHOULD**, **MAY** are RFC 2119/8174. + +## 1. Scope and model + +A **stage set** is a **base** and **layers**. The base is any envelope a sink decodes +under SPEC §4 or §7.1. A layer is its own `WAI1` envelope whose capability is +**refinement-only** (SPEC §5, *Refinement capabilities*) and which names, by content +hash, the base and the envelopes it refines. + +A sink holds, for every **unit** of the base's output, a **stage** (0 after the base +decodes) and a **producer**: the content hash of the envelope that produced the +unit's current output, the base at stage 0. Layers apply per unit, and every stage is +presentable. A base never names its layers, so a layer MAY be produced after its +base, by the base's publisher or by a party the sink accepts (§10). + +## 2. Terms + +- **Content hash**: [jwp-receipts](jwp-receipts.md) §2.1, + `BLAKE3("moq-jwp:object\x01" ‖ envelope_bytes)` over the envelope bytes exactly as + received, written as 64 lowercase hexadecimal digits. +- **Unit**: the smallest presentable piece of a base's output, fixed by the base's + capability: one frame of video; the whole picture of an image; the whole clip of + audio; the whole attribute tensor of a splat cloud. +- **Span**: a contiguous run of a base's units. +- **Producer**: for a unit, the content hash of the envelope that produced its current + output. +- **Refinement-only capability**: a capability registered with that role (SPEC §5, + §8). + +## 3. The `refines` member + +A `WAI1` manifest whose `model_requirement.capability` is refinement-only **MUST** +carry `refines`, and its `model_requirement.fallback` **MUST** be `null`. A manifest +whose capability is not refinement-only **MUST NOT** carry it. + +```json +"refines": { "base": "<64 hex>", + "prev": "<64 hex>" | [ {"producer": "<64 hex>", "count": 4}, ... ], + "layer": 2, + "span": { "from": 0, "count": 4 } } +``` + +| member | presence | meaning | +|---|---|---| +| `base` | REQUIRED | Content hash of the stage set's base. | +| `prev` | REQUIRED | The producer each unit the layer covers is expected to have. A string means every such unit. An array of runs gives, in unit order, `count` consecutive units whose producer is `producer`. | +| `layer` | REQUIRED | Integer 1–255: the stage this layer brings each unit to. | +| `span` | OPTIONAL | `from` (integer ≥ 0) and `count` (integer ≥ 1): the units the layer covers. Absent, or `null` (which is the same as absent, as SPEC §3.1 reads an OPTIONAL member): every unit of the base. | + +Rules: + +- Hashes **MUST** be 64 lowercase hexadecimal digits (in a `WAI2` entry they are + component ids; SPEC §7.1, *Refinement components*). +- When `layer` is 1, every producer `prev` names **MUST** be `base`. +- A run array **MUST NOT** contain two adjacent runs with the same producer. A layer + whose units have one producer **MUST** write `prev` as a string, so an array of one + run is malformed. +- With a `span`, the run counts **MUST** sum to its `count`. The span **MUST** lie + within the base's units, and with no span the run counts **MUST** sum to the base's + unit count; a sink checks these once it holds the base (§4.1 step 2). +- `from + count` **MUST** fit in 32 bits, and so **MUST** each `count`. + +Any other JSON type (an integer is written as SPEC §2 defines one), or a value outside +these rules, makes `refines` malformed (`refines-malformed`); the envelope still +parses. A member of `refines` this +revision does not define is ignored (SPEC §8). + +## 4. Applying layers + +### 4.1 Algorithm + +A sink that implements this extension **MUST** process each received envelope *R* +whose capability is refinement-only as follows. Steps 1–6 decide for the whole +envelope; steps 7–10 decide for each unit *u* the layer covers. The first step that +fails is the reason for the envelope (1–6) or for the unit (7–10). + +1. `refines` is present and well formed (§3) (`refines-malformed`). *R*'s capability + is refinement-only, and its `model_requirement.fallback` is `null` + (`capability-not-refinement`). +2. The sink has decoded a base whose content hash is `refines.base`. Otherwise it + **MAY** hold *R* until its bound (§4.3), and then discards it (`base-absent`). + With the base, `refines` is checked against the base's unit count (§3) + (`refines-malformed`). +3. The base was decoded through its manifest's `model_requirement.capability`, or + through the `WAI2` component whose exploded envelope (SPEC §7.1) has the content + hash `refines.base`, and not through a fallback (`base-via-fallback`). +4. The capability the base was decoded through is admitted by *R*'s capability (SPEC + §5, *Base admission*) (`base-not-admitted`). +5. Where the sink verified a delivery receipt for the base, it has verified one for + *R* as §10 requires (`refinement-unattested`). +6. The payload is well formed for *R*'s capability, in this order: it parses + (Appendix A) (`payload-malformed`); its unit count is the number of units the + layer covers (`payload-malformed`); its declared cost (Appendix A.5) is within the + limit the sink applies, if it applies one (`cost-over-limit`); retaining *R* keeps + the chain within its budget (§4.5) (`chain-over-limit`); and every unit's stream + decodes to its last symbol and ends canonically (`payload-malformed`). A payload + malformed for one unit is malformed for all. No unit's shape is read before step + 9. +7. Let *P(u)* be the producer `prev` gives for *u*. *u* is **conflicted** when it is + frozen, or when an envelope with different content, the same `base` and the same + `layer`, whose producer for *u* is also *P(u)*, has passed step 8 for *u*, earlier + (before the base's chain was dropped included, §4.4) or in the same batch (§4.4) + (`layer-conflict`). A conflicted unit is **frozen**: + the sink **MUST** apply no further layer to it, except the replay of its own record + after its base's chain is dropped and the base decoded again (§4.4); it keeps the + output it had (a layer applied to it before the conflict stays applied), and the + sink **MUST** report the conflict. +8. *u* is **ready** when its stage is `layer − 1` and its producer is *P(u)*. A unit + whose stage is below `layer − 1` keeps *R* held for it, and is evaluated again when + its stage advances; at the bound *R* is discarded for that unit (`layer-gap`). Any + other unit is not ready (`prev-mismatch`). +9. *R*'s payload reads *u*'s current output in the shape its staged form needs + (`geometry-mismatch`). +10. Under a live schedule, *R* arrived at least `lead_ms` before *u*'s presentation + time (§8) (`deadline-missed`). + +The sink then computes, for every unit that passed, the next stage by *R*'s staged +form (§6), and sets the unit's stage to `layer` and its producer to *R*'s content +hash. Application to one unit is atomic. A sink **MUST NOT** present a unit's output +from a layer that failed for that unit, and **MUST NOT** present a partly applied +unit. + +### 4.2 Reasons + +| step | reason | scope | effect | +|---|---|---|---| +| 1 | `refines-malformed`, `capability-not-refinement` | envelope | discarded | +| 2 | `base-absent` | envelope | held, then discarded | +| 2 | `refines-malformed` | envelope | discarded | +| 3 | `base-via-fallback` | envelope | discarded | +| 4 | `base-not-admitted` | envelope | discarded | +| 5 | `refinement-unattested` | envelope | discarded | +| 6 | `payload-malformed`, `cost-over-limit`, `chain-over-limit` | envelope | discarded | +| 7 | `layer-conflict` | unit | not applied; the unit is frozen | +| 8 | `prev-mismatch` | unit | not applied to the unit | +| 8 | `layer-gap` | unit | held for the unit, then not applied | +| 9 | `geometry-mismatch` | unit | not applied to the unit | +| 10 | `deadline-missed` | unit | not applied to the unit | + +The list is closed. Two conforming sinks that receive the same envelopes in the same +order, at the same times, with the same receipts, hold bound, cost limit, chain budget +and schedule, report the same reasons per envelope and per unit and reach the same +stages and producers. The order of arrival changes the outcome only through the chain +budget at step 6 (§4.5), step 7, the hold bound at step 8 (§4.3: what is refused on +arrival and what expires) and step 10. + +### 4.3 Holding + +A sink **MUST** declare the bound to which it holds envelopes it cannot yet apply, as +a count of envelopes and a duration from arrival. An envelope held past the duration +is discarded with its hold reason (`base-absent`, or `layer-gap` for each unit it is +held for). An envelope that would exceed the count is discarded on arrival with the +reason it would have been held for. A unit refined by a held envelope that is +released within the bound, and that the chain budget admits, reaches the same output +as one refined in order. + +An envelope held for its base and received again is held once. If it comes with +stronger evidence of its delivery receipt (§10: a receipt verified over its bytes is +stronger than one verified by inclusion alone, which is stronger than none), the sink +keeps the stronger evidence for steps 5 and 10; its hold still runs from its first +arrival. + +### 4.4 Batches + +When the base an envelope names is decoded, the envelopes held for it are released +together; when a unit's stage advances, the envelopes held for that unit are released +together. Each envelope released, or offered on arrival, passes steps 1–6, and for +each unit step 7 against the envelopes that passed step 8 earlier and step 8, alone. +The (envelope, unit) pairs that are ready then form one **batch**: step 7 compares +every pair of the batch with the others, and only then do steps 9 and 10 and +application run. So two conflicting layers released together are both refused and +the unit frozen, whichever arrived first; a layer that conflicts with one that passed +step 8 earlier is refused, and the unit frozen at the stage it has. When a unit is +frozen, every envelope held for it is refused for it (`layer-conflict`). Applying a +batch can release another, which is evaluated the same way. + +A sink that decodes again a base whose chain it holds keeps the chain: re-decoding a +base does not reset its units' stages, producers or frozen state, and releases and +reports nothing. If the sink has verified the base's delivery receipt since, the base +is attested from then on (§10). Attestation belongs to the base's content hash: a base +once attested stays attested, held or dropped, and a decode that carries no receipt +does not undo it; a receipt the sink verifies before it decodes the base makes the base +attested when it is decoded. Only forgetting the base (§14) ends it. A sink **MAY** drop a base's chain when it no longer presents the +base. Every envelope held for the chain's units is then discarded (`base-absent`, +reported once for each), and an envelope for that base is held as for any base the +sink has not decoded. The sink keeps each unit's **record**: the producers of the +stages it reached, the envelope that passed step 8 at its last stage without being +applied, and whether it is frozen. A record is empty when the unit reached no stage, +nothing passed step 8 for it and it is not frozen; the sink keeps only the others, and +nothing of a base all of whose records are empty and that is not attested. When it +decodes the base again, each unit +starts at stage 0 with its record. Step 7 compares a layer with the recorded +producers, so a unit advances again only along them: a layer whose key a recorded +stage holds with other content conflicts, and the unit freezes. A frozen unit stays +frozen: it may replay its own recorded producers, in order, up to its recorded stage +and never past it, and every other layer is refused for it (`layer-conflict`). So it +presents its base's output or the output of one of its recorded producers, never any +other. + +A sink **MUST** declare a **record budget**, in bytes, for what it keeps of the bases it +does not hold: their records, and the attestation of a base dropped while attested or +attested before it was decoded (§10). A kept base counts 32 bytes, and each record it +keeps 8 bytes, 32 for each recorded stage and 32 for an envelope that passed step 8 +without being applied. When what it keeps passes the budget, the sink forgets whole +bases, the one whose record or attestation it wrote longest ago first, until it is +within it. A forgotten base is as one the sink never decoded and never attested +(§14). + +These counts are the interoperable measure: every conforming sink counts the same +bytes, so two sinks with the same record budget keep and forget the same bases. What an +implementation allocates for each counted byte is its own, and it states that ratio +beside its record budget. + +### 4.5 Chain budget + +A sink **MUST** declare a budget, in bytes, for the chain of each base it decodes. The +chain's **state** is the sum of: + +- for every unit, the bytes of its current output (1 per u8 element, 2 per i16 + sample), and 32 bytes for each stage its record holds (the producers step 7 + compares: its stages above 0, or, for a base decoded again, the stages it reached + before, §4.4); +- for every layer the chain **retains**, its payload's bytes and its declared working + bytes (Appendix A.5), and, for each unit it covers and has not been applied to, one + unit's output (its declared output over its units) and 32 bytes (the stage it would + add). + +A chain retains a layer from the moment it passes step 6 to the end of the offer, +release or expiry after which no unit is held for it; the sink then drops it. At step +6, after the cost limit, a layer whose payload, declared output and working bytes and +32 bytes for each unit it covers, added to the state, pass the budget is refused +(`chain-over-limit`) before any stream is decoded. An envelope the chain still +retains, received again, adds nothing. Applying a layer to a unit replaces its output +with one, and adds a stage, that the layer's retention counted, and that unit's share +leaves the layer's: an application lowers the state by the unit's old output (and by +32 bytes more where it replays a recorded stage). The +state rises only when a layer is admitted, so it never passes the budget, and the +bytes a chain holds never pass the state. + +## 5. The tier of a stage, and the stage hash + +- **The base step**: `decode-equivalence` when the capability the base was decoded + through has the registry class `StandardDefined`, or `NeuralIntegerExact` with a pin + that resolved (SPEC §3.1) and declares `decode-equivalence`; + `entropy-consistency` when the class is `NeuralEntropyExact`; otherwise none. +- **A layer step**: the tier its registration gives (§6); `decode-equivalence` for the + residual form. + +A unit's tier is the weakest step of its chain, composed as +[derived-tracks](derived-tracks.md) §3 rule 1. A layer acts on reconstructed output, +so `entropy-consistency` does not pass through it (derived-tracks §3 rule 2): a unit +over such a base holds no tier once a layer applies. + +The **stage hash** of a unit is the SHA-256 of its output in the canonical buffer of +SPEC §7 *Digests*: RGB8 for a picture or a frame; the samples as signed 16-bit +little-endian integers, channels interleaved, for audio; the attribute tensor's bytes, +plane after plane, for a splat. The stage hash of several units is the SHA-256 of +their 32-byte stage hashes concatenated in unit order, as that section defines a clip +digest. A stage hash **MUST NOT** be published, compared as a conformance value, or +sealed in a receipt unless every unit it covers holds `decode-equivalence`. + +## 6. Staged forms + +A refinement-only capability's registration fixes its staged form, payload format, +admitted bases (SPEC §5, *Base admission*), tier and truncation points. + +### 6.1 Residual (`WIR1`) + +`wai.image.int_refine`, `wai.video.int_refine`, `wai.audio.int_refine` and +`wai.splat.int_refine` carry a `WIR1` payload (Appendix A): one rANS stream per +unit. For each element of a unit, the next stage is `clamp(pred + r · q)`. `pred` is +the payload's predictor applied to the unit's current output; `r` is the decoded +residual; `q` is the payload's step; and the clamp is to the element type's range. +Every operation is on integers of at most 64 bits. + +| capability | unit | element | predictors | +|---|---|---|---| +| `wai.image.int_refine` | the picture, RGB8 | u8 | identity, up2 | +| `wai.video.int_refine` | one frame, RGB8 | u8 | identity, up2 | +| `wai.audio.int_refine` | the clip, `i16` PCM, channels interleaved | i16 | identity, rate | +| `wai.splat.int_refine` | the attribute tensor | u8 | identity | + +A residual layer reads no parameter set: everything its decode reads is in its +payload and in the unit's current output. The base's parameter set was checked when +the base was decoded (SPEC §3.1, check 5 among them), and nothing a layer reads needs +the check again. Tier: `decode-equivalence`. Every layer boundary is a truncation +point. + +### 6.2 Other staged forms + +This revision registers the residual form only. A refinement-only capability of +another staged form (the continuation of a world's state, an enhancement layer +carried as its own object, a byte prefix of a progressive codestream, the further +codebooks of a residual-vector-quantised code) is registered by a revision that fixes +its form, payload, admitted bases, tier and truncation points. Where the form decodes +a prefix of a base codec's own codestream, that revision cites the clause of the +codec's standard that defines the decode of such a prefix, and its reference sink +decodes one. + +### 6.3 Truncation points + +A refinement capability declares truncation points only where its staged form defines +the decode of a prefix. For the residual form that is every layer boundary. + +## 7. Deferral + +A layer **MAY** be published after its base, served only on request, or never. A sink +**MUST** present the current stage of a unit without waiting for a layer it has not +been told to expect. A later revision of a refinement is a new chain from the same +`base`, distinguished by `prev` from layer 2 on. Every layer 1 names the base as its +producer, so `prev` cannot distinguish two revisions of layer 1: they are two +envelopes of the same `base`, `layer` and producer, and conflict (§4.1 step 7). + +## 8. Live deadlines + +Under a live schedule, every unit has a presentation time, given by the base's timing +and the target latency the sink applies, and every layer has a lead, `lead_ms`: the +time before a unit's presentation by which the layer must have arrived, and been +verified under §10, to be applied to it. A sink **MUST NOT** delay presenting a unit's +current stage to wait for any layer. A layer that arrived less than `lead_ms` before a +unit's presentation time is not applied to that unit (`deadline-missed`). It **MAY** +be applied to units of its span that are not yet due. A still image **MAY** be +refined after it is presented. A sink **MUST** count every unit presented below the +stage its expected layers would have reached on time. Verifying a layer's receipt +(§10) adds the time to receive and verify that receipt to the layer's arrival, which +sets the least `lead_ms` that can be met: over a base whose receipt the sink verified, +a layer counts as arrived when the sink verified its receipt (§4.3). + +A stage set split across bases (a base for each group of frames) has one clock: a base +whose units are the set's units from position `k` on presents its unit `i` at the +set's position `k + i`, and a sink gives each base its first position. + +## 9. Carriage + +A layer is a `WAI1` envelope, carried and stored as any envelope is, or a `WAI2` +component with `role: "refinement"` (SPEC §7.1, *Refinement components*). A component +and its exploded envelope (SPEC §7.1) are the same object for every rule that names +an object by content hash, so a layer reaches the same stages whichever way it +travelled. This revision binds no other carriage. + +## 10. Attestation of layers + +Each layer is an object under [jwp-receipts](jwp-receipts.md). A sink that verified a +delivery receipt for a base **MUST NOT** apply a layer unless it has verified a +delivery receipt for the layer, through that extension's §3 step 6, over the layer's +envelope bytes as received. That receipt must be signed by the base's signer, or by a +key the sink holds with the authority `delivery` under the same origin +(jwp-receipts §7.5). A receipt verified without the layer's envelope bytes (§3 steps 1, +2, 3 and 5, step 4 not run) shows that a publisher signed an object with that content +hash; it never authorizes an application. A content hash in `refines` binds a layer +to a base, not to its publisher. A sink that verifies a base's delivery receipt after +it decoded the base applies step 5 to every envelope that arrives after it; envelopes +it admitted before keep the decision they had. The verification belongs to the base's +content hash and holds across the base's chain being dropped and the base decoded +again (§4.4). + +## 11. Counting bytes + +A figure of delivered bytes for a stage set: + +- **MUST** count the bytes of the `WAI1` and `WAI2` envelopes as delivered (SPEC §2: + magic, length prefixes, manifest and payload); +- **MUST** state separately the receipt, index and transport framing bytes, and the + parameter-set bytes a sink fetched; +- **MUST NOT** count a text re-encoding of a payload (JSON, base64) in place of the + payload's registered binary format. + +A figure over a payload in any other form is not a staged-delivery byte figure. + +## 12. Conformance + +A conforming sink of this extension implements §4, §5, §8 and §10, and every staged +form whose capabilities it advertises. The corpus +[`staged-conformance/`](../staged-conformance/) holds the cases: for each, a +conforming sink reports the stated reason per envelope and unit, and reaches the +stated stage, producer, tier and stage hash per unit. Its `README.md` states the +cases, what each base's stage 0 is checked against, and the independent verifier +`staged_verify.py`. + +## 13. Earlier layered carriage + +A prototype MoQ binding in this repository carries a layer index in an object +header (`mrl_layer`, with `mrl_total_layers` and a subscriber's `max_mrl_layer`). For +an object that is a `WAI1` envelope with `refines`, a sink reads the layer from +`refines.layer`, which sits inside the envelope and so inside its content hash, and +ignores the header. + +## 14. Security considerations + +- A layer is not authenticated by its `refines`; §10 authenticates it. +- Deferral lets anyone mint a chain for a public base hash. §10 and §4.1 step 7 bound + what a sink presents. A conflict between two attested chains is evidence that an + authorized signer signed both, and the sink reports it. +- A hold bound (§4.3), a cost limit (Appendix A.5) and a chain budget (§4.5) are + resource limits. A layer's header declares its output, its symbols and its working + bytes before any symbol is decoded, so a sink refuses a layer past its limit or its + budget without decoding it. A sink's own work for a layer is linear in the + layer's payload bytes and declared symbols, up to a factor logarithmic in its + units: it parses the payload once, at step 6; decides each unit the layer covers + once for each offer or release of the layer; finds a unit's producer in `prev` by + bisection over its runs; and compares a unit's key at step 7 with the producers of + that unit's stages. Beyond the chain's state, it keeps a fixed amount per unit of + the base and per unit held. +- An encrypted layer ([confidentiality](confidentiality.md)) binds to the content hash + of its base's envelope as delivered, ciphertext included, and is decrypted before + §4.1 step 6. +- A sink keeps the record of a base whose chain it dropped (§4.4), so a base decoded + again cannot be refined along other producers than it had; the record counts in the + chain's state when the base is decoded again (§4.5), and, while the base is not + held, in the sink's record budget (§4.4). A sink that forgets a base, by + `StageSink::forget_base` in the reference or because its record budget is full, + treats it as never decoded and never attested: a layer that conflicted with the + forgotten history can apply, and step 5 no longer requires a receipt for it. A sink + sizes its record budget for the bases it may decode again. +- Where the base is attested (§10), a frozen unit presents only its base's output or + the output of one of the producers it recorded, replayed in order (§4.4): a party + that can sign conflicting layers can stop a unit's refinement, and cannot make it + present anything else. Over a base whose receipt the sink did not verify, anyone who learns the + base's hash can mint a layer, and the first one ready for a unit applies to it. + +## Appendix A. `WIR1` + +### A.1 Layout + +Every multi-byte field is little-endian; offsets are in bytes. + +| off | size | field | rule | +|---|---|---|---| +| 0 | 4 | magic `WIR1` | | +| 4 | 1 | version | `1` | +| 5 | 1 | `elem` | `1` = u8, `2` = i16 | +| 6 | 1 | `predictor` | `0` identity, `1` up2, `2` rate | +| 7 | 1 | `table_mode` | `0` one table for every unit, `1` one per unit | +| 8 | 4 | `n_units` | ≥ 1 | +| 12 | 4 | `d0` | ≥ 1: rows (picture), samples per channel (audio), planes (splat) | +| 16 | 4 | `d1` | ≥ 1: columns, `1`, the planes' width | +| 20 | 4 | `d2` | ≥ 1: channels (`3` for RGB8), channels, the planes' height | +| 24 | 4 | `rate_num` | predictor 2: the output rate, ≥ 1; otherwise `0` | +| 28 | 4 | `rate_den` | predictor 2: the rate of the stage below, ≥ 1; otherwise `0` | +| 32 | 4 | `q` | 1..=65535 | +| 36 | 4 | `offset` | `i32`: the symbol offset of every table | +| 40 | 2 | `cdf_len` | 3..=4097 | +| 42 | 2 | reserved | `0` | +| 44 | T·`cdf_len`·4 | the tables | T = 1 or `n_units`; each a table of [integer-payloads](integer-payloads.md) §2 | +| … | `n_units`·4 | stream lengths | `u32` each, a multiple of 4, ≥ 8 | +| … | Σ | the streams | one per unit, in unit order; nothing follows | + +Each stream is decoded as [integer-payloads](integer-payloads.md) §2 decodes a rANS +stream (precision 16, the bypass escape of 4-bit groups), against the unit's table at +`offset`. + +### A.2 The unit kind + +A capability fixes what its units hold (§6.1), and so the fields a payload may carry: + +- RGB8 (`wai.image.int_refine`, `wai.video.int_refine`): `elem` 1, `predictor` 0 or + 1, `d2` = 3; +- PCM (`wai.audio.int_refine`): `elem` 2, `predictor` 0 or 2, `d1` = 1; +- attribute tensor (`wai.splat.int_refine`): `elem` 1, `predictor` 0. + +### A.3 Order of the checks + +A parser checks, in this order, and refuses with the first that fails: + +1. the magic (`bad_magic`); +2. the version byte: present (`stream_length`), and 1 (`version`); +3. the 44-byte header: present (`stream_length`); +4. the reserved field (`reserved_nonzero`); +5. the header's fields, in the order `elem`, `predictor`, `table_mode`, `n_units`, + `d0`/`d1`/`d2`, the rate fields, `q`, `cdf_len`; then the unit kind's rules + (A.2) (`field_range`); +6. the caps, in this order (`too_large`): a unit's `d0·d1·d2 ≤ 2^26`; + `n_units ≤ 2^20`; `n_units·d0·d1·d2 ≤ 2^30`; +7. the tables, one at a time, each present (`stream_length`) then checked + (`cdf_malformed`); the stream lengths, present (`stream_length`), each a multiple + of 4 and at least 8 (`stream_length`); the streams, present (`stream_length`); +8. nothing after the last stream (`trailing`). + +Every size is formed in 64-bit arithmetic with overflow checked, and a size past 64 +bits is past the caps, so a 32-bit sink refuses exactly what a 64-bit one does. A +parser reads tables and lengths from the bytes the payload holds and allocates +nothing a payload's bytes do not justify. + +### A.4 Decode + +For each unit *u* the layer is applied to, with the unit's current output of shape +`(e0, e1, e2)`: + +1. The shape must be the one the predictor reads (`geometry_mismatch`), before any + symbol is decoded: `(d0, d1, d2)` for identity; `(⌈d0/2⌉, ⌈d1/2⌉, d2)` for up2; + `(n′, 1, d2)` with `⌊n′·rate_num/rate_den⌋ = d0` for rate. +2. For element *i* in raster order (`i = (a·d1 + b)·d2 + c`), decode the residual `r` + from *u*'s stream. A stream that runs out during a symbol's decode is refused at + that symbol (`stream_exhausted`), and nothing after it is decoded. The element is + `clamp(pred(i) + r·q)`, to `[0, 255]` for u8 and `[−32768, 32767]` for i16. `r` lies + in `[−2^31, 2^31 − 1]` and `|r·q| < 2^47`, so every step fits `i64`. +3. After the unit's last element, the stream ends canonically + ([integer-payloads](integer-payloads.md) §2) (`stream_noncanonical`). + +**Predictors**, with `prev` the unit's current output: + +- **identity**: `pred(i) = prev(i)`. +- **up2**: `prev` is `h′ × w′ × c` with `h′ = ⌈d0/2⌉`, `w′ = ⌈d1/2⌉` and `c = d2`, + so odd sizes are allowed. For output `(y, x, ch)`: `iy = y >> 1`, `fy = y & 1`, + `ix = x >> 1`, `fx = x & 1`, and `p(Y, X) = prev[min(Y, h′ − 1), min(X, w′ − 1), ch]`. + The prediction is `p(iy, ix)` when both are 0; + `(p(iy, ix) + p(iy, ix + 1) + 1) >> 1` when only `fx` is 1; + `(p(iy, ix) + p(iy + 1, ix) + 1) >> 1` when only `fy` is 1; and + `(p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2` when + both are. This is the half-pel arithmetic of [integer-payloads](integer-payloads.md) + §3. +- **rate**: `prev` holds `n′` samples of `c = d2` interleaved channels. For output + sample *i* and channel *ch*: `s = i·rate_den`, `lo = s div rate_num`, + `Ï� = s mod rate_num`, `hi = min(lo + 1, n′ − 1)`, `a = prev[lo·c + ch]`, + `b = prev[hi·c + ch]`, and the prediction is `rdiv(a·rate_num + (b − a)·Ï�, rate_num)`, + where `rdiv(n, d) = sign(n)·⌊(2|n| + d) / 2d⌋` rounds half away from zero. This is + `a + (b − a)·Ï�/rate_num` rounded half away from zero, the linear interpolation at + source position `i·rate_den/rate_num`, in exact integer arithmetic. + +### A.5 Declared cost + +A payload's header declares, before any symbol is decoded: its output, +`n_units·d0·d1·d2` elements of 1 byte (u8) or 2 bytes (i16); its symbols, +`n_units·d0·d1·d2`, one entropy-decode step each; and the most bytes its decode holds +at once beyond the payload and the stages below, its parsed tables (`4·T·cdf_len`), +the index of its streams (`8·n_units`) and one unit's output while it is built. It +runs no network. A sink that applies a limit compares this cost with it after the +parse and before any stream is decoded (§4.1 step 6, `cost-over-limit`), and counts +it in the chain's state while it retains the layer (§4.5). + +### A.6 Refusal codes + +The list is closed, and its codes are those [integer-payloads](integer-payloads.md) §8 +uses for the same faults: `bad_magic`, `version`, `reserved_nonzero`, `field_range`, +`too_large`, `cdf_malformed`, `stream_length`, `trailing`, `stream_exhausted`, +`stream_noncanonical` and `geometry_mismatch`. A parser reports the first that +applies, in the order of A.3. §4.1 runs A.4 in two parts: step 6 decodes every unit's +stream to its end (A.4 steps 2 and 3, which read no prediction), so +`stream_exhausted` and `stream_noncanonical` are the envelope's `payload-malformed`, +reported before any unit's shape is read; step 9 then checks each unit's shape (A.4 +step 1), so `geometry_mismatch` is that unit's `geometry-mismatch`. A payload whose +`n_units` is not the number of units the layer covers is the envelope's +`payload-malformed` with the code `field_range`. Every other code is the envelope's +`payload-malformed`. diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 97be35c03..ee08dbeea 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -101,6 +101,20 @@ "edition": null, "registered": "2026-06-27" }, + { + "capability": "wai.audio.int_refine", + "kind": "refinement", + "status": "registered", + "media": "audio", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.audio.ivas", "kind": "payload", @@ -427,6 +441,20 @@ "edition": null, "registered": "2026-06-12" }, + { + "capability": "wai.image.int_refine", + "kind": "refinement", + "status": "registered", + "media": "image", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.image.jp2", "kind": "payload", @@ -965,6 +993,20 @@ "edition": null, "registered": "2026-06-28" }, + { + "capability": "wai.splat.int_refine", + "kind": "refinement", + "status": "registered", + "media": "volumetric", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.splat.sog", "kind": "payload", @@ -1211,6 +1253,20 @@ "edition": null, "registered": "2026-06-27" }, + { + "capability": "wai.video.int_refine", + "kind": "refinement", + "status": "registered", + "media": "video", + "role": "refinement", + "tier": "lossy", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Refinement capabilities (staged delivery)", + "edition": null, + "registered": "2026-10-01" + }, { "capability": "wai.video.j2k", "kind": "payload", diff --git a/wai/registry/capabilities.schema.json b/wai/registry/capabilities.schema.json index cbec87f0c..1d1db5385 100644 --- a/wai/registry/capabilities.schema.json +++ b/wai/registry/capabilities.schema.json @@ -43,6 +43,7 @@ "enum": [ "payload", "companion", + "refinement", "derivation", "record" ] @@ -72,6 +73,7 @@ "standalone", "companion", "enhancement", + "refinement", null ] }, diff --git a/wai/staged-conformance/.gitattributes b/wai/staged-conformance/.gitattributes new file mode 100644 index 000000000..eff1df73a --- /dev/null +++ b/wai/staged-conformance/.gitattributes @@ -0,0 +1,2 @@ +# Hashed files: a line-ending conversion on checkout would change every digest. +* -text diff --git a/wai/staged-conformance/README.md b/wai/staged-conformance/README.md new file mode 100644 index 000000000..112002b05 --- /dev/null +++ b/wai/staged-conformance/README.md @@ -0,0 +1,93 @@ +# staged-conformance — staged delivery + +The corpus of [`extensions/staged-delivery.md`](../extensions/staged-delivery.md) §12: +stage sets of a base and refinement layers, and the steps a sink takes with them. + +**Stage-equivalence.** For each case, a conforming sink that offers its layers, decodes +its bases and expires its holds in the order and at the times `case.json` states +reports, at every step, the reasons the case states per envelope and per unit, and +ends with every unit at the stated stage, producer, frozen flag, tier, shape and stage +hash (SHA-256 of the unit's canonical buffer, SPEC §7 *Digests*; stated only where the +unit holds `decode-equivalence`). The reasons are §4.2's; a `payload-malformed` +report names the `WIR1` code (Appendix A.6), and a `cost-over-limit` report the figure +past the limit (`output bytes`, `symbols` or `working bytes`). A step reports once per +envelope it decided: what was refused for the whole envelope, or per unit what was +applied, held and refused (a unit held and decided within the same step is reported by +its decision); `held_for_base` marks an envelope held for a base the sink has not +decoded. The order of a step's reports is not part of the outcome. + +**Evidence.** Each offer states what the sink verified of the layer's delivery receipt +(§10), as a token: `delivered-verified`, a receipt verified through jwp-receipts §3 +step 6 over the envelope's bytes as received; `inclusion-verified`, a receipt verified +without those bytes (§3 steps 1, 2, 3 and 5), which never authorizes an application; +`none`, no receipt verified. A base's `attested` states whether the sink verified a +receipt for it, so that step 5 requires `delivered-verified` of its layers. + +- `cases//` — one case: `case.json`, each base's envelope (`base*.wai`) and its + stage-0 output (`stage0*.bin`: the units' canonical buffers concatenated, in unit + order), and the layers (`*.wai`). `case.json` states each base's capability it was + decoded through, whether through a fallback, whether the sink verified a delivery + receipt for it, whether its pin resolved, its tier, its units and the live clock + position of its first unit (`first_unit`, §8); the sink's hold bound, cost limit, + chain budget (§4.5), record budget (§4.4) and live schedule; the steps (offer, decode + a base, expire, drop a base's chain, attest a base); and what the sink reports and + holds. +- `wai2/` — `WAI2` envelopes with refinement components (SPEC §7.1) and + `index.json`, which states for each what `check_component_roles` reports, whether + each `envelope_manifest` matches its entry, the content hash of each component's + exploded envelope (`null` for an entry that cannot be exploded, the others exploded + all the same), and entry 0's refinements: a live-style chain packed with its + manifests verbatim, which explodes back to its sources (`*.src.wai`) byte for + byte; an envelope written with ids and no `envelope_manifest`; a refinement whose + payload does not match its `sha256`; an `envelope_manifest` that does not match its + entry, and one that names a fallback; each refinement rule of SPEC §7.1 broken once. + Its `packs` list gives `WAI1` envelopes and whether a packer packs them: bytes before + or after a manifest's JSON value, bytes after a payload, and a manifest that inside a + `WAI2` entry passes the JSON depth limit are refused. +- `schedule.json` — arrival traces, and the unit-by-unit presentation and stage a + sink gives each (§8), with the count of units presented below the stage their + layers would have reached on time. +- `index.json` — the cases and their final clip hashes. + +**The content is synthetic**: pictures, frames and PCM computed by integer functions in +the generator. The bases of the integer codecs are the committed vectors under +[`../wai-rs/tests/vectors/`](../wai-rs/tests/vectors/), decoded against their +parameter sets with their pins; the `wai.video.int_motion` bases are coded by a +keyframe-0 encoder in the generator (zero motion, one block per frame), and the PNG +and FLAC bases are written by small writers in the generator (stored deflate with +the five row filters; VERBATIM subframes). + +**What checks each base's stage 0.** + +| base | checked by | +|---|---| +| `wai.video.int_motion` (`WIV1`, keyframe 0) | `wai_staged_vectors verify` (the reference decode) and `staged_verify.py` (an independent decode) | +| `wai.image.png` | `staged_verify.py` (zlib and the row filters); a fixture to `wai_staged_vectors`, which builds without `codecs` | +| `wai.audio.flac` | `staged_verify.py` (VERBATIM subframes, both CRCs); a fixture to `wai_staged_vectors` | +| `wai.neural.int_hyper`, `wai.video.int_hyper` (keyframe 1), `wai.audio.int_codec`, `wai.splat.int_codec` | `wai_staged_vectors verify` (the reference decode, pinned). Each base's payload is a committed vector's bitstream under [`../wai-rs/tests/vectors/`](../wai-rs/tests/vectors/), and `staged_verify.py`, which has no decoder for these, checks `stage0.bin` against the decoded output that vector's `sha256.json` states (`int_hyper/tiny`, `int_video/video_nk` frame by frame, `int_audio/audio`, `int_splat/splat`); `int_kernels_vectors` checks those sidecars against the decoders. The cross-architecture goldens of [`../byte-exact-conformance/`](../byte-exact-conformance/) also hold the `int_hyper` image's stage 0 | + +Every layer application, in every case, is checked by both. + +**Verifiers.** + +- `wai_staged_vectors verify .` (from `../wai-rs`, features + `staged,video_int,audio_int,splat_int`): regenerates every file and requires it + byte for byte, replays every case through `staged::StageSink`, decodes the bases it + can and checks their tiers, and replays the schedule. `wai_staged_vectors gen .` + writes the corpus. +- `staged_verify.py .`: pure Python with the standard library, sharing no code with + `wai-rs`. It reimplements, from the extension and SPEC: a strict reader of envelope + JSON (UTF-8 with no byte-order mark, no member named twice, no NaN or Infinity, no + number past the float range, no lone surrogate, the depth limit, `-0`, a fraction or + an exponent as no integer) with the members SPEC §3 and §7.1 require; the content + hash; `refines`; `WIR1` (its parse in order, its cost, its streams, the three + predictors and `rdiv`); the sink as §4 states it, each envelope through §4.1's + numbered steps in turn, batches as §4.4 forms them, holds and their bound, frozen + units, the chain budget, live deadlines on one saturating clock across bases, and + receipt evidence; tiers and stage hashes; the PNG and FLAC decodes; the schedule; + exploding a `WAI2` component (reading the manifest's members as the text they are) + with the refinement role rules; and what a packer refuses. It takes BLAKE3 from + [`../claim-conformance/_blake3.py`](../claim-conformance/_blake3.py) and the rANS + decoder and the `WIV1` keyframe-0 decode from + [`../int-payload-conformance/int_payload_verify.py`](../int-payload-conformance/int_payload_verify.py), + both written from their specifications. diff --git a/wai/staged-conformance/cases/attested_across_drop/L1.wai b/wai/staged-conformance/cases/attested_across_drop/L1.wai new file mode 100644 index 000000000..224a5ba45 Binary files /dev/null and b/wai/staged-conformance/cases/attested_across_drop/L1.wai differ diff --git a/wai/staged-conformance/cases/attested_across_drop/base_0.wai b/wai/staged-conformance/cases/attested_across_drop/base_0.wai new file mode 100644 index 000000000..44ce7cec6 Binary files /dev/null and b/wai/staged-conformance/cases/attested_across_drop/base_0.wai differ diff --git a/wai/staged-conformance/cases/attested_across_drop/base_1.wai b/wai/staged-conformance/cases/attested_across_drop/base_1.wai new file mode 100644 index 000000000..44ce7cec6 Binary files /dev/null and b/wai/staged-conformance/cases/attested_across_drop/base_1.wai differ diff --git a/wai/staged-conformance/cases/attested_across_drop/case.json b/wai/staged-conformance/cases/attested_across_drop/case.json new file mode 100644 index 000000000..18756086d --- /dev/null +++ b/wai/staged-conformance/cases/attested_across_drop/case.json @@ -0,0 +1,235 @@ +{ + "format": "wai-staged-case/1", + "name": "attested_across_drop", + "about": "an attested base's layer 1 applies with its receipt; the chain is dropped and the base decoded again by a decode that carries no receipt: it is still attested, so layer 1 with no receipt is refinement-unattested, and with its receipt it replays", + "bases": [ + { + "file": "base_0.wai", + "object": "6fd89fffe05e4ef6defa6726fd1bcac8a578e85210a15cdae65b4654d5fe7e6b", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "84a4afb831e2d2c98b261aaa08b1f7c1e49a7df9c3a1f7327bf87ec140cda4ae", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "6fd89fffe05e4ef6defa6726fd1bcac8a578e85210a15cdae65b4654d5fe7e6b", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "84a4afb831e2d2c98b261aaa08b1f7c1e49a7df9c3a1f7327bf87ec140cda4ae", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "delivered-verified" + }, + { + "op": "remove", + "index": 0, + "at_ms": 2 + }, + { + "op": "base", + "index": 1, + "at_ms": 3 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 5, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "20c254f70a8d4de095e3064f92bc01bd93cbd7fbb2a3266bcec7e9a8902b1bc2" + }, + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2724b6f993f4946ecfaa99aa18dffcb0ba2125592be2bb146a8b754fe74fefab" + } + ], + "clip_sha256": "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "20c254f70a8d4de095e3064f92bc01bd93cbd7fbb2a3266bcec7e9a8902b1bc2" + }, + { + "stage": 1, + "producer": "0ccfe9af9e31cf9014699a20610168f81692920e29adebd6ea6d277da3e8ca3e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2724b6f993f4946ecfaa99aa18dffcb0ba2125592be2bb146a8b754fe74fefab" + } + ], + "clip_sha256": "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/attested_across_drop/stage0_0.bin b/wai/staged-conformance/cases/attested_across_drop/stage0_0.bin new file mode 100644 index 000000000..a931ec801 Binary files /dev/null and b/wai/staged-conformance/cases/attested_across_drop/stage0_0.bin differ diff --git a/wai/staged-conformance/cases/attested_across_drop/stage0_1.bin b/wai/staged-conformance/cases/attested_across_drop/stage0_1.bin new file mode 100644 index 000000000..a931ec801 Binary files /dev/null and b/wai/staged-conformance/cases/attested_across_drop/stage0_1.bin differ diff --git a/wai/staged-conformance/cases/attested_after_decode/L1_f0.wai b/wai/staged-conformance/cases/attested_after_decode/L1_f0.wai new file mode 100644 index 000000000..c8f26e670 Binary files /dev/null and b/wai/staged-conformance/cases/attested_after_decode/L1_f0.wai differ diff --git a/wai/staged-conformance/cases/attested_after_decode/L1_f1.wai b/wai/staged-conformance/cases/attested_after_decode/L1_f1.wai new file mode 100644 index 000000000..e3e86e1aa Binary files /dev/null and b/wai/staged-conformance/cases/attested_after_decode/L1_f1.wai differ diff --git a/wai/staged-conformance/cases/attested_after_decode/base_0.wai b/wai/staged-conformance/cases/attested_after_decode/base_0.wai new file mode 100644 index 000000000..562a89da7 Binary files /dev/null and b/wai/staged-conformance/cases/attested_after_decode/base_0.wai differ diff --git a/wai/staged-conformance/cases/attested_after_decode/base_1.wai b/wai/staged-conformance/cases/attested_after_decode/base_1.wai new file mode 100644 index 000000000..562a89da7 Binary files /dev/null and b/wai/staged-conformance/cases/attested_after_decode/base_1.wai differ diff --git a/wai/staged-conformance/cases/attested_after_decode/case.json b/wai/staged-conformance/cases/attested_after_decode/case.json new file mode 100644 index 000000000..9c1c68be5 --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/case.json @@ -0,0 +1,255 @@ +{ + "format": "wai-staged-case/1", + "name": "attested_after_decode", + "about": "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + "bases": [ + { + "file": "base_0.wai", + "object": "ca89ed081688f3f5370fc6d96e471d085b7606850f9d3e55c2a876f3bcbc6545", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "f6a56010bb71a9e7848c764e499e81f834664224e35ec9c978f794ff0e3c8848", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "ca89ed081688f3f5370fc6d96e471d085b7606850f9d3e55c2a876f3bcbc6545", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "f6a56010bb71a9e7848c764e499e81f834664224e35ec9c978f794ff0e3c8848", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_f0.wai", + "object": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "bytes": 535 + }, + { + "file": "L1_f1.wai", + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "bytes": 527 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 2 + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 6, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c164503f2d738b8729252a24bf6e94300979aa7aa11427195f1221cfffd81e98" + }, + { + "stage": 1, + "producer": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f1029000aa1799224e89bc1b00021dad8cc869e9708b8b7bb94a7a63aea27d80" + } + ], + "clip_sha256": "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "25eac95b83d649a906696dae8e163fd4ac24570371602c10cbb3e2a8e1c54d2a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c164503f2d738b8729252a24bf6e94300979aa7aa11427195f1221cfffd81e98" + }, + { + "stage": 1, + "producer": "c0ab531af4d89041d976b16c74a5d7d2595f6d2972d91a065d4e4114699e3a02", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f1029000aa1799224e89bc1b00021dad8cc869e9708b8b7bb94a7a63aea27d80" + } + ], + "clip_sha256": "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin b/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin new file mode 100644 index 000000000..7e998917b --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/stage0_0.bin @@ -0,0 +1 @@ +(0@0@P8@P@PhPXhPhx`xˆxx�8HPHPhH`hXhp`xˆpxˆx€˜x� H``P`p¨Ðøpxˆx��x� ˆ˜¨˜¨°Xp€¸Øÿ¸àÿÈèÿˆ ¨�¨¨˜¨À¨ÀÐÀàÿÀðÿÈøÿØÿÿèÿÿ¨¸È°ÈØÈÐØ��¨Øÿÿàÿÿèÿÿ¸ÀÐÀØàÈÐèÐàø˜°° °ÈøÿÿÀÀØÐààØàðØðøèÿÿ¨ÀȸÈàÈÐèÐØèàèÿèðÿðÿÿÿÿÿ(0@0@P8@P@PhPXhPhxhxˆxx�8HPHPhH`hXhp`x€pxˆx€˜x� H``P`p`p€px€xˆ�x� ˆ˜¨˜ °`p€px‡p€—€ˆŸÈÿÿ�¨¨˜¨À¨ÀÐx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÀØÈÐØˆ� �§¯ØÿÿèÿÿÿÿÿÿÿÿÿÿÿÐØø˜°° ¨È°·ÏÿÿÿÿÿÿÿÿÿØðøèÿÿ¨ÀÀ¸ÈàÈÐèÐØàÿÿÿèðÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin b/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin new file mode 100644 index 000000000..7e998917b --- /dev/null +++ b/wai/staged-conformance/cases/attested_after_decode/stage0_1.bin @@ -0,0 +1 @@ +(0@0@P8@P@PhPXhPhx`xˆxx�8HPHPhH`hXhp`xˆpxˆx€˜x� H``P`p¨Ðøpxˆx��x� ˆ˜¨˜¨°Xp€¸Øÿ¸àÿÈèÿˆ ¨�¨¨˜¨À¨ÀÐÀàÿÀðÿÈøÿØÿÿèÿÿ¨¸È°ÈØÈÐØ��¨Øÿÿàÿÿèÿÿ¸ÀÐÀØàÈÐèÐàø˜°° °ÈøÿÿÀÀØÐààØàðØðøèÿÿ¨ÀȸÈàÈÐèÐØèàèÿèðÿðÿÿÿÿÿ(0@0@P8@P@PhPXhPhxhxˆxx�8HPHPhH`hXhp`x€pxˆx€˜x� H``P`p`p€px€xˆ�x� ˆ˜¨˜ °`p€px‡p€—€ˆŸÈÿÿ�¨¨˜¨À¨ÀÐx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÀØÈÐØˆ� �§¯ØÿÿèÿÿÿÿÿÿÿÿÿÿÿÐØø˜°° ¨È°·ÏÿÿÿÿÿÿÿÿÿØðøèÿÿ¨ÀÀ¸ÈàÈÐèÐØàÿÿÿèðÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/attested_before_decode/L1.wai b/wai/staged-conformance/cases/attested_before_decode/L1.wai new file mode 100644 index 000000000..38e234510 Binary files /dev/null and b/wai/staged-conformance/cases/attested_before_decode/L1.wai differ diff --git a/wai/staged-conformance/cases/attested_before_decode/base.wai b/wai/staged-conformance/cases/attested_before_decode/base.wai new file mode 100644 index 000000000..9e990fd8f Binary files /dev/null and b/wai/staged-conformance/cases/attested_before_decode/base.wai differ diff --git a/wai/staged-conformance/cases/attested_before_decode/case.json b/wai/staged-conformance/cases/attested_before_decode/case.json new file mode 100644 index 000000000..420b327b4 --- /dev/null +++ b/wai/staged-conformance/cases/attested_before_decode/case.json @@ -0,0 +1,145 @@ +{ + "format": "wai-staged-case/1", + "name": "attested_before_decode", + "about": "the sink verifies the base's receipt before it decodes the base: the decode, which carries no receipt, gives an attested base, so layer 1 with no receipt is refinement-unattested and applies with its receipt", + "bases": [ + { + "file": "base.wai", + "object": "21aef5d23168e3198c5abbbfef160382965005bb9099263ac5983f703fad2231", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "fd636a41da98bfdf266929ba483a8170e05dfe22b58d1b7fb787d12385dd2581", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "bd3510c59be44e8a4870496cb5dcc9e534f55f9e04d576572dc3b408146daa81", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "attest", + "index": 0, + "at_ms": 0 + }, + { + "op": "base", + "index": 0, + "at_ms": 1 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 3, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [], + [ + { + "object": "bd3510c59be44e8a4870496cb5dcc9e534f55f9e04d576572dc3b408146daa81", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "bd3510c59be44e8a4870496cb5dcc9e534f55f9e04d576572dc3b408146daa81", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "bd3510c59be44e8a4870496cb5dcc9e534f55f9e04d576572dc3b408146daa81", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3b00f93323ba39d7eb422b204f9484f1c8a8dd53212d98bb59ae328e4a58d37f" + }, + { + "stage": 1, + "producer": "bd3510c59be44e8a4870496cb5dcc9e534f55f9e04d576572dc3b408146daa81", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3203699f5689e8c009dac0a47a17a433ed898e8b9ad830515870861dca58c02c" + } + ], + "clip_sha256": "893d5537bffddf6524f550c8e283f52ae9db467d9da05c3f7cb2527ab2e49e22", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/attested_before_decode/stage0.bin b/wai/staged-conformance/cases/attested_before_decode/stage0.bin new file mode 100644 index 000000000..4d668d943 --- /dev/null +++ b/wai/staged-conformance/cases/attested_before_decode/stage0.bin @@ -0,0 +1 @@ +  0 0000H0HX@H`HXpX`p0000@(@X@P`HX`Hhp`hxh€€(@P@PXˆ°ØPhp`px`pˆp€�€ˆ HPh˜Àà¨Èè Ðÿp€�€ˆ ˆ˜¨ˆ¨¸¨Àð¨Øÿ°àÿÀèÿÈøÿ�¨°˜°°¨¸Àpp�¸èÿÈèÿØÿÿ ¨¸¨°À°ÈиÈàx� ˆ °Øÿÿ ¸¸¨ÀÐÀÈÐÈÐàÐè𘠰 ¨¸°¸À¸ÀÐÈÈèÐÐààèøàðÿ  8 0800H0HXHH`H`pX`p0000H(HX@P`HX`Phphhxh€€(@P@PXHP`Php`px`pˆp€�€ˆ PPhP`h`hp`p‡¸àÿ€ˆ ˆ˜¨ˆ¨°``x`x‡h€�ÀèÿÈðÿØÿÿ˜°°¨°Àpp�pˆ—ÈèÿØÿÿèÿÿðÿÿðÿÿ°Èàx� ˆ °�Ÿ·èÿÿðÿÿÿÿÿÈÐàÈà𘠰 ¨¸°¸À¸ÀÐÿÿÿÐÐàààøØðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai new file mode 100644 index 000000000..38836a01b Binary files /dev/null and b/wai/staged-conformance/cases/audio_flac_rate_ladder/L1.wai differ diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai new file mode 100644 index 000000000..ce9b7ec72 Binary files /dev/null and b/wai/staged-conformance/cases/audio_flac_rate_ladder/L2.wai differ diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai b/wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai new file mode 100644 index 000000000..5ee396a23 Binary files /dev/null and b/wai/staged-conformance/cases/audio_flac_rate_ladder/base.wai differ diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json new file mode 100644 index 000000000..7b6c988a3 --- /dev/null +++ b/wai/staged-conformance/cases/audio_flac_rate_ladder/case.json @@ -0,0 +1,124 @@ +{ + "format": "wai-staged-case/1", + "name": "audio_flac_rate_ladder", + "about": "a stereo FLAC base at 8 kHz (a stage-0 fixture here), a rate layer to 16 kHz, then one to 48 kHz", + "bases": [ + { + "file": "base.wai", + "object": "1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8", + "decoded_via": "wai.audio.flac", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 400, + 1, + 2 + ], + "elem": "i16" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "bfd17b4865c3c7a982f18a397c05e5d1df483bbb98bd29e2e9e2a672afe3063f", + "decode": { + "kind": "flac" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab", + "bytes": 12711 + }, + { + "file": "L2.wai", + "object": "753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c", + "bytes": 18367 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "53a48057cb99b88fd235b9165799122df2d1b55a4b84245112a5a5ea948458ab", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "753e8ac465d347547c688690eb51615f19118c0a4c17490499fbb31aefd5cd0c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 2400, + 1, + 2 + ], + "stage_sha256": "6280c4edf77a03b58c588aaa10646b083227bb56cd49c7966ce75a134ff4f7c1" + } + ], + "clip_sha256": "0408de38e23d42bd6af8064e109c73ab90582eaad588c68fbcf8b25fe9cf4c29", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/audio_flac_rate_ladder/stage0.bin b/wai/staged-conformance/cases/audio_flac_rate_ladder/stage0.bin new file mode 100644 index 000000000..60a9a7417 Binary files /dev/null and b/wai/staged-conformance/cases/audio_flac_rate_ladder/stage0.bin differ diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/L1.wai b/wai/staged-conformance/cases/audio_int_codec_1l/L1.wai new file mode 100644 index 000000000..7f0efde87 Binary files /dev/null and b/wai/staged-conformance/cases/audio_int_codec_1l/L1.wai differ diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/base.wai b/wai/staged-conformance/cases/audio_int_codec_1l/base.wai new file mode 100644 index 000000000..205d92a2a Binary files /dev/null and b/wai/staged-conformance/cases/audio_int_codec_1l/base.wai differ diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/case.json b/wai/staged-conformance/cases/audio_int_codec_1l/case.json new file mode 100644 index 000000000..ce9fd47cf --- /dev/null +++ b/wai/staged-conformance/cases/audio_int_codec_1l/case.json @@ -0,0 +1,103 @@ +{ + "format": "wai-staged-case/1", + "name": "audio_int_codec_1l", + "about": "a pinned wai.audio.int_codec base, then an identity layer", + "bases": [ + { + "file": "base.wai", + "object": "d45db2bc98dd38873553beb15fdb026a4e6b7fd7c8c7dbe4f4a0b6fcc0709a54", + "decoded_via": "wai.audio.int_codec", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8192, + 1, + 1 + ], + "elem": "i16" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "b8ff83a442c112661ec7a41d6e51cb32559510a699a0e9bb37fa301f8952978a", + "decode": { + "kind": "int", + "set": { + "model.wam": "wai-rs/tests/vectors/int_audio/audio_model.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "e462824f3bf3827ec4d8861e1c93f64fabf2bec20303013347eee7a271f968a2", + "bytes": 19375 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "e462824f3bf3827ec4d8861e1c93f64fabf2bec20303013347eee7a271f968a2", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "e462824f3bf3827ec4d8861e1c93f64fabf2bec20303013347eee7a271f968a2", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8192, + 1, + 1 + ], + "stage_sha256": "b9ca33fc990f6a28c479c4a8193dca0bd9faa1d8dedf9cef9614513da4b1d0bd" + } + ], + "clip_sha256": "ec7a4ad312d40a6ebdb33a5b55df02967415c3eb8f065c72e427d1a189b8810d", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/audio_int_codec_1l/stage0.bin b/wai/staged-conformance/cases/audio_int_codec_1l/stage0.bin new file mode 100644 index 000000000..f6acedd95 Binary files /dev/null and b/wai/staged-conformance/cases/audio_int_codec_1l/stage0.bin differ diff --git a/wai/staged-conformance/cases/base_not_admitted/L1.wai b/wai/staged-conformance/cases/base_not_admitted/L1.wai new file mode 100644 index 000000000..0f83e75c7 Binary files /dev/null and b/wai/staged-conformance/cases/base_not_admitted/L1.wai differ diff --git a/wai/staged-conformance/cases/base_not_admitted/base.wai b/wai/staged-conformance/cases/base_not_admitted/base.wai new file mode 100644 index 000000000..1ba05ab62 Binary files /dev/null and b/wai/staged-conformance/cases/base_not_admitted/base.wai differ diff --git a/wai/staged-conformance/cases/base_not_admitted/case.json b/wai/staged-conformance/cases/base_not_admitted/case.json new file mode 100644 index 000000000..a25614378 --- /dev/null +++ b/wai/staged-conformance/cases/base_not_admitted/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "base_not_admitted", + "about": "a wai.image.int_refine layer over a wai.video.int_motion base: base-not-admitted", + "bases": [ + { + "file": "base.wai", + "object": "c7d8d9fe61ff4c4d71f082b5134fc15a12932c1b5305ec1a6a47b8b1f08c4948", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "7cc3b7cdbd202cf8705313c7ed5edf6e93bf2893246e6b799f23b6a237ea592d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "ccfd9b3c75deb27f1af1a96c7fe9b5f3ed2555e8d5a07d7350d886dd3f166ba7", + "bytes": 415 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ccfd9b3c75deb27f1af1a96c7fe9b5f3ed2555e8d5a07d7350d886dd3f166ba7", + "envelope": "base-not-admitted", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "c7d8d9fe61ff4c4d71f082b5134fc15a12932c1b5305ec1a6a47b8b1f08c4948", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "935d3f83da4f483b6fd7c956e90f3af287f53c44e9c74dc18d29e1384787ddde" + }, + { + "stage": 0, + "producer": "c7d8d9fe61ff4c4d71f082b5134fc15a12932c1b5305ec1a6a47b8b1f08c4948", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "243d926cc424dffc093682b48e3b60dd0b1b83b023b076660aa1b6e45c98f248" + } + ], + "clip_sha256": "e532573c38be3eeb5dddc8c5ce7c0af82beb3a5bfd71556f4581acdd7a47508c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/base_not_admitted/stage0.bin b/wai/staged-conformance/cases/base_not_admitted/stage0.bin new file mode 100644 index 000000000..00eba569f --- /dev/null +++ b/wai/staged-conformance/cases/base_not_admitted/stage0.bin @@ -0,0 +1 @@ + 88@08H0HX8H`H`hXhpXxˆ(8@08XHP`PPhP`x`xxpp�p�˜@PP@`h�Àè`pxhx€pˆ˜x� � ¨X`h Ðè¨Ðø¸èÿxˆ �˜¨� ¸¨°¸¨Øø°àÿÈðÿÐøÿØøÿ¨¨À ¸À¸ÈÐp� ÐðÿÐÿÿØÿÿ ÀÀ°ÈÐÈÈàÐÐ舘   °èÿÿ°ÀÈÀÈØÈØØÈàøØèÿ¨°À¨¸Ð°ÀØÈÐàØØèØàøàøÿèÿÿ 8 8@08H0PX8HhH`hXhp`xˆ08H08XHPhPPhP`x`xxpx�p�˜HPX@`hH`p`pxpx€pˆ˜x� � ¨X`hXpp`p€pˆ—Àèÿ�˜¨�˜¸¨¨¸`x€h€—€�—ÐøÿØøÿèÿÿ °À¸ÈÐp�˜ˆ�§ÐÿÿØÿÿèÿÿðÿÿÿÿÿÐÐàˆ˜   °¨¯ÇøÿÿÿÿÿÿÿÿÈàøØèÿ¨°À¨°Ð°ÀÐÈÐàÿÿÿØàøàøÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/base_readded/L1a.wai b/wai/staged-conformance/cases/base_readded/L1a.wai new file mode 100644 index 000000000..7406fa2ef Binary files /dev/null and b/wai/staged-conformance/cases/base_readded/L1a.wai differ diff --git a/wai/staged-conformance/cases/base_readded/L1b.wai b/wai/staged-conformance/cases/base_readded/L1b.wai new file mode 100644 index 000000000..55dc6764f Binary files /dev/null and b/wai/staged-conformance/cases/base_readded/L1b.wai differ diff --git a/wai/staged-conformance/cases/base_readded/base.wai b/wai/staged-conformance/cases/base_readded/base.wai new file mode 100644 index 000000000..f32687d64 Binary files /dev/null and b/wai/staged-conformance/cases/base_readded/base.wai differ diff --git a/wai/staged-conformance/cases/base_readded/case.json b/wai/staged-conformance/cases/base_readded/case.json new file mode 100644 index 000000000..9ae8b939e --- /dev/null +++ b/wai/staged-conformance/cases/base_readded/case.json @@ -0,0 +1,185 @@ +{ + "format": "wai-staged-case/1", + "name": "base_readded", + "about": "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", + "bases": [ + { + "file": "base.wai", + "object": "9e8b544b5ba77b86d9986da2b8e6964b7a708e4e2aa200404e58ba749330003a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5f4d2ea2a606d3f3de14d288069926538e01a018e6f10ac019089824c6657ecc", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "ad1cfba24ca80c3fd7a1f85c2273de0fa6ec172adf96409cc0056bff48ef9136", + "bytes": 595 + }, + { + "file": "L1b.wai", + "object": "d0d98b05f3e4b5f81e27b9d8a022cb8b51bc817264bb0f71dbdc7b4da3e449bd", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 3 + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 4, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ad1cfba24ca80c3fd7a1f85c2273de0fa6ec172adf96409cc0056bff48ef9136", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "d0d98b05f3e4b5f81e27b9d8a022cb8b51bc817264bb0f71dbdc7b4da3e449bd", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [], + [ + { + "object": "d0d98b05f3e4b5f81e27b9d8a022cb8b51bc817264bb0f71dbdc7b4da3e449bd", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "ad1cfba24ca80c3fd7a1f85c2273de0fa6ec172adf96409cc0056bff48ef9136", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e42e01c170f901fc31b7699ba7583378d4589db98ff8e1483eceaf553615ea16" + }, + { + "stage": 1, + "producer": "ad1cfba24ca80c3fd7a1f85c2273de0fa6ec172adf96409cc0056bff48ef9136", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6d64fbd60316861023b12fdda81b530bde59aa5d234fbeb428c4433f425d6820" + } + ], + "clip_sha256": "11513b7b7c495cc504f4d6deb308d15c95c08aaca41e85479f2359764f49046d", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/base_readded/stage0.bin b/wai/staged-conformance/cases/base_readded/stage0.bin new file mode 100644 index 000000000..70f6dbf79 --- /dev/null +++ b/wai/staged-conformance/cases/base_readded/stage0.bin @@ -0,0 +1 @@ + 0 0@(8H0@H@XXHXhPpphh€(@H8@P8HP@XhX`xXpˆppˆpˆ˜8@P@``˜ÀàXh€hpˆxˆ˜x˜ ˆ˜ P`p Ðð°ØøÀàÿx� €˜ ˆ¨¨ °¸ Ðø°Øÿ¸ðÿÐøÿØÿÿ ¨À¨°È¨ÈÐx�˜ÈøÿÐøÿèÿÿ ¸À¨¸Ð¸ÐÐÐÐàˆ�¨˜¨°ðÿÿ°¸È¸ÈØÈÈàÐØðØàø °È¨¸Ð°ÀظÐàÈàèÐèðàðÿðÿÿ 0 0H(8H0@H@XXHXhPpphh€(@H8HP8HP@XhX`xXpˆppˆpˆ˜8@X@``P`hXh€hp€x€˜€� ˆ˜ P`pXpxhx€x€�Àðÿ€˜ ˆ¨¨ °¸`p€hx�x�ŸÐðÿØÿÿàÿÿ¨°È¨ÈЀ�˜ˆ˜ŸÐðÿèÿÿèÿÿðÿÿÿÿÿÐÐàˆ� ˜¨°¨¯·øÿÿÿÿÿÿÿÿÐØðØàø °À¨¸Ð°ÀظÐàÿÿÿÐèðàðÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/base_via_fallback/L1.wai b/wai/staged-conformance/cases/base_via_fallback/L1.wai new file mode 100644 index 000000000..caa04fdd0 Binary files /dev/null and b/wai/staged-conformance/cases/base_via_fallback/L1.wai differ diff --git a/wai/staged-conformance/cases/base_via_fallback/base.wai b/wai/staged-conformance/cases/base_via_fallback/base.wai new file mode 100644 index 000000000..4cc03dde0 Binary files /dev/null and b/wai/staged-conformance/cases/base_via_fallback/base.wai differ diff --git a/wai/staged-conformance/cases/base_via_fallback/case.json b/wai/staged-conformance/cases/base_via_fallback/case.json new file mode 100644 index 000000000..c1a7b6469 --- /dev/null +++ b/wai/staged-conformance/cases/base_via_fallback/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "base_via_fallback", + "about": "the base was decoded through its manifest's fallback: base-via-fallback", + "bases": [ + { + "file": "base.wai", + "object": "d08b631ce29e9aa8957c8d8f43a84203cd84b66773410c0caad30fa49709a445", + "decoded_via": "wai.video.int_motion", + "via_fallback": true, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "b9b63b558d4832e2d5fe0c412887bd8b3c1ac9d50d2209b1bf4481df46ba205b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "b0967e3138741cacbcff4a924f2e6e7e078e4af0098dbcc2d2cb05e92478b55e", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b0967e3138741cacbcff4a924f2e6e7e078e4af0098dbcc2d2cb05e92478b55e", + "envelope": "base-via-fallback", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "d08b631ce29e9aa8957c8d8f43a84203cd84b66773410c0caad30fa49709a445", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7ea10b5f41e2d150acd0416764d4de09eadef3858499eeccd71191e88d402cef" + }, + { + "stage": 0, + "producer": "d08b631ce29e9aa8957c8d8f43a84203cd84b66773410c0caad30fa49709a445", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "96ed06bef055c67736dfcf5e06c3025333b0413044c66b26efdcb3b75138f013" + } + ], + "clip_sha256": "ceebade3184f55da1c234a64e00d29d7177669551c8f0a3691177790545a0ac5", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/base_via_fallback/stage0.bin b/wai/staged-conformance/cases/base_via_fallback/stage0.bin new file mode 100644 index 000000000..ef564a24a --- /dev/null +++ b/wai/staged-conformance/cases/base_via_fallback/stage0.bin @@ -0,0 +1 @@ +(8H(HH@HP@XhX`hXhxhx€hˆ�8HP@XhX`hXp€h€€h€˜€�˜ˆ˜ P`hX`x°Èøhx�x€ ˆ˜ ˜ °˜¨¸Xpx¨ØÿÀèÿÐðÿ˜˜°˜¨° ¸È°¸Ð¸àÿÐèÿÐøÿØÿÿèÿÿ°ÈÈÀÐØÀØà�˜°Øÿÿðÿÿøÿÿ°ÀØÈØàØØðààø˜¨À°ÀÀøÿÿ¸ÐØÐØèØèøàðÿèðÿ¸ÀØÀÈàÀÐèØàðàøøèÿÿðÿÿøÿÿ(@H(HP@HX@XhX`h`hxhx€hˆ�@HP@XhX`h`p€h€€h€˜€�˜ˆ˜ P`hX`xhh€hx�x€ ˆ˜ ˜ °˜¨°`px`x�xˆŸˆ�ŸØøÿ˜¨° ¸È°¸Ðp€�ˆˆŸˆ˜§ØÿÿèÿÿøÿÿÀÈØÀØà�˜¨�Ÿ¯èÿÿøÿÿøÿÿÿÿÿÿÿÿààø˜¨À°ÀÀ°ÇÇÿÿÿÿÿÿÿÿÿØðÿèðÿ¸ÀØÀÈàÀÐèØàðÿÿÿèÿÿèÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/batch_conflict_release/L1.wai b/wai/staged-conformance/cases/batch_conflict_release/L1.wai new file mode 100644 index 000000000..09ac4c357 Binary files /dev/null and b/wai/staged-conformance/cases/batch_conflict_release/L1.wai differ diff --git a/wai/staged-conformance/cases/batch_conflict_release/L2a.wai b/wai/staged-conformance/cases/batch_conflict_release/L2a.wai new file mode 100644 index 000000000..8473f8d9a Binary files /dev/null and b/wai/staged-conformance/cases/batch_conflict_release/L2a.wai differ diff --git a/wai/staged-conformance/cases/batch_conflict_release/L2b.wai b/wai/staged-conformance/cases/batch_conflict_release/L2b.wai new file mode 100644 index 000000000..fdebd8ae6 Binary files /dev/null and b/wai/staged-conformance/cases/batch_conflict_release/L2b.wai differ diff --git a/wai/staged-conformance/cases/batch_conflict_release/base.wai b/wai/staged-conformance/cases/batch_conflict_release/base.wai new file mode 100644 index 000000000..2c93f3f36 Binary files /dev/null and b/wai/staged-conformance/cases/batch_conflict_release/base.wai differ diff --git a/wai/staged-conformance/cases/batch_conflict_release/case.json b/wai/staged-conformance/cases/batch_conflict_release/case.json new file mode 100644 index 000000000..0bf98c77e --- /dev/null +++ b/wai/staged-conformance/cases/batch_conflict_release/case.json @@ -0,0 +1,208 @@ +{ + "format": "wai-staged-case/1", + "name": "batch_conflict_release", + "about": "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", + "bases": [ + { + "file": "base.wai", + "object": "3e70bf638ce90438ec0558ed23229b6915a3e58736a897a3f45ad916a83c46c7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "61cae63ae78b67bfe98aed1387a7781120f5a16f1aa3605633810b30a1710977", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2a.wai", + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "bytes": 943 + }, + { + "file": "L2b.wai", + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "bytes": 935 + }, + { + "file": "L1.wai", + "object": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "eb051506ca9764570729f78c96cd5498cc53880f2e26566e8c3e6d9c185dcf1a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "9c361336e24cbbf8d7e91489226f285d63697c24f25078018ecaf2fc99e57aa1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3b34aea57788ede6bffae46cfc385087b3d88e2ca4343b6f1263c55588a0d053" + }, + { + "stage": 1, + "producer": "7a44a78e33ad52aaec6319d45d579e387339f9c0ffae0301f0cded809d4bcb51", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d88b339d03e35a326944a921552088c8072c6094bba4b96dfdd064f97f254010" + } + ], + "clip_sha256": "6620d5f7e2d50dc37635c3881cd29a06cf6de2444320e0159a203fc600cc8aae", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/batch_conflict_release/stage0.bin b/wai/staged-conformance/cases/batch_conflict_release/stage0.bin new file mode 100644 index 000000000..bbd335b53 Binary files /dev/null and b/wai/staged-conformance/cases/batch_conflict_release/stage0.bin differ diff --git a/wai/staged-conformance/cases/budget_partly_applied/L1.wai b/wai/staged-conformance/cases/budget_partly_applied/L1.wai new file mode 100644 index 000000000..2898b91fe Binary files /dev/null and b/wai/staged-conformance/cases/budget_partly_applied/L1.wai differ diff --git a/wai/staged-conformance/cases/budget_partly_applied/L1x.wai b/wai/staged-conformance/cases/budget_partly_applied/L1x.wai new file mode 100644 index 000000000..f5d430b6a Binary files /dev/null and b/wai/staged-conformance/cases/budget_partly_applied/L1x.wai differ diff --git a/wai/staged-conformance/cases/budget_partly_applied/L2.wai b/wai/staged-conformance/cases/budget_partly_applied/L2.wai new file mode 100644 index 000000000..2dcaa1d4e Binary files /dev/null and b/wai/staged-conformance/cases/budget_partly_applied/L2.wai differ diff --git a/wai/staged-conformance/cases/budget_partly_applied/base.wai b/wai/staged-conformance/cases/budget_partly_applied/base.wai new file mode 100644 index 000000000..1e38e3f53 Binary files /dev/null and b/wai/staged-conformance/cases/budget_partly_applied/base.wai differ diff --git a/wai/staged-conformance/cases/budget_partly_applied/case.json b/wai/staged-conformance/cases/budget_partly_applied/case.json new file mode 100644 index 000000000..5d2eb849c --- /dev/null +++ b/wai/staged-conformance/cases/budget_partly_applied/case.json @@ -0,0 +1,182 @@ +{ + "format": "wai-staged-case/1", + "name": "budget_partly_applied", + "about": "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", + "bases": [ + { + "file": "base.wai", + "object": "7a1f3fd3c178fd939c8ac0c1951ad378bb08da6a85c762394b69766ad71ef3a7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "19ac5dd5c595b4def507ec3aa0238718fc3d8972d44bb44cc5f9195e93abfd47", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "831449088f97460da5d77cab959903425e529f8a0670b80fcc47046b7da41753", + "bytes": 535 + }, + { + "file": "L2.wai", + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "bytes": 782 + }, + { + "file": "L1x.wai", + "object": "ca2ea0790270916c7eb67d32d997a191aeb9df827bd4b1fa4e8ddd38708ba750", + "bytes": 535 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1940, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1x.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "831449088f97460da5d77cab959903425e529f8a0670b80fcc47046b7da41753", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [ + 0 + ], + "refused": [] + } + ], + [ + { + "object": "ca2ea0790270916c7eb67d32d997a191aeb9df827bd4b1fa4e8ddd38708ba750", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + }, + { + "object": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ab8af1bca23c023336e602a6d5b5418792976b8033d33c6c99b6c42a5e9c6894" + }, + { + "stage": 2, + "producer": "a2681570d66d8e136ff0f3d30c0d72b901a2920b2721b288136c32e9826da15b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "1dde3ca447b566cc50731df85b84992f5ced961630c051adb80b5efb593145dd" + } + ], + "clip_sha256": "b74036d5d3befc48255321629b144b058dd516a33adb48bd65762b789ccad475", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/budget_partly_applied/stage0.bin b/wai/staged-conformance/cases/budget_partly_applied/stage0.bin new file mode 100644 index 000000000..610872968 --- /dev/null +++ b/wai/staged-conformance/cases/budget_partly_applied/stage0.bin @@ -0,0 +1 @@ + 08(0H8@H@X`P``X`xhpxhp�(@H8@`H``P`x`pxp€ˆx€�x� PX`H`p Ðèpxˆp€˜x� ˆ  ˜ °Xpx°ØøÀØÿÀðÿˆ˜ ˜ ¨ ¨¸ °Ð°èÿÀèÿÐøÿØøÿàÿÿ °È¸ÀÈ¸ÐØ€ˆ¨ÐøÿØÿÿðÿÿ°ÀȸÈÐÈÐàØØð˜ ¸¨¨Èøÿÿ¸ÀÐÀÈàÐàðÐèÿàøÿ°¸À¸ÀÐÈÐØÐàèØèðèøÿðÿÿÿÿÿ 08(0H8@H@X`P`hX`xhpxpp�(@H8H`H``P`x`pxp€ˆx€�x� PX`H`pXpppxˆx€˜x� ˆ  ˜ °Xpxhx€xx‡x�ŸÐøÿ˜ ¨  ¸ °Èhˆ�xˆ�ˆ˜ŸØøÿàÿÿèÿÿ¸ÀÈ¸ÐØ€ˆ¨ˆ˜¯ØÿÿðÿÿðÿÿÿÿÿÿÿÿØØð˜ ¸¨¨È°¯ÇÿÿÿÿÿÿÿÿÿÐèÿàðÿ°¸À¸ÀÐÈÐØÐàèÿÿÿèøÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/bypass_extremes/L1.wai b/wai/staged-conformance/cases/bypass_extremes/L1.wai new file mode 100644 index 000000000..51d4d3c46 Binary files /dev/null and b/wai/staged-conformance/cases/bypass_extremes/L1.wai differ diff --git a/wai/staged-conformance/cases/bypass_extremes/base.wai b/wai/staged-conformance/cases/bypass_extremes/base.wai new file mode 100644 index 000000000..d942bb986 Binary files /dev/null and b/wai/staged-conformance/cases/bypass_extremes/base.wai differ diff --git a/wai/staged-conformance/cases/bypass_extremes/case.json b/wai/staged-conformance/cases/bypass_extremes/case.json new file mode 100644 index 000000000..cb0074b2d --- /dev/null +++ b/wai/staged-conformance/cases/bypass_extremes/case.json @@ -0,0 +1,122 @@ +{ + "format": "wai-staged-case/1", + "name": "bypass_extremes", + "about": "a table of one symbol at q = 1: nearly every residual takes the bypass escape, and the layer is exact", + "bases": [ + { + "file": "base.wai", + "object": "5036fc6abafd20625a7555e28aec85b8a77fef3bef00c6be826ba489dc0fafe2", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "ba2f7ebb664ac685de2ae18af060e7fe0b4c767ae439ffb1ae6c533911f3dd65", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "25db4602b79c63dfb01cecddfdbca9ade2153d016cf96d108cc03b557d21d9de", + "bytes": 1807 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "25db4602b79c63dfb01cecddfdbca9ade2153d016cf96d108cc03b557d21d9de", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "25db4602b79c63dfb01cecddfdbca9ade2153d016cf96d108cc03b557d21d9de", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "66dee210c5c713e8c48b19e2745183dc5d37c24263ab8a6b3f633c9187ebded4" + }, + { + "stage": 1, + "producer": "25db4602b79c63dfb01cecddfdbca9ade2153d016cf96d108cc03b557d21d9de", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fb93a43aaba925b831b3dc60d8b91a511cab52f491ad52147ca3bb54806a946c" + } + ], + "clip_sha256": "cb6d3b466bf10bb01752c1379a6ca1f6bcd9961eb27d0cb57542a901b71fc5c7", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/bypass_extremes/stage0.bin b/wai/staged-conformance/cases/bypass_extremes/stage0.bin new file mode 100644 index 000000000..767a98407 --- /dev/null +++ b/wai/staged-conformance/cases/bypass_extremes/stage0.bin @@ -0,0 +1 @@ +  8(8@(8@8HXHPhHXp`h€(0@08@0HXHH`P`hX`p`xˆhˆ�08X@PX�ÀèXhphpxh€�pˆ˜€˜ HPh ¸è Èð°àÿp€˜ˆ�˜ˆ °˜ ¸¨Ðð¨ÐÿÀèÿÀðÿÐÿÿ˜¨°¨°À ¸Ðpˆ˜ÈèÿÈøÿàÿÿ˜¨À¨°Ð°ÈÐÀÐØˆ� ˜˜°àÿÿ ¸À¸ÈÐÈÈàÈØèÐèð�°°¨¸À°ÀÐ°ÈØÀØàÐèèàèøððÿ  8(8@(8H8HXHPhHXp`h€(0@08@0PXHP`P`h`hp`xˆh€�0@XHPXH`pXpphpxh€ˆpˆ˜€� HXhXXpXhxh€‡¸àÿ€�˜ˆ ¨˜ ¸`px`p‡xˆ�ÀðÿÐÿÿàÿÿ¨°À ¸Ðpˆ˜€ˆŸÈøÿàÿÿàÿÿðÿÿøÿÿÀÐØˆ� ˜˜¨˜¯¯èÿÿÿÿÿÿÿÿÈØèÐèð�¨°¨¸À°ÀÐ°ÈØÿÿÿÐàèàèøèðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/capability_not_refinement/L1.wai b/wai/staged-conformance/cases/capability_not_refinement/L1.wai new file mode 100644 index 000000000..714335db3 Binary files /dev/null and b/wai/staged-conformance/cases/capability_not_refinement/L1.wai differ diff --git a/wai/staged-conformance/cases/capability_not_refinement/base.wai b/wai/staged-conformance/cases/capability_not_refinement/base.wai new file mode 100644 index 000000000..ae977f88b Binary files /dev/null and b/wai/staged-conformance/cases/capability_not_refinement/base.wai differ diff --git a/wai/staged-conformance/cases/capability_not_refinement/case.json b/wai/staged-conformance/cases/capability_not_refinement/case.json new file mode 100644 index 000000000..89a0b794c --- /dev/null +++ b/wai/staged-conformance/cases/capability_not_refinement/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "capability_not_refinement", + "about": "refines on a wai.video.int_motion envelope: capability-not-refinement", + "bases": [ + { + "file": "base.wai", + "object": "9386292a9a98f4a3fd014d0a4ff0361cbe18545b446c72fa52448e48b60663d6", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f57d55aad6d85d3da43bcf0541b48c941af2cbd5ae9bceada4eb496b523fae95", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "7c39597cdccea374758b5447b67cf0ba019eee80a6b0b6bea97c5e435d6dc7cd", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "7c39597cdccea374758b5447b67cf0ba019eee80a6b0b6bea97c5e435d6dc7cd", + "envelope": "capability-not-refinement", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "9386292a9a98f4a3fd014d0a4ff0361cbe18545b446c72fa52448e48b60663d6", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "4ad325b6f0ad682e84747c4797333c49a45a004f08830c3ef8053f19424872e5" + }, + { + "stage": 0, + "producer": "9386292a9a98f4a3fd014d0a4ff0361cbe18545b446c72fa52448e48b60663d6", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7e751833166e7a151347ba4a2e08e68e84fbd3ad0d8edaedb3a45e8be2047ea3" + } + ], + "clip_sha256": "39d743f21b782cc4ea1d1bacb50e0244af72459a21746ef19e6571a9a712c3ad", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/capability_not_refinement/stage0.bin b/wai/staged-conformance/cases/capability_not_refinement/stage0.bin new file mode 100644 index 000000000..02ff4ff76 --- /dev/null +++ b/wai/staged-conformance/cases/capability_not_refinement/stage0.bin @@ -0,0 +1 @@ +(((@(@@88X@P`@XhPhhhxx 8@0@H8P`PX`P`x`h€p€ˆ€ˆ˜0P`@X`˜¸ð`p€pp€x€˜x˜ ˆ˜ H`p˜Ðè¨Øø¸àÿx� €˜ ˆ¨¨ °¸ Øÿ¸àÿ¸ðÿÐøÿØÿÿ˜¨À¨°È¨ÈÐpˆ�ÈøÿÐøÿèÿÿ ¸À¨ÈиÈàÈØè€ °˜¨°èÿÿ°¸È¸ÀØÈØèÐàèØèð˜°À ¸Ð°ÀØ¸ÐØÈÐðØàøàèÿèøÿ(((@(@@8@X@Ph@XhPhphxx(8@8@P8P`PX`P`xhh€p€ˆ€€˜8P`@X`PXx`p€pp€x€˜x� ˆ˜ P`pXpp`x€p€‡Àðÿ€˜ ˆ¨¨ °¸Xx‡x€‡p�ŸÐðÿØÿÿàÿÿ¨°È¨ÈÐxˆ�ˆ˜ŸÐðÿèÿÿàÿÿðÿÿÿÿÿÈØà€ °˜¨° ¯·øÿÿÿÿÿÿÿÿÐØèØèð˜°À ¸Ð°ÀØ¸ÐØÿÿÿØàøàèÿèøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/cdf_malformed/L1.wai b/wai/staged-conformance/cases/cdf_malformed/L1.wai new file mode 100644 index 000000000..c2286514e Binary files /dev/null and b/wai/staged-conformance/cases/cdf_malformed/L1.wai differ diff --git a/wai/staged-conformance/cases/cdf_malformed/base.wai b/wai/staged-conformance/cases/cdf_malformed/base.wai new file mode 100644 index 000000000..db3a11d95 Binary files /dev/null and b/wai/staged-conformance/cases/cdf_malformed/base.wai differ diff --git a/wai/staged-conformance/cases/cdf_malformed/case.json b/wai/staged-conformance/cases/cdf_malformed/case.json new file mode 100644 index 000000000..5c9314130 --- /dev/null +++ b/wai/staged-conformance/cases/cdf_malformed/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "cdf_malformed", + "about": "a table entry set to 0 in mid-table: payload-malformed (cdf_malformed)", + "bases": [ + { + "file": "base.wai", + "object": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "e7c50678b697bf23050f7b9ddc7256e5da1c9f2e4ce7c3e3f4db8407583057a7", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "82b9092649e9b1386370377ba5e3a432488d773df2294148edcedc94300519ca", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "82b9092649e9b1386370377ba5e3a432488d773df2294148edcedc94300519ca", + "envelope": "payload-malformed", + "detail": "cdf_malformed", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cd22012ce1f60626fe6cc008e8783a28d80a043023d6b45178536560aa3aa1f5" + }, + { + "stage": 0, + "producer": "92b42e2c7a7db02e114f5cc92abe65af343c1806f4a2000923eb5124e408d455", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b5d83ed2343c5f54ae126fa8eebf3e1c4e96791c85a31adab9722b24d2b468f9" + } + ], + "clip_sha256": "a691f382b2ad20d285b30a25c07273f155b5c0e73245c1ae76a4b4a7e86810ff", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cdf_malformed/stage0.bin b/wai/staged-conformance/cases/cdf_malformed/stage0.bin new file mode 100644 index 000000000..81cef2879 Binary files /dev/null and b/wai/staged-conformance/cases/cdf_malformed/stage0.bin differ diff --git a/wai/staged-conformance/cases/chain_budget_retained/L1.wai b/wai/staged-conformance/cases/chain_budget_retained/L1.wai new file mode 100644 index 000000000..cc335d451 Binary files /dev/null and b/wai/staged-conformance/cases/chain_budget_retained/L1.wai differ diff --git a/wai/staged-conformance/cases/chain_budget_retained/L2.wai b/wai/staged-conformance/cases/chain_budget_retained/L2.wai new file mode 100644 index 000000000..c293b4ac5 Binary files /dev/null and b/wai/staged-conformance/cases/chain_budget_retained/L2.wai differ diff --git a/wai/staged-conformance/cases/chain_budget_retained/base.wai b/wai/staged-conformance/cases/chain_budget_retained/base.wai new file mode 100644 index 000000000..1a4dbd094 Binary files /dev/null and b/wai/staged-conformance/cases/chain_budget_retained/base.wai differ diff --git a/wai/staged-conformance/cases/chain_budget_retained/case.json b/wai/staged-conformance/cases/chain_budget_retained/case.json new file mode 100644 index 000000000..93d05646e --- /dev/null +++ b/wai/staged-conformance/cases/chain_budget_retained/case.json @@ -0,0 +1,188 @@ +{ + "format": "wai-staged-case/1", + "name": "chain_budget_retained", + "about": "a budget that holds the base and either layer, not both: layer 2, held, is retained, so layer 1 is chain-over-limit; once layer 2 expires and is dropped, layer 1 applies", + "bases": [ + { + "file": "base.wai", + "object": "b6c319bdbec933d6d3d5e45cc65cb68763a31654badf025d2766364924e83759", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "ce62db24b33cea47d4432a269313e134a0e53e9cd65921744b01c7e90d3122f4", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "46a97b83c801e78f6a767184639c725ae354655354b260d4eeeb35836b586eaa", + "bytes": 587 + }, + { + "file": "L2.wai", + "object": "d9cd97ff18e6c9e88f5e779d1065c1584f6ff622543a1c5d082522a38872eb05", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1884, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1002 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1003, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "d9cd97ff18e6c9e88f5e779d1065c1584f6ff622543a1c5d082522a38872eb05", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "46a97b83c801e78f6a767184639c725ae354655354b260d4eeeb35836b586eaa", + "envelope": "chain-over-limit", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "d9cd97ff18e6c9e88f5e779d1065c1584f6ff622543a1c5d082522a38872eb05", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-gap" + ], + [ + 1, + "layer-gap" + ] + ] + } + ], + [ + { + "object": "46a97b83c801e78f6a767184639c725ae354655354b260d4eeeb35836b586eaa", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "46a97b83c801e78f6a767184639c725ae354655354b260d4eeeb35836b586eaa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c36a9da5e9305e9139804f6e3a834dbf31ecc41ed525bd0c6d901829ad4d8610" + }, + { + "stage": 1, + "producer": "46a97b83c801e78f6a767184639c725ae354655354b260d4eeeb35836b586eaa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c5eb4afa4e6f2440122ab4361f4c23bb377ca8a9eb8374d3b2c7377e7499670e" + } + ], + "clip_sha256": "9b359fd5c3a4e4212911fdd0c0cbec31d00d46474656b49a655b4ef122bbded2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/chain_budget_retained/stage0.bin b/wai/staged-conformance/cases/chain_budget_retained/stage0.bin new file mode 100644 index 000000000..d19e55eb9 --- /dev/null +++ b/wai/staged-conformance/cases/chain_budget_retained/stage0.bin @@ -0,0 +1 @@ + 0@08P@PXHP`Php`pxhp�x€�0HXHP`HXpPhxhx€x€˜x�˜ˆ˜ HXp`hp¨Øðpx�p€ ˆ˜ ˆ ° °¸`pˆ¸ØÿÀèÿÐðÿˆ ° ¨¸ °È¸¸ÐÀàÿÈøÿÐøÿàÿÿèÿÿ¸¸ÐÀÐØÀÐè�˜¨Øøÿèÿÿèÿÿ°ÈØÈÐØÐàèØðÿ˜¨À¨ÀÀøÿÿ¸ØàÈÐðÐàðèèøðøÿ¸ÀØÀÈØÈØðÐèøàèÿàøÿðÿÿÿÿÿ 0@0@P@PXHP`Phphpxhp�x€�0HXHP`PXpPh€hx€x€�x�˜ˆ˜ HXp`hx`xxxx�p€˜ˆ� ˆ ° °¸hpˆpx‡xˆŸˆ�ŸÐøÿ ¨¸ °È¸¸Ðx€�ˆ˜Ÿˆ˜ŸàÿÿèÿÿøÿÿÀÐØÀÐè�˜¨�˜¯èÿÿèÿÿøÿÿÿÿÿÿÿÿØèÿ˜¨À¨ÀÀ¸·Ïÿÿÿÿÿÿÿÿÿèèøðøÿ°ÀÐÀÈØÈØðÐèøÿÿÿàøÿðÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/chain_over_limit/L1.wai b/wai/staged-conformance/cases/chain_over_limit/L1.wai new file mode 100644 index 000000000..7f75adb90 Binary files /dev/null and b/wai/staged-conformance/cases/chain_over_limit/L1.wai differ diff --git a/wai/staged-conformance/cases/chain_over_limit/base.wai b/wai/staged-conformance/cases/chain_over_limit/base.wai new file mode 100644 index 000000000..dd23ff8da Binary files /dev/null and b/wai/staged-conformance/cases/chain_over_limit/base.wai differ diff --git a/wai/staged-conformance/cases/chain_over_limit/case.json b/wai/staged-conformance/cases/chain_over_limit/case.json new file mode 100644 index 000000000..0ad6fa587 --- /dev/null +++ b/wai/staged-conformance/cases/chain_over_limit/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "chain_over_limit", + "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", + "bases": [ + { + "file": "base.wai", + "object": "513f379e56b8d49a34f8ab0819a564cac71647e3d9bc3d98191259f2b6e0587e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "11e4e6d347666fbc7dbb346643bb35a1994a00c1234290673a42182c6a8dbfd0", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "9a1fc1dd33e5f1a866212143d9948054af1190d8d7ace19e89a07cd5c7fdf875", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1335, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "9a1fc1dd33e5f1a866212143d9948054af1190d8d7ace19e89a07cd5c7fdf875", + "envelope": "chain-over-limit", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "513f379e56b8d49a34f8ab0819a564cac71647e3d9bc3d98191259f2b6e0587e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "80d915b8092dc1a6b54b24de51abdc0dacc9a345452fe1babc399caef1a3f92e" + }, + { + "stage": 0, + "producer": "513f379e56b8d49a34f8ab0819a564cac71647e3d9bc3d98191259f2b6e0587e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "277434f91cd2e9c0028c9505e316200a9bd71e551044cb548dcea9b1da31269c" + } + ], + "clip_sha256": "b139902d21228247e2a7901db2077ca0e346c6f34c398b62be71d4516a1dcdf5", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/chain_over_limit/stage0.bin b/wai/staged-conformance/cases/chain_over_limit/stage0.bin new file mode 100644 index 000000000..b0e43beab --- /dev/null +++ b/wai/staged-conformance/cases/chain_over_limit/stage0.bin @@ -0,0 +1 @@ +((@0@P8HX@PXPXhXh€hp€hx˜8PXHX`HXhPhxhp€pˆˆpˆ €˜ HPhP`p°Ððp€ˆxˆ˜€˜ �˜¨˜¨°`px°ÐÿÀàÿÀøÿ�˜¨�¨°˜¸¸°ÈȸàÿÈèÿÐðÿØøÿèÿÿ¨¸ÐÀÈØ¸Øà€�°Ðÿÿèÿÿøÿÿ°ÈиØàÈØðØàð ¨À °ÈðÿÿÀÈØÈÐàØèðØøÿèÿÿ¨¸ÐÀÈØÀÐèÈàðàðøðøÿðÿÿøÿÿ((@0@X8HX@P`XXhXh€hp€h€˜8PXHX`PXhXpxhp€p€ˆpˆ˜€˜ HXhX`xhpxp€ˆ€ˆ˜€˜ �˜¨˜¨°`pxhp�x€—x˜ŸØøÿ�¨°˜¸¸°ÀÈp€—€ˆ—ˆ�§Øøÿèÿÿðÿÿ¸ÀظØà€�°ˆŸ·èÿÿøÿÿøÿÿÿÿÿÿÿÿØàè ¨À °È°ÇÏÿÿÿÿÿÿÿÿÿØøÿèÿÿ¨¸ÐÀÈØÀÐèÈØðÿÿÿðøÿèÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/clamp_extremes/L1.wai b/wai/staged-conformance/cases/clamp_extremes/L1.wai new file mode 100644 index 000000000..72d581b5f Binary files /dev/null and b/wai/staged-conformance/cases/clamp_extremes/L1.wai differ diff --git a/wai/staged-conformance/cases/clamp_extremes/base.wai b/wai/staged-conformance/cases/clamp_extremes/base.wai new file mode 100644 index 000000000..b236007f4 Binary files /dev/null and b/wai/staged-conformance/cases/clamp_extremes/base.wai differ diff --git a/wai/staged-conformance/cases/clamp_extremes/case.json b/wai/staged-conformance/cases/clamp_extremes/case.json new file mode 100644 index 000000000..3abf34e67 --- /dev/null +++ b/wai/staged-conformance/cases/clamp_extremes/case.json @@ -0,0 +1,122 @@ +{ + "format": "wai-staged-case/1", + "name": "clamp_extremes", + "about": "targets at 0 and 255 with q = 200: residuals that overshoot are clamped to the u8 range", + "bases": [ + { + "file": "base.wai", + "object": "32635ea9c6905a9799bfef51a0338de30f28baf2ae61eeffe061d704cc383a19", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "0bb1e1d33eb2ef6bc52e678534c42731500d60f8cf8da7ef4932196d81ddc57b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "a5aa99dad81129f4693a0762034ee951f1ac78b1a47f9e381a51358a76ca38fd", + "bytes": 495 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "a5aa99dad81129f4693a0762034ee951f1ac78b1a47f9e381a51358a76ca38fd", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "a5aa99dad81129f4693a0762034ee951f1ac78b1a47f9e381a51358a76ca38fd", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e204e6503da0e0d2eb9b6865dc386fa6fcfa1e10835e467455fa7a3b6fb4a0b5" + }, + { + "stage": 1, + "producer": "a5aa99dad81129f4693a0762034ee951f1ac78b1a47f9e381a51358a76ca38fd", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ab1c1db33c19af7eb1e8aa25582bb165c18bd2790ab70b80cfee99287cc7ff77" + } + ], + "clip_sha256": "bf3594e190c527a1126118071829dde7d08b149b4f1cd5e060ce6c770005af13", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/clamp_extremes/stage0.bin b/wai/staged-conformance/cases/clamp_extremes/stage0.bin new file mode 100644 index 000000000..a65b942bf --- /dev/null +++ b/wai/staged-conformance/cases/clamp_extremes/stage0.bin @@ -0,0 +1 @@ +(08((8 8P8PP@P`HXh``x(0808H08X8P`PX`Xhphh€h€ˆ0HH8PX�¸ØP`h`h€`€ˆx€˜ˆ�¨HXh˜Èð¨Ðð¸Øÿx€˜€� �˜°˜¨¨ Èð°Ðÿ¸ØÿÀðÿÐøÿ�˜¸ ¨À°¸Àxx�ÈèÿÈøÿØøÿ¨°¸°¸È°ÀÐÀÈàˆ˜˜� ¨àÿÿ °¸°ÈȸÐÐÀØèÈè𘨸˜¸È¨¸ÐÀÀØÈÐØØàèàðøðøÿ(08((@(8P8PP@P`HXh``x(0808H0@X8P`PX`Xhphh€h€€0HH8XXHX`Phh`h€`€€x€˜ˆ�¨HXpPhp`pxpx‡¸àÿ€� �˜°˜¨¨Xhxhp�px‡ÀðÿÐøÿØøÿ ¨¸°¸Àxx�€ˆ�ÈøÿØøÿèÿÿøÿÿðÿÿÀÈàˆ˜˜ˆ ¨˜§¯èÿÿøÿÿÿÿÿÀØèÈè𘨸˜°È¨¸ÐÀÀØÿÿÿÐàèàèøèøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/conflict_after_apply/L1a.wai b/wai/staged-conformance/cases/conflict_after_apply/L1a.wai new file mode 100644 index 000000000..d0b0679bf Binary files /dev/null and b/wai/staged-conformance/cases/conflict_after_apply/L1a.wai differ diff --git a/wai/staged-conformance/cases/conflict_after_apply/L1b.wai b/wai/staged-conformance/cases/conflict_after_apply/L1b.wai new file mode 100644 index 000000000..008aa8566 Binary files /dev/null and b/wai/staged-conformance/cases/conflict_after_apply/L1b.wai differ diff --git a/wai/staged-conformance/cases/conflict_after_apply/L2.wai b/wai/staged-conformance/cases/conflict_after_apply/L2.wai new file mode 100644 index 000000000..b412470e5 Binary files /dev/null and b/wai/staged-conformance/cases/conflict_after_apply/L2.wai differ diff --git a/wai/staged-conformance/cases/conflict_after_apply/base.wai b/wai/staged-conformance/cases/conflict_after_apply/base.wai new file mode 100644 index 000000000..3ed8511c9 Binary files /dev/null and b/wai/staged-conformance/cases/conflict_after_apply/base.wai differ diff --git a/wai/staged-conformance/cases/conflict_after_apply/case.json b/wai/staged-conformance/cases/conflict_after_apply/case.json new file mode 100644 index 000000000..5ca04c097 --- /dev/null +++ b/wai/staged-conformance/cases/conflict_after_apply/case.json @@ -0,0 +1,184 @@ +{ + "format": "wai-staged-case/1", + "name": "conflict_after_apply", + "about": "layer 1a applies; a different layer 1b conflicts and freezes the units at stage 1; a layer 2 over 1a is then refused", + "bases": [ + { + "file": "base.wai", + "object": "1d319df65b75012b3d2cff45bdd6589e70dece53e0be4efb463c17e8c271f0ad", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5f44ba0bb608d0e5b3edbbe7ac5d6199a23dce06f59aa79bddc324f7a18da6bf", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "bytes": 587 + }, + { + "file": "L1b.wai", + "object": "511d1ebed578caa52a9eb4edbc5306dd65baeb1767a4d9b1b1e318e1a8b686c3", + "bytes": 591 + }, + { + "file": "L2.wai", + "object": "3532844b0c4ba684437fc6901f2ab5366bf120d00132355cd31600a119b5a617", + "bytes": 799 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "511d1ebed578caa52a9eb4edbc5306dd65baeb1767a4d9b1b1e318e1a8b686c3", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [ + { + "object": "3532844b0c4ba684437fc6901f2ab5366bf120d00132355cd31600a119b5a617", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "64869721cf6c98101a5dc9384c743251506554cdccc52f95eb85f944cf79561f" + }, + { + "stage": 1, + "producer": "ebc3e691e4396cb0639b7b6d8f00a0c06922dfe14198e1466668373b101c3db5", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "27322b811fd94a706da3f59122e54f2b202eacfd59f7f84e8f024158ad8bc950" + } + ], + "clip_sha256": "4a856cfc54c1f88820a2319f3e995165e3ae3821541149342e2b65d148c48a2b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/conflict_after_apply/stage0.bin b/wai/staged-conformance/cases/conflict_after_apply/stage0.bin new file mode 100644 index 000000000..9e828123d --- /dev/null +++ b/wai/staged-conformance/cases/conflict_after_apply/stage0.bin @@ -0,0 +1 @@ +((@(8P8PP8P`P`pXppXpˆp€ˆ8HP8H`HXpXhp`hˆpxˆx€�ˆ˜ HXhXhh Èø`xˆpˆ�ˆˆ ˆ °  °`hx¨Øø¸ØÿÈèÿˆ ¨˜¨°¨¸¸¨°È¸èÿÀðÿÈøÿàÿÿèÿÿ°°È¸ÈÐÀÐØˆ˜ Ðøÿèÿÿèÿÿ¨ÀÐÈÈàÈÐðØè𘨰¨°¸øÿÿ¸ÐÐÀØèÈàðØàÿððÿ¨¸È¸ÀàÈÐèÐØèàèðàðÿðÿÿøÿÿ((@(8P8PP8P`P`pXpp`p€p€ˆ8HP8H`HXpXhp`hˆpxˆx€�ˆ˜ HXhXhhXh€`x€pˆˆˆˆ˜€ °  °`hx`x€px—€ˆ—Ðÿÿ˜¨° °¸¨°Èpˆ‡x�Ÿ€˜§àÿÿèÿÿðÿÿ¸ÈÈÀÐØˆ˜ �˜·èÿÿàÿÿðÿÿÿÿÿÿÿÿÐè𘨰¨°¸°·ÏøÿÿÿÿÿÿÿÿØàÿèðÿ¨¸È¸ÀØÈÐèÐØèÿÿÿàðÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/conflict_held/L1a.wai b/wai/staged-conformance/cases/conflict_held/L1a.wai new file mode 100644 index 000000000..5ad11056c Binary files /dev/null and b/wai/staged-conformance/cases/conflict_held/L1a.wai differ diff --git a/wai/staged-conformance/cases/conflict_held/L1b.wai b/wai/staged-conformance/cases/conflict_held/L1b.wai new file mode 100644 index 000000000..22c7ad445 Binary files /dev/null and b/wai/staged-conformance/cases/conflict_held/L1b.wai differ diff --git a/wai/staged-conformance/cases/conflict_held/base.wai b/wai/staged-conformance/cases/conflict_held/base.wai new file mode 100644 index 000000000..096b8afcb Binary files /dev/null and b/wai/staged-conformance/cases/conflict_held/base.wai differ diff --git a/wai/staged-conformance/cases/conflict_held/case.json b/wai/staged-conformance/cases/conflict_held/case.json new file mode 100644 index 000000000..0752689ae --- /dev/null +++ b/wai/staged-conformance/cases/conflict_held/case.json @@ -0,0 +1,178 @@ +{ + "format": "wai-staged-case/1", + "name": "conflict_held", + "about": "two different layers 1 held for the base and released together: neither applies, and both units are frozen", + "bases": [ + { + "file": "base.wai", + "object": "c7834682987d8196f8ac3df437a117eff2478d1edff949d7eb9d967cf2dbf934", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "150f42458cdd800a4fc70be9ce597c40c4f0b674603d5c9ffd13868e1116bbe3", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "3d6d1b3ce29cb6cb9ad2fc9aa731136709e3dc8959a2f25876e50dc75051974d", + "bytes": 579 + }, + { + "file": "L1b.wai", + "object": "1862d22342b99f811abc97d9aba5049efea609c615c50ca88207df51aa8a7fbb", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "3d6d1b3ce29cb6cb9ad2fc9aa731136709e3dc8959a2f25876e50dc75051974d", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "1862d22342b99f811abc97d9aba5049efea609c615c50ca88207df51aa8a7fbb", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3d6d1b3ce29cb6cb9ad2fc9aa731136709e3dc8959a2f25876e50dc75051974d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "1862d22342b99f811abc97d9aba5049efea609c615c50ca88207df51aa8a7fbb", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "c7834682987d8196f8ac3df437a117eff2478d1edff949d7eb9d967cf2dbf934", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "969bd579ef62a220ebdedca9202319c612a3d864d32fdd94b401c3c6c0cd32f9" + }, + { + "stage": 0, + "producer": "c7834682987d8196f8ac3df437a117eff2478d1edff949d7eb9d967cf2dbf934", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ed8e0cfc28c20965f0543d358da6575f4398d086bb26a694647115a1f953763f" + } + ], + "clip_sha256": "3dda0036e74dc29f95ae738b5eeb45e9028444716482de142cadc69f05d7b99c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/conflict_held/stage0.bin b/wai/staged-conformance/cases/conflict_held/stage0.bin new file mode 100644 index 000000000..21a284b4e --- /dev/null +++ b/wai/staged-conformance/cases/conflict_held/stage0.bin @@ -0,0 +1 @@ +(0(0H8HP8HPHX`X`xhx€hx€8@P@HP@X`XXpXpˆhpˆp€˜x˜ @H`PXx Èøhx€xˆ�x� €˜¨˜¨°X`x°Øø°ØÿÀðÿ€�¨˜ °˜ À°°È¸ÐÿÀèÿÐøÿØÿÿàÿÿ°°È¸ÈиÈà€ˆ¨ØøÿØÿÿðÿÿ¨ÀØÀÈÐÈØàÈà𘨰¨¨¸ðÿÿ¸ÀØÀÐèÈØðØðøèøÿ ÀиÈÐÈØØÈàèØèøèøøðøÿÿÿÿ(0(0H8HP@HXHX`X`xhx€hx€8@P@HP@``X`p`p€hpˆp€˜€� @P`PXxXh€hx€x€�€�˜€˜¨�¨°X`xhx€hx�x�—Èðÿ˜ ¨˜ À¨°Èpp�xˆ—ˆ˜ŸØÿÿàÿÿðÿÿ¸ÈиÈà€ˆ¨�˜¯ØÿÿðÿÿðÿÿÿÿÿÿÿÿÈà𘠰¨¨¸¨·ÇøÿÿÿÿÿÿÿÿØðøèøÿ ¸Ð¸ÈÐÀØØÈààÿÿÿèðøðøÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/cost_over_limit/L1.wai b/wai/staged-conformance/cases/cost_over_limit/L1.wai new file mode 100644 index 000000000..a546fa90d Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit/L1.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit/base.wai b/wai/staged-conformance/cases/cost_over_limit/base.wai new file mode 100644 index 000000000..499cb8b21 Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit/base.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit/case.json b/wai/staged-conformance/cases/cost_over_limit/case.json new file mode 100644 index 000000000..034595402 --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit/case.json @@ -0,0 +1,123 @@ +{ + "format": "wai-staged-case/1", + "name": "cost_over_limit", + "about": "a sink whose cost limit is 100 output bytes: the 384-byte layer is cost-over-limit", + "bases": [ + { + "file": "base.wai", + "object": "3cbd573d75ea70e5f1cb7741c6263d84446c806cda603e258680d166b3087b70", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "c331f748c17f548c2fb7c2d6f679a4959c15b2fb592923446ba6ed902fb9dab2", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "ea8e72983eb0f54da85e6eb04e0cae906cb7bc49c45d5d91d90ba1c6c383ed94", + "bytes": 575 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": { + "output_bytes": 100, + "symbols": 18446744073709551615, + "working_bytes": 18446744073709551615 + }, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ea8e72983eb0f54da85e6eb04e0cae906cb7bc49c45d5d91d90ba1c6c383ed94", + "envelope": "cost-over-limit", + "detail": "output bytes", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "3cbd573d75ea70e5f1cb7741c6263d84446c806cda603e258680d166b3087b70", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "154335a71e9659e50e5efc7cc56c6f669880915d61c132ba93997ae3c43e54c0" + }, + { + "stage": 0, + "producer": "3cbd573d75ea70e5f1cb7741c6263d84446c806cda603e258680d166b3087b70", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "09174bd7c4b3bdf240b6493623bc7fcc70f57b9540fb6c34471ab947cd608589" + } + ], + "clip_sha256": "2b8ae031e774ec0b8efbd0a1332185e8395689213fd113dcf38d6ad8e9236a7a", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cost_over_limit/stage0.bin b/wai/staged-conformance/cases/cost_over_limit/stage0.bin new file mode 100644 index 000000000..d1b4ada1b --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit/stage0.bin @@ -0,0 +1 @@ +(@(8@(@P8PPPP`P`h`hx`€€(8P8PPHH`H`pX`p`p€hˆ�pˆ˜HH`H`p Àø`xxp€ˆpˆ˜€� �˜°Xhp Èø°ÐÿÀàÿx˜˜ˆ ° °° ¸À°Ðÿ¸èÿÀðÿÐðÿàÿÿ ¸À°¸È¸ÀЀ�˜Èøÿàøÿèÿÿ¨¸È°ÈÈÀÈØÈØè�¨¸˜°Àðÿÿ°ÈÐÀÈØÀàèÐèðØèÿ °È¨ÀиÈàÈØàÈèèàðøèÿÿðÿÿ(@(8@(@P@PPPPhP`h`hx`€€(8P8PPHHhH`pXhp`p€pˆ�pˆ˜HH`H`p``€`xxp€ˆpˆ˜€�˜�˜°XhpXhxhp�x€—Àøÿˆ ° °°˜°Àhp‡pˆ�x�ŸÐðÿàÿÿàÿÿ°¸È¸ÀЀ�˜€˜§àøÿèÿÿðÿÿøÿÿÿÿÿÈØè�¨¸˜°¸¨¿ÇøÿÿÿÿÿÿÿÿÐèðØèÿ °È¨ÀиÀàÈØàÿÿÿàðøèÿÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/L1.wai b/wai/staged-conformance/cases/cost_over_limit_symbols/L1.wai new file mode 100644 index 000000000..161fe27d1 Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit_symbols/L1.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/base.wai b/wai/staged-conformance/cases/cost_over_limit_symbols/base.wai new file mode 100644 index 000000000..f63e746e5 Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit_symbols/base.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/case.json b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json new file mode 100644 index 000000000..3a972851b --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_symbols/case.json @@ -0,0 +1,123 @@ +{ + "format": "wai-staged-case/1", + "name": "cost_over_limit_symbols", + "about": "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", + "bases": [ + { + "file": "base.wai", + "object": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "90505a23d051df0d5ed6bf7b6baaa8e14ab5a940e51b44c10e0cc47c6958bd4d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "b60647ec8bfbab8ddccdea62b935851b32b4d2d15e02b62d8ddf029a54381924", + "bytes": 595 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": { + "output_bytes": 18446744073709551615, + "symbols": 64, + "working_bytes": 18446744073709551615 + }, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b60647ec8bfbab8ddccdea62b935851b32b4d2d15e02b62d8ddf029a54381924", + "envelope": "cost-over-limit", + "detail": "symbols", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d46a8c04a3efdcb0ea7ca67a174af553a33527b37136762ba7cbf6263c13b187" + }, + { + "stage": 0, + "producer": "17a4bbf83638fddab4404a156f5eaee382933d438fc8fe92bc5ffddaa891e93a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6cd64862accc34f777535d1924f4089f63f1895a9593588f00919f0a5a9caff9" + } + ], + "clip_sha256": "e4561786406a2ad52a46f3814fe516cd07745917fa5150b2b25b49f71218d2db", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin b/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin new file mode 100644 index 000000000..9b9c2926f --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_symbols/stage0.bin @@ -0,0 +1 @@ +8@08@08XHP`PX`Ppxhpxhˆ�08X8P`P`hH`x`p€pxˆp€�x˜ HX`H`x¨Ðèpxˆp€ˆ€�˜€˜¨˜ °`hx°Ðÿ¸àÿÀðÿ€�¨˜ °˜¨À¨°Ð°àÿ¸èÿÐøÿÐÿÿàÿÿ¨ÀÀ¸ÀÐÀØà€�¨Ðøÿàÿÿèÿÿ¸ÈÐÀÀØÈÐàÈà𘠰¨°¸øÿÿ¸ÈàÀØàÐØèØèÿàøÿ°ÀÈ°ÈØÀØàÐØèÐàøèðÿðÿÿøÿÿ8@08@0@XHP`PX`Ppxhpxh€�08X8P`P`hPhx`x€p€ˆp€�€� HX`P`x`pppxˆx€ˆ€�˜€�¨˜ °`hxhp‡p€—x��Èðÿ˜ °˜¨À¨°Ðh€�pˆ—ˆ˜ŸÐÿÿàÿÿðÿÿ¸ÀÐÀÐØ€�¨�˜§àÿÿèÿÿøÿÿÿÿÿÿÿÿÈà𘠰¨°¸°¿¿øÿÿÿÿÿÿÿÿØèÿàøÿ°ÀȰÈиØàÐØèÿÿÿèèÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/cost_over_limit_working/L1.wai b/wai/staged-conformance/cases/cost_over_limit_working/L1.wai new file mode 100644 index 000000000..dceb8541d Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit_working/L1.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit_working/base.wai b/wai/staged-conformance/cases/cost_over_limit_working/base.wai new file mode 100644 index 000000000..105960af4 Binary files /dev/null and b/wai/staged-conformance/cases/cost_over_limit_working/base.wai differ diff --git a/wai/staged-conformance/cases/cost_over_limit_working/case.json b/wai/staged-conformance/cases/cost_over_limit_working/case.json new file mode 100644 index 000000000..f856985b0 --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_working/case.json @@ -0,0 +1,123 @@ +{ + "format": "wai-staged-case/1", + "name": "cost_over_limit_working", + "about": "a sink whose cost limit is 100 working bytes: a layer that holds its tables, its stream index and one 192-byte frame is cost-over-limit (working bytes)", + "bases": [ + { + "file": "base.wai", + "object": "460dd25d65a258dca13ebdd426660b58fdb8870b20c5aa78ffd3e4ab2a9a52a6", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "0783ae62907aa7f3f3de3720128dceb914d8c0966662032ac695c8b5c6b43f69", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "804996dacf25dd9a20ea112dafc5cfd5abdc85df1ae80adfc2fbc5964968e03c", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": { + "output_bytes": 18446744073709551615, + "symbols": 18446744073709551615, + "working_bytes": 100 + }, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "804996dacf25dd9a20ea112dafc5cfd5abdc85df1ae80adfc2fbc5964968e03c", + "envelope": "cost-over-limit", + "detail": "working bytes", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "460dd25d65a258dca13ebdd426660b58fdb8870b20c5aa78ffd3e4ab2a9a52a6", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "1678f8e0369b591920f4afc82164e1960cf86b6f8e4e57df34a978a4fb438638" + }, + { + "stage": 0, + "producer": "460dd25d65a258dca13ebdd426660b58fdb8870b20c5aa78ffd3e4ab2a9a52a6", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "de306343ebef2de994c77fd4e0bbff94b0884bcefca97d1cdf573665c101d0e5" + } + ], + "clip_sha256": "08b88ef3b0bd8d6cd033ffe1dc09cb6e95bf6bd592ef7ac497fbf01b99d71fae", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/cost_over_limit_working/stage0.bin b/wai/staged-conformance/cases/cost_over_limit_working/stage0.bin new file mode 100644 index 000000000..835f99642 --- /dev/null +++ b/wai/staged-conformance/cases/cost_over_limit_working/stage0.bin @@ -0,0 +1 @@ + 8H00H0HXHH`H`pX`x`pˆpˆˆ0@X@H`@X`P`php€pxˆp�˜x�¨@XhXXp Ððpx€xˆ�x� ˆ ¨˜ ÀXh€°Øÿ¸ØÿÈèÿˆ ¨� ¸ ¸È°¸È¸ØÿÀðÿÈøÿØÿÿàÿÿ ÀÀ¸ÀÐÀÈØˆ� ÐÿÿØÿÿðÿÿ°¸ÈÈÈàÀàèÐèø˜°°¨°Èøÿÿ¸ÈØÀØðØàèèèðèÿÿ°ÀȸÈÐÈÐàÐèðØàøàðÿèøÿÿÿÿ 8H08H0H`HH`H`p``x`xˆpˆˆ0@X@H`HX`P`php€pxˆx�˜x� @XhXXpXpxpx€xˆ�x� ˆ˜¨˜ ÀXh€hx‡px—€ˆŸÈÿÿ� ¸ °È°¸Èxx�x�—€˜§Øÿÿàÿÿèÿÿ¸¸ÐÀÈØˆˆ �§¯ØÿÿðÿÿøÿÿÿÿÿÿÿÿÈèø˜°° °È°·Ïÿÿÿÿÿÿÿÿÿàèðèÿÿ°ÀȸÈÐÈÐØÈàðÿÿÿàðÿèøÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/deadline_held_layer/L1.wai b/wai/staged-conformance/cases/deadline_held_layer/L1.wai new file mode 100644 index 000000000..a33e0fe12 Binary files /dev/null and b/wai/staged-conformance/cases/deadline_held_layer/L1.wai differ diff --git a/wai/staged-conformance/cases/deadline_held_layer/L2.wai b/wai/staged-conformance/cases/deadline_held_layer/L2.wai new file mode 100644 index 000000000..4649e7cd7 Binary files /dev/null and b/wai/staged-conformance/cases/deadline_held_layer/L2.wai differ diff --git a/wai/staged-conformance/cases/deadline_held_layer/base.wai b/wai/staged-conformance/cases/deadline_held_layer/base.wai new file mode 100644 index 000000000..bac78dcb7 Binary files /dev/null and b/wai/staged-conformance/cases/deadline_held_layer/base.wai differ diff --git a/wai/staged-conformance/cases/deadline_held_layer/case.json b/wai/staged-conformance/cases/deadline_held_layer/case.json new file mode 100644 index 000000000..a5c3a81a3 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_held_layer/case.json @@ -0,0 +1,171 @@ +{ + "format": "wai-staged-case/1", + "name": "deadline_held_layer", + "about": "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", + "bases": [ + { + "file": "base.wai", + "object": "9d3fee0b3f8dc53be2c330d26f2d79f9140b6e938b87ac4f96437c887d7e91f8", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "ec6bbf86767acbb42ddf2e5a456a62b7ff62c7f629a8105ac0e8281f7d928fff", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": { + "t0_ms": 100, + "unit_ms": 40, + "target_latency_ms": 100, + "lead_ms": [ + 10, + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 175, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 180, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "3b178bb976cf61d303a743766bcffb139dcafcb51e2d3e9bf6f109e2aff44bf0", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fed27cc0f4641935ead64c558da6becb03dca32a7210df66bebf3aba543e8be8" + }, + { + "stage": 2, + "producer": "82268f093ba4e9a08335d6218a823b79333c972883df2573a44300403e582651", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "38942db7ef49b4787da8d6f264ffbb27271fef8d7fe9a7a577dfccb8baa2a6e3" + } + ], + "clip_sha256": "ee605579ccb95d90c626343ece2fd5e347647f588b73d48ccf01c7432b724bd2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/deadline_held_layer/stage0.bin b/wai/staged-conformance/cases/deadline_held_layer/stage0.bin new file mode 100644 index 000000000..e8800b2c6 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_held_layer/stage0.bin @@ -0,0 +1 @@ +(08(08(HP@HP@PhX`hXh€(080@H0@X@XXPhp`pxhp€h€�@HP@Ph˜Àà`hxhpxp€ˆpˆ˜ˆ� @`h¨Èð°Ðÿ°àøp€˜ˆ�˜ˆ˜°˜ À Øø¨ØÿÀàÿÀðÿÐðÿ˜°°¨¸À¸¸Ðpx˜ÀèÿÐøÿÐÿÿ¨¸À°¸È¸Àà¸Ðàˆ�°˜ ¸àÿÿ¨¸Ð°ÈÐÀØØÈØðÐèø ¨°°°À¸ÀØÀÈØÐÐàØØðàøøèÿÿ(08(0@0HP@HXHPhX`p`h€(080@H0@X@XXPhp`pxhp€p€�@HP@PhP`h`hxhpxp€ˆp€˜ˆ� H`h`hxhp‡h€€¸àÿˆ�˜ˆ˜°˜ ÀXx€`x‡x€�ÀèÿÐðÿàÿÿ¨°À°¸Ðpx˜xˆ—ÐøÿÐÿÿèÿÿøÿÿÿÿÿ¸Ðàˆ�¨˜ ¸˜¯¿èÿÿøÿÿÿÿÿÈØðÐèø ¨°¨°À¸ÀØÀÈØÿÿÿØØðàðøèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/deadline_missed/L1_f0.wai b/wai/staged-conformance/cases/deadline_missed/L1_f0.wai new file mode 100644 index 000000000..5fb9ef046 Binary files /dev/null and b/wai/staged-conformance/cases/deadline_missed/L1_f0.wai differ diff --git a/wai/staged-conformance/cases/deadline_missed/L1_f1.wai b/wai/staged-conformance/cases/deadline_missed/L1_f1.wai new file mode 100644 index 000000000..83077a2bc Binary files /dev/null and b/wai/staged-conformance/cases/deadline_missed/L1_f1.wai differ diff --git a/wai/staged-conformance/cases/deadline_missed/base.wai b/wai/staged-conformance/cases/deadline_missed/base.wai new file mode 100644 index 000000000..a6916acd0 Binary files /dev/null and b/wai/staged-conformance/cases/deadline_missed/base.wai differ diff --git a/wai/staged-conformance/cases/deadline_missed/case.json b/wai/staged-conformance/cases/deadline_missed/case.json new file mode 100644 index 000000000..523f17a07 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_missed/case.json @@ -0,0 +1,155 @@ +{ + "format": "wai-staged-case/1", + "name": "deadline_missed", + "about": "units presented at 140 and 180 with a 30 ms lead: frame 0's layer arrives at 120 (late), frame 1's at 120 (in time)", + "bases": [ + { + "file": "base.wai", + "object": "9b139e7ac5c3231d15e871001ff8cf0649f60391eba0306b83bc0267190f6739", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "d3e3133bbca15a03251e0b8c00c0d62e81151f1cc910210d446683c168c2574a", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_f0.wai", + "object": "783e92af6358629fbc76984280c5b6a88b81f631915d5fdd0074e51befeaa7db", + "bytes": 535 + }, + { + "file": "L1_f1.wai", + "object": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "bytes": 531 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": { + "t0_ms": 100, + "unit_ms": 40, + "target_latency_ms": 40, + "lead_ms": [ + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_f0.wai", + "at_ms": 120, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 120, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "783e92af6358629fbc76984280c5b6a88b81f631915d5fdd0074e51befeaa7db", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ], + [ + { + "object": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "9b139e7ac5c3231d15e871001ff8cf0649f60391eba0306b83bc0267190f6739", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6c9b90150098dfe58322f95d98e6b27e4897bc07b911459e609f3e0789f09210" + }, + { + "stage": 1, + "producer": "11f6d840df88f4ec276d3430307474dd4cc6c213fa687bcb20d092edc153e650", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "66f3a1cc649317d5a1e32bdb8048bd03d2879ad3876dd5003102a9caee49b2a5" + } + ], + "clip_sha256": "1bdf1771136b8f541afd56214d709a0f3e0dde069b41422c9003d0c29e18c771", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/deadline_missed/stage0.bin b/wai/staged-conformance/cases/deadline_missed/stage0.bin new file mode 100644 index 000000000..f32ab0a54 --- /dev/null +++ b/wai/staged-conformance/cases/deadline_missed/stage0.bin @@ -0,0 +1 @@ + 00 0H0HH@PPHP`P`x`hxpp�(@H8HP@P`H`p`hphx€xx�x��HH`HXh¨Èè`xxpx�p��ˆ�¨�¨¨Xh€¨Èÿ¸ÐÿÈèÿˆ�¨�¨¨˜°¸°°È°ØÿÀàÿÈèÿÐÿÿèÿÿ¨°À¨¸ÈÀÈЈˆ Øðÿàÿÿàÿÿ¨¸À¸ÈÐÈÈàÈà𘠰 ¸Àøÿÿ°ÈØ¸ØØÈØðàèðèðÿ¨°È¸ÀÐÀØØÐÐèØàøàèøðøÿøÿÿ 00 0H0HH@PXHP`P`x`hxppˆ0@H8HP@P`Php`hphx€xx�x��HP`PXp`hp`xxp€�x��ˆ�¨�¨¨Xh€`h‡pp‡€ˆ—Èðÿˆ¨¨˜°¸¨°Èhx‡x€—€ˆ—Èÿÿèÿÿðÿÿ¨¸È¸ÈЈˆ ��§ØÿÿàÿÿðÿÿÿÿÿÿÿÿÈàð� ° °À°¿ÇøÿÿÿÿÿÿÿÿØèðèðÿ¨°È¸ÀÐÀÐØÈÐàÿÿÿàèøðøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/evidence_upgrade/L1.wai b/wai/staged-conformance/cases/evidence_upgrade/L1.wai new file mode 100644 index 000000000..8032aee70 Binary files /dev/null and b/wai/staged-conformance/cases/evidence_upgrade/L1.wai differ diff --git a/wai/staged-conformance/cases/evidence_upgrade/base.wai b/wai/staged-conformance/cases/evidence_upgrade/base.wai new file mode 100644 index 000000000..c6d87266d Binary files /dev/null and b/wai/staged-conformance/cases/evidence_upgrade/base.wai differ diff --git a/wai/staged-conformance/cases/evidence_upgrade/case.json b/wai/staged-conformance/cases/evidence_upgrade/case.json new file mode 100644 index 000000000..2c73b0cbf --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade/case.json @@ -0,0 +1,149 @@ +{ + "format": "wai-staged-case/1", + "name": "evidence_upgrade", + "about": "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", + "bases": [ + { + "file": "base.wai", + "object": "b3a636280af97f4dfaaef711a6d705030f40b22d912652300c62f0228827544d", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5d3b649a55ad222c8585292dac654b0828d95311501a94da9cee15e1ea65090e", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 5, + "evidence": "delivered-verified" + }, + { + "op": "base", + "index": 0, + "at_ms": 6 + } + ], + "expect": { + "reports": [ + [ + { + "object": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "92bc090e6b0ce9e73a9cea5e85e8a3e762b3bb99f9cea18ee07b69c2bdc39aad" + }, + { + "stage": 1, + "producer": "23d1abb0c7cb72c71f5a9704b28587f40de559d3e38adc8b11f961e09c493a9a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "baec27a25765715583998dbbe0465a998988b6795f00bdf3e783948084c81ac8" + } + ], + "clip_sha256": "97edd7b43a75c09b47bed54626f511065c1abba7e3dbfffe7aa3398eca890a7b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/evidence_upgrade/stage0.bin b/wai/staged-conformance/cases/evidence_upgrade/stage0.bin new file mode 100644 index 000000000..8294ee80a --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade/stage0.bin @@ -0,0 +1 @@ + 0 0@0@H@@X@XXXXpXh€hx€0@@@@X8XXP`hX`€hp€px˜p�˜8PhP``˜ÀðXp€h€€€€�x˜¨˜˜¸P`p¨Ðð°ÐøÀØÿ€˜ � °˜¨¸˜¸À°Ðø¸èÿÀèÿØøÿØÿÿ °À°ÀȸÈЀ�˜Èðÿàøÿèÿÿ°¸ÈÀÀØÀÈØÈØà� °˜ Àèÿÿ°ÈÈ¸ÐØÈàèÈèøàèÿ°°È°ÀÈÀÈàÈÐàÈàèØèðàøÿøÿÿ 8 0H0@H@@X@XXXXpXh€hx€0@@@@X@XXP`pXh€hp€px˜p�˜8PhP`hP`x`p€h€€€€�x˜ �˜°P`p`pxpp€xx�Èøÿ�˜°˜¨¸˜¸Àhp€pˆ�xˆŸØøÿØÿÿèÿÿ°ÀÀ¸ÈЀ�˜€�§àøÿèÿÿøÿÿÿÿÿÿÿÿÈØà� °˜ À ¯ÏðÿÿÿÿÿÿÿÿÈèøàèÿ¨°È°¸ÈÀÈàÈÐàÿÿÿØèðàðÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/L1.wai b/wai/staged-conformance/cases/evidence_upgrade_deadline/L1.wai new file mode 100644 index 000000000..a0605585e Binary files /dev/null and b/wai/staged-conformance/cases/evidence_upgrade_deadline/L1.wai differ diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai b/wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai new file mode 100644 index 000000000..c94dd7c4c Binary files /dev/null and b/wai/staged-conformance/cases/evidence_upgrade_deadline/base.wai differ diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json new file mode 100644 index 000000000..29eb58655 --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade_deadline/case.json @@ -0,0 +1,160 @@ +{ + "format": "wai-staged-case/1", + "name": "evidence_upgrade_deadline", + "about": "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", + "bases": [ + { + "file": "base.wai", + "object": "e59fdd4e7c37a73cb52cfe892da45ee145ca2adf0bc5e59dcf6d12c8d0396e7e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "fbef590b43726dec81cc945d337b760f96605ac7e1dfc550b559c36e3f63f166", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": { + "t0_ms": 0, + "unit_ms": 40, + "target_latency_ms": 100, + "lead_ms": [ + 96 + ] + } + }, + "steps": [ + { + "op": "offer", + "file": "L1.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 5, + "evidence": "delivered-verified" + }, + { + "op": "base", + "index": 0, + "at_ms": 6 + } + ], + "expect": { + "reports": [ + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [ + [ + 0, + "deadline-missed" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "e59fdd4e7c37a73cb52cfe892da45ee145ca2adf0bc5e59dcf6d12c8d0396e7e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e644f1ac16fa13e720e65273eb0877cb6316655c7339eb23a87912940469cfbe" + }, + { + "stage": 1, + "producer": "3c41fb403d64bd906ed33337da8ea31951e0bb300bb4dcbd5355266e28474e94", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ef15f3ec3f859551f8c4ec5fdc81a327739644428ba39bee65309a533178e484" + } + ], + "clip_sha256": "f9a7d9010bd6d8b13be120607274ca4ea0f5c57dee5a2fd7c177d293d92918f6", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin b/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin new file mode 100644 index 000000000..209bb5806 --- /dev/null +++ b/wai/staged-conformance/cases/evidence_upgrade_deadline/stage0.bin @@ -0,0 +1 @@ + @(8H(8H8H`HX`HXp`pppxˆ08P0H`HX`HhxPpxhpˆxˆˆxˆ @XhP`p¨Ðð`xˆxˆˆxˆ˜ˆ�¨� ¸Phx¨Èð¸àøÀàÿˆ�˜ˆ °˜¨¸¨¸À¨ØÿÀàÿÈðÿÐÿÿàÿÿ¨°À¨ÀиÀà€ˆ Øøÿàÿÿðÿÿ°¸À¸ÈÐÀØØÈà蘘¨ °Àøÿÿ°ÈØÈÈØÐÐàØàøàðø¨¸È°ÀظÐàÈÐèÐèðØðÿèÿÿðÿÿ @(8H(@P8P`HX`P`p`xppxˆ08P8H`HX`HhxPpxhpˆxˆˆxˆ HXhPhp`pp`xˆxˆˆxˆ˜ˆ�¨ˆ °Xhx`hxp€€€€—Ððÿˆ˜°˜¨¸¨¸À`x‡x€�€�§ÐÿÿØÿÿðÿÿ¨ÀиÀà€ˆ �˜ŸàÿÿðÿÿøÿÿøÿÿÿÿÿÈà蘘¨ °À°·¿ðÿÿÿÿÿÿÿÿØàøàèø °È°ÀظÈàÈÐàÿÿÿØðÿèÿÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/frozen_refuses_held/L1a.wai b/wai/staged-conformance/cases/frozen_refuses_held/L1a.wai new file mode 100644 index 000000000..4ef67d78c Binary files /dev/null and b/wai/staged-conformance/cases/frozen_refuses_held/L1a.wai differ diff --git a/wai/staged-conformance/cases/frozen_refuses_held/L1b.wai b/wai/staged-conformance/cases/frozen_refuses_held/L1b.wai new file mode 100644 index 000000000..207389267 Binary files /dev/null and b/wai/staged-conformance/cases/frozen_refuses_held/L1b.wai differ diff --git a/wai/staged-conformance/cases/frozen_refuses_held/L3.wai b/wai/staged-conformance/cases/frozen_refuses_held/L3.wai new file mode 100644 index 000000000..bf00961c8 Binary files /dev/null and b/wai/staged-conformance/cases/frozen_refuses_held/L3.wai differ diff --git a/wai/staged-conformance/cases/frozen_refuses_held/base.wai b/wai/staged-conformance/cases/frozen_refuses_held/base.wai new file mode 100644 index 000000000..ad0b336b1 Binary files /dev/null and b/wai/staged-conformance/cases/frozen_refuses_held/base.wai differ diff --git a/wai/staged-conformance/cases/frozen_refuses_held/case.json b/wai/staged-conformance/cases/frozen_refuses_held/case.json new file mode 100644 index 000000000..c23565832 --- /dev/null +++ b/wai/staged-conformance/cases/frozen_refuses_held/case.json @@ -0,0 +1,196 @@ +{ + "format": "wai-staged-case/1", + "name": "frozen_refuses_held", + "about": "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", + "bases": [ + { + "file": "base.wai", + "object": "3b22769b3655d2d40e6088c198acf1c621aef4fc03a87cd3059e76594f7817e4", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "fca9fd5587687b760e9cf7404ec9b31728c35893cf5473c9697a1e0a696a418e", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L3.wai", + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "bytes": 791 + }, + { + "file": "L1a.wai", + "object": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "bytes": 627 + }, + { + "file": "L1b.wai", + "object": "60a22722db1888adb6276ec3d000afdd22a48780a538f0e56d0fcd95a9a08094", + "bytes": 631 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 3, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "60a22722db1888adb6276ec3d000afdd22a48780a538f0e56d0fcd95a9a08094", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "0f0220cdae1ca73c04708465a2f09932704a86202d377e0ac9845ac2f134a79c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "02e96440c5b5c89c4f4a640743c0ee168e90614bed124db87cf92c982d77b144" + }, + { + "stage": 1, + "producer": "e067e3140e46cf850faa7fbc2b4640408e47c091905fe6b49c96651541a0fd58", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cda9839d1d0a4092fc8b1fe55bcf766e3441268ec2166da0d6b20d08daf68641" + } + ], + "clip_sha256": "98e342cc102fba975a75e08ca3e3fdfb83c271443b66516eb7b6c3ad35579c2c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin b/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin new file mode 100644 index 000000000..825309ccb --- /dev/null +++ b/wai/staged-conformance/cases/frozen_refuses_held/stage0.bin @@ -0,0 +1 @@ +(0@0HP@HXHPhXhp`hˆ`€ˆx€�@HXHX`H`pXpxpp�px˜€�˜ˆ�°H`pXpp°Ðøhˆ˜€�˜€˜¨˜¨¸˜¸À`xˆ°àÿ¸ðÿÀðÿ�¨¨  À°°È¸ÈÐÀàÿÈðÿÐÿÿàÿÿàÿÿ°ÀÐÀÈàÀØà�˜ àÿÿàÿÿøÿÿ¸ÈÐÀØèÈàðØàø¨°À¨ÀÈðÿÿÈÈàÐØààèðàðÿøøÿ°ÈØÀÈàÈÐèÐàøèøøèÿÿøÿÿÿÿÿ(0@0HP@PXHPhXhp`h€h€ˆx€ˆ@HXH``H`xXpxppˆpx˜€ˆ˜ˆ�°H`pXpppp€hˆ�€�˜€�¨�¨¸˜°À`x€h€‡p��€�§Øÿÿ˜ À°°À¸ÈÐx€Ÿ€�§ˆŸ¯àÿÿàÿÿðÿÿÀÀàÀÐà�˜ ˜§¯àÿÿøÿÿÿÿÿÿÿÿÿÿÿØàø¨°¸¨ÀȰ¿ÏÿÿÿÿÿÿÿÿÿØðÿðøÿ°ÈظÈàÈÐèÐØøÿÿÿèÿÿøÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/geometry_mismatch/L1.wai b/wai/staged-conformance/cases/geometry_mismatch/L1.wai new file mode 100644 index 000000000..0ed012e58 Binary files /dev/null and b/wai/staged-conformance/cases/geometry_mismatch/L1.wai differ diff --git a/wai/staged-conformance/cases/geometry_mismatch/base.wai b/wai/staged-conformance/cases/geometry_mismatch/base.wai new file mode 100644 index 000000000..6db26ad1d Binary files /dev/null and b/wai/staged-conformance/cases/geometry_mismatch/base.wai differ diff --git a/wai/staged-conformance/cases/geometry_mismatch/case.json b/wai/staged-conformance/cases/geometry_mismatch/case.json new file mode 100644 index 000000000..a8d6b93ba --- /dev/null +++ b/wai/staged-conformance/cases/geometry_mismatch/case.json @@ -0,0 +1,128 @@ +{ + "format": "wai-staged-case/1", + "name": "geometry_mismatch", + "about": "a layer coded for 4×4 frames over 8×8 frames: geometry-mismatch for each unit", + "bases": [ + { + "file": "base.wai", + "object": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "a351f27f93e94c491659d57aec589a1faeee7f5959eff2ecead77096eab14be6", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "cbd4beb0765824c6f587b115842f0779b8b74c5a7db1f16990b3d34c2c1ba46c", + "bytes": 415 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "cbd4beb0765824c6f587b115842f0779b8b74c5a7db1f16990b3d34c2c1ba46c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "geometry-mismatch" + ], + [ + 1, + "geometry-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "718de625810da6093c2a2bf758421dbc9d2fba12cfaf32a9a66af6980674552e" + }, + { + "stage": 0, + "producer": "e28d3948368ac049f8a5fff3134677c9fc50b87a6b1b353b46b7845b55cf4374", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "44a045223af10d71d4d19f12c0e272407638bd221e78a05a19aa66e24a13ca48" + } + ], + "clip_sha256": "313eabaa3073a65f3307278dd1ef33d59b156aee8a4748eda0fa9571a1e6ac41", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/geometry_mismatch/stage0.bin b/wai/staged-conformance/cases/geometry_mismatch/stage0.bin new file mode 100644 index 000000000..9a91c4fd4 --- /dev/null +++ b/wai/staged-conformance/cases/geometry_mismatch/stage0.bin @@ -0,0 +1 @@ + 8H8@H8PXHPpPhp`hˆh€�xˆ�8@XHP`P`x``xpp€p€ˆˆ� ��°Hhp`pp Ðøhxˆx�˜��¨�¨¸˜°¸`hˆ°èÿ¸ðÿÐðÿ� ¸˜°¸¨¸À°ÀÈÀèÿÈøÿÐÿÿàÿÿðÿÿ¸¸È¸ÐØÐÐè�˜¨àÿÿàÿÿðÿÿ°ÀàÈÐàÐØèÐèÿ˜°¸°¸ÀÿÿÿÀÐØÐØðØàøèøøðÿÿ¸ÈÐ¸ÐØÈàèÐèøØðÿðøÿÿÿÿÿÿÿ @H8@H8PXHPpPhp`hˆh€ˆxˆ�8@XHP`P`x``xpp€p€ˆˆ� ˆ�°Hhp`px`p€pxˆx�˜��¨� ¸˜°¸`hˆhˆ‡x�—ˆ�ŸØÿÿ˜°¸¨¸¸°ÀÈxˆ�€˜§ˆ§¯àÿÿðÿÿøÿÿ¸ÐØÈÐè�˜ ˜§·àÿÿðÿÿøÿÿÿÿÿÿÿÿÐèÿ˜°¸°¸À·ÇÇÿÿÿÿÿÿÿÿÿàøøðÿÿ¸ÀÐ¸ÐØÀàèÐèøÿÿÿðøÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai b/wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai new file mode 100644 index 000000000..dbdf29c8f Binary files /dev/null and b/wai/staged-conformance/cases/held_then_prev_mismatch/L1b.wai differ diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai b/wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai new file mode 100644 index 000000000..2c4479062 Binary files /dev/null and b/wai/staged-conformance/cases/held_then_prev_mismatch/L2.wai differ diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/base.wai b/wai/staged-conformance/cases/held_then_prev_mismatch/base.wai new file mode 100644 index 000000000..18c3b3d46 Binary files /dev/null and b/wai/staged-conformance/cases/held_then_prev_mismatch/base.wai differ diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/case.json b/wai/staged-conformance/cases/held_then_prev_mismatch/case.json new file mode 100644 index 000000000..623e532e2 --- /dev/null +++ b/wai/staged-conformance/cases/held_then_prev_mismatch/case.json @@ -0,0 +1,165 @@ +{ + "format": "wai-staged-case/1", + "name": "held_then_prev_mismatch", + "about": "a layer 2 over layer 1a is held; layer 1b applies instead and releases it: the units' producer is 1b, so layer 2 is prev-mismatch", + "bases": [ + { + "file": "base.wai", + "object": "cd82ab300acc83e107c1fea5b0f5b1e1ce8f1c02007b38f630787731f0061c29", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "955af093ad6f5a0d6c51f2ae3067cff3202c4aa352a61777e9f23f21a3302ff3", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2.wai", + "object": "995af5d5a0314e856a80f2cbc28cb3b877c82c71724757f1ff3f51b1e424477f", + "bytes": 795 + }, + { + "file": "L1b.wai", + "object": "2606f6dca5ad7e4e27af479ca0f3e5422594977e58f25b4d20714e75826af744", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "995af5d5a0314e856a80f2cbc28cb3b877c82c71724757f1ff3f51b1e424477f", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "2606f6dca5ad7e4e27af479ca0f3e5422594977e58f25b4d20714e75826af744", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "995af5d5a0314e856a80f2cbc28cb3b877c82c71724757f1ff3f51b1e424477f", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "prev-mismatch" + ], + [ + 1, + "prev-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "2606f6dca5ad7e4e27af479ca0f3e5422594977e58f25b4d20714e75826af744", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "c1e2b35601084938df1047442088e0af8017f4b4279362be9ff12cb8f6835a79" + }, + { + "stage": 1, + "producer": "2606f6dca5ad7e4e27af479ca0f3e5422594977e58f25b4d20714e75826af744", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ab4cf24e895fa91e1577f03d381bc625407d366d62276c9f4164cca3b1e9b24d" + } + ], + "clip_sha256": "83265622ac3f3f335c5d4df9231ba2c4ba27617722a2a9b446084740cee13fb2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/held_then_prev_mismatch/stage0.bin b/wai/staged-conformance/cases/held_then_prev_mismatch/stage0.bin new file mode 100644 index 000000000..6f2d5f46d --- /dev/null +++ b/wai/staged-conformance/cases/held_then_prev_mismatch/stage0.bin @@ -0,0 +1 @@ + ( 0 8H08H0H`HXXHXhXpp080@H0@HHP`PXpPhp`pˆppˆ0@P8PX�¸èX`p`p€px€p€˜ˆˆ @P`�Èè Èø°Øÿpx˜ˆˆ €˜ � ¸ Ðð¨Èÿ°àÿ¸èÿÈøÿ� ¨ ¨¸¨¸Èh€ˆÀàÿÈðÿØÿÿ˜°¸ °È¸¸ÐÀÈØ€� ˆ˜°àÿÿ¨°À°ÀиÈàÀÐèÐØø� °¨¨À°¸ÈÀÈиØàÐàèÐèøèðÿ ( 0 8H08H0H`HXXH`pXpp080@H0@HHP`PXpPhp`pˆppˆ0@P8PXHXpX`p`p€px€p€�ˆˆ @P`HhpXh€hx‡¸Øÿˆˆ €˜ � ¸Xpx`h‡h€�¸èÿÈðÿØøÿ ¨¸ ¸Èh€ˆx€ŸÈðÿØÿÿàÿÿèÿÿÿÿÿÀÈЀ� ˆ˜°˜§¯ðÿÿøÿÿøÿÿÀÈàÈØø� °¨¨À°°ÈÀÈÐÿÿÿÐØèÐèøèèÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/hold_bound_count/L1a.wai b/wai/staged-conformance/cases/hold_bound_count/L1a.wai new file mode 100644 index 000000000..72dad1667 Binary files /dev/null and b/wai/staged-conformance/cases/hold_bound_count/L1a.wai differ diff --git a/wai/staged-conformance/cases/hold_bound_count/L1b.wai b/wai/staged-conformance/cases/hold_bound_count/L1b.wai new file mode 100644 index 000000000..5d162a48c Binary files /dev/null and b/wai/staged-conformance/cases/hold_bound_count/L1b.wai differ diff --git a/wai/staged-conformance/cases/hold_bound_count/base.wai b/wai/staged-conformance/cases/hold_bound_count/base.wai new file mode 100644 index 000000000..ea6d9f209 Binary files /dev/null and b/wai/staged-conformance/cases/hold_bound_count/base.wai differ diff --git a/wai/staged-conformance/cases/hold_bound_count/case.json b/wai/staged-conformance/cases/hold_bound_count/case.json new file mode 100644 index 000000000..81db11b04 --- /dev/null +++ b/wai/staged-conformance/cases/hold_bound_count/case.json @@ -0,0 +1,154 @@ +{ + "format": "wai-staged-case/1", + "name": "hold_bound_count", + "about": "a bound of one held envelope: the first layer is held for its base, the second is base-absent on arrival", + "bases": [ + { + "file": "base.wai", + "object": "62e1cdcdbea1c01f911ef377af4f4c70511ccff2160fa089cce2c5e64472891c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "356c145b9dcaae2533dcf092657319ec7b0123a9024f14539a2ae52ba461ad71", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "bytes": 575 + }, + { + "file": "L1b.wai", + "object": "4f74a6d56381c49084a446bef4e7940055f7fee8e41c944329e3f00b94b4c28d", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 1, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4f74a6d56381c49084a446bef4e7940055f7fee8e41c944329e3f00b94b4c28d", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d05d257f52caba6d9d4fd3a60f4a197cd57a44354c37e879a093f746772784c8" + }, + { + "stage": 1, + "producer": "cabbab54708aba4e296c646fd4a02d16106c778fa3597a56aeafac6bdb0d1909", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "30b28e1cf0b5745f0463c7af8735ffcca8c7a0ec213ef75e694a2660c6dd63d7" + } + ], + "clip_sha256": "ac1fa9507d1004bcc1eaaf366da0013acd8c196dd5fdc2448232847348d75058", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/hold_bound_count/stage0.bin b/wai/staged-conformance/cases/hold_bound_count/stage0.bin new file mode 100644 index 000000000..b7d04f4a9 --- /dev/null +++ b/wai/staged-conformance/cases/hold_bound_count/stage0.bin @@ -0,0 +1 @@ + 8 0H8HP@HXHXhX`xXh€pxˆ(@P@HP@`hX`pXpphpˆp€˜xˆ¨@PhX`h Èøhx€xˆ�xˆ ˆ�¨� ¨X`x°Øø°ØÿÀàÿˆ˜ � ¨˜°¸¨°À¸ØÿÀèÿÀøÿÐøÿàÿÿ¨¸À°ÀÈÀÈØ€� Èðÿàÿÿèÿÿ¸¸Ð¸ÐØÀÐØÈàð� ¸ °ÈðÿÿÀÈÐÈØàÐØèØððèðÿ¨¸È°ÀظÐàÐØðÐàøèðÿèøÿøÿÿ (8 8H8HP@HXH`hXhxXh€px€(@P@HP@`hX`p`pxhpˆp€˜€ˆ¨@PhX`hXh€hx€x€�€ˆ ˆ� � ¨X`xhx€hx�x€ŸÐøÿ�˜¨�°¸¨°Àpx�xˆ�x˜—ÐøÿØÿÿðÿÿ°ÀÈÀÈØ€ˆ ˆ�§àÿÿèÿÿÿÿÿÿÿÿÿÿÿÈàð� ¸ °È¨¿ÏÿÿÿÿÿÿÿÿÿØððèðÿ¨°È°ÀظÈàÐØèÿÿÿèèÿèøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/L1.wai b/wai/staged-conformance/cases/image_int_hyper_2l/L1.wai new file mode 100644 index 000000000..3ecbdd2cf Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_2l/L1.wai differ diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/L2.wai b/wai/staged-conformance/cases/image_int_hyper_2l/L2.wai new file mode 100644 index 000000000..65f97ca5a Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_2l/L2.wai differ diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/base.wai b/wai/staged-conformance/cases/image_int_hyper_2l/base.wai new file mode 100644 index 000000000..b5d7ce17d Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_2l/base.wai differ diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/case.json b/wai/staged-conformance/cases/image_int_hyper_2l/case.json new file mode 100644 index 000000000..3f3a17b38 --- /dev/null +++ b/wai/staged-conformance/cases/image_int_hyper_2l/case.json @@ -0,0 +1,129 @@ +{ + "format": "wai-staged-case/1", + "name": "image_int_hyper_2l", + "about": "a pinned wai.neural.int_hyper base, then two identity layers", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "4fc1bd94bd48b2c1ded99c0053eed742f5399cec09dbd2f8694cca6a30e44875", + "bytes": 1071 + }, + { + "file": "L2.wai", + "object": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "bytes": 631 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "4fc1bd94bd48b2c1ded99c0053eed742f5399cec09dbd2f8694cca6a30e44875", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7bf0f413db62aa185969b9ccf925d40f0281601268899bb7f2e97344487f4189", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "a73f8f964dd0b93d32f14b35a80c3c96bb0c98ce77359bc208125cce96804ff0" + } + ], + "clip_sha256": "6f693b31869a008c460dfd0563ca14f213cf3634776362841a398da7b8369caa", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin b/wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin new file mode 100644 index 000000000..4b754aa32 Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_2l/stage0.bin differ diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai b/wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai new file mode 100644 index 000000000..742c9067b Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_unpinned/L1.wai differ diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/base.wai b/wai/staged-conformance/cases/image_int_hyper_unpinned/base.wai new file mode 100644 index 000000000..444eb9dea Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_unpinned/base.wai differ diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json new file mode 100644 index 000000000..4862747c7 --- /dev/null +++ b/wai/staged-conformance/cases/image_int_hyper_unpinned/case.json @@ -0,0 +1,105 @@ +{ + "format": "wai-staged-case/1", + "name": "image_int_hyper_unpinned", + "about": "an unpinned wai.neural.int_hyper base: the layer applies, and no unit holds a tier, so no stage hash is stated", + "bases": [ + { + "file": "base.wai", + "object": "3fc4569e249bf6b7fbb3b5f7145688ba8f8b63368a0714ec52d9376034ebacf2", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "none", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "bd735bc642caf68dff419458489ccdcf79994910ef6146e14658e8d643e1be47", + "bytes": 1071 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "bd735bc642caf68dff419458489ccdcf79994910ef6146e14658e8d643e1be47", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "bd735bc642caf68dff419458489ccdcf79994910ef6146e14658e8d643e1be47", + "frozen": false, + "tier": "none", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": null + } + ], + "clip_sha256": null, + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_int_hyper_unpinned/stage0.bin b/wai/staged-conformance/cases/image_int_hyper_unpinned/stage0.bin new file mode 100644 index 000000000..4b754aa32 Binary files /dev/null and b/wai/staged-conformance/cases/image_int_hyper_unpinned/stage0.bin differ diff --git a/wai/staged-conformance/cases/image_png_up2/L1.wai b/wai/staged-conformance/cases/image_png_up2/L1.wai new file mode 100644 index 000000000..a126806df Binary files /dev/null and b/wai/staged-conformance/cases/image_png_up2/L1.wai differ diff --git a/wai/staged-conformance/cases/image_png_up2/base.wai b/wai/staged-conformance/cases/image_png_up2/base.wai new file mode 100644 index 000000000..7da83fe95 Binary files /dev/null and b/wai/staged-conformance/cases/image_png_up2/base.wai differ diff --git a/wai/staged-conformance/cases/image_png_up2/case.json b/wai/staged-conformance/cases/image_png_up2/case.json new file mode 100644 index 000000000..931e4bc31 --- /dev/null +++ b/wai/staged-conformance/cases/image_png_up2/case.json @@ -0,0 +1,100 @@ +{ + "format": "wai-staged-case/1", + "name": "image_png_up2", + "about": "a PNG base (a stage-0 fixture here), an up2 layer at q = 1", + "bases": [ + { + "file": "base.wai", + "object": "4c8d9bd9c4bb6aee9a7fdefb3dc880021e0a8152722d638d007aaafe6ddeba69", + "decoded_via": "wai.image.png", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "308e58555d50408420a50a50eadbfd02ca35d3293c022ccc56a38721661e2b47", + "decode": { + "kind": "png" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "bytes": 3439 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "364ef2830bb6b63e18d2524c521b5a9bc45813775eb98542fe5d4d3c2c835500", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 32, + 32, + 3 + ], + "stage_sha256": "9eecf15c0bedefea68924d3895462dd5a1958603a4ed0d85976519dcb37a9ac0" + } + ], + "clip_sha256": "d90170c38e38b10a4c6c4ebf129dc5aa577521373ecdf020b6b66207027f004e", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_png_up2/stage0.bin b/wai/staged-conformance/cases/image_png_up2/stage0.bin new file mode 100644 index 000000000..8d82dad59 --- /dev/null +++ b/wai/staged-conformance/cases/image_png_up2/stage0.bin @@ -0,0 +1 @@ +)&5)2'A6?*3M(BK7@J4NXCLV@[dOYbMWq\enZc}5%: 26)/D%8@0:F+FT>DP:U_Xb‹®Þœ¶ã¡»ë¦ÉèŸÎý¢Îù·Óÿµ×ÿ½àÿ|Ž €’–…�¨Ž—£–�¯�¡·JTn˜Èí Âï£Äú«Öÿ«Û÷²Ýÿ¶ãÿºîÿÅåÿ‡�¦Š•¡–�¯›¢¶¢£¸¦©½�¿ó£Äø¨Êþ®Ñÿ³ÖÿºÝÿÁåÿÉìÿÂõÿÇûÿÏøÿ•Ÿº›¥¿¡«Æ©²½°ºÄcm‡±Íü¸Òÿ¶çÿ¼êÿÀäÿÅèÿÑñÿ×úÿÖÿÿ•§½–­Âœ®Ê¯»Ç°¹ÇºÀÌpy‚ºåÿ¿çÿ¿áÿÈéÿÕïÿÚôÿÚýÿãÿÿåÿÿ£¶È¤¯Î®¸Ó·ÉÍ»ÍßÄÎØ|…�»ìÿÇêÿÐìÿÙóÿÕÿÿàÿÿßÿÿèÿÿñÿÿ¨ÃȪ¾Ò±ÃÖ¿ÆàÂÐæÎÚæw’›‚”¦ÓöÿÖüÿÓÿÿåÿÿæÿÿóÿÿíÿÿ°ÂÖ¼ÆÐ·ÊÒ»ÓÝËÝÞÍÕðØêô„ž§’›§�¢¸•¬¸Ÿ±»ñÿÿ²¼ÉªÂÖ½ÅϹÅÑÅÖÛÈÏìÑãìÏã÷Øâðãéþ�ª´Ÿ©°–¨º£µ¼±»Å­·Ì¬¿Ñ»ÎÓÁÌÖÊÔêÒÓçÌÞôÙãïÛåüÞñÿëþÿœ¦Àœ´½©¶Ì¦ÂεÇÙ¾ÅÙ½ÏØÌÏãÇÑìÎåñÍèûÖäðÛîÿçíÿå÷ÿóÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai b/wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai new file mode 100644 index 000000000..a9b962d65 Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_from_int_hyper/L1.wai differ diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/L2.wai b/wai/staged-conformance/cases/image_up2_from_int_hyper/L2.wai new file mode 100644 index 000000000..4ccf21c35 Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_from_int_hyper/L2.wai differ diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai b/wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai new file mode 100644 index 000000000..b5d7ce17d Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_from_int_hyper/base.wai differ diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json new file mode 100644 index 000000000..625d44ca4 --- /dev/null +++ b/wai/staged-conformance/cases/image_up2_from_int_hyper/case.json @@ -0,0 +1,129 @@ +{ + "format": "wai-staged-case/1", + "name": "image_up2_from_int_hyper", + "about": "a 16×16 base, an up2 layer to 32×32, then an identity layer", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "90f3de688b569aca302e82601b0f8ad60e9835c9e359185e84e79964118c1487", + "bytes": 3119 + }, + { + "file": "L2.wai", + "object": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "bytes": 1219 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "90f3de688b569aca302e82601b0f8ad60e9835c9e359185e84e79964118c1487", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "f6c3dafa7ad80b2e36846b5cd26b887505be2498336b5473d5c7071d34474811", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 32, + 32, + 3 + ], + "stage_sha256": "00137b8b9fe5ea5b802d961ca247e7f4e5bbf501c51d741215ee22dfda465d11" + } + ], + "clip_sha256": "095a966d4a0e7f066e7c8cd5e7593c77951c6f554d87d402e32225dc55f7109d", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin b/wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin new file mode 100644 index 000000000..4b754aa32 Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_from_int_hyper/stage0.bin differ diff --git a/wai/staged-conformance/cases/image_up2_odd/L1.wai b/wai/staged-conformance/cases/image_up2_odd/L1.wai new file mode 100644 index 000000000..dadc6156e Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_odd/L1.wai differ diff --git a/wai/staged-conformance/cases/image_up2_odd/L2.wai b/wai/staged-conformance/cases/image_up2_odd/L2.wai new file mode 100644 index 000000000..c2784df8f Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_odd/L2.wai differ diff --git a/wai/staged-conformance/cases/image_up2_odd/base.wai b/wai/staged-conformance/cases/image_up2_odd/base.wai new file mode 100644 index 000000000..b5d7ce17d Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_odd/base.wai differ diff --git a/wai/staged-conformance/cases/image_up2_odd/case.json b/wai/staged-conformance/cases/image_up2_odd/case.json new file mode 100644 index 000000000..b1b94147a --- /dev/null +++ b/wai/staged-conformance/cases/image_up2_odd/case.json @@ -0,0 +1,129 @@ +{ + "format": "wai-staged-case/1", + "name": "image_up2_odd", + "about": "up2 to an odd size, then up2 from it: 16×16 → 31×31 → 61×61", + "bases": [ + { + "file": "base.wai", + "object": "aaf90792d6ff5318e5f306fbb000dfcbe73637e244aad1a2eff6dae537cd6b5f", + "decoded_via": "wai.neural.int_hyper", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dce7282d46f88f03d5dc0fd42f325756d310fe1fc982b6e1792a1fbfbeb482cf", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "6a80778b94cdfd81f5b7e7af0b10d0e7717cc8b050202a3ebef961fc7fbcc4bf", + "bytes": 3331 + }, + { + "file": "L2.wai", + "object": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "bytes": 4843 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6a80778b94cdfd81f5b7e7af0b10d0e7717cc8b050202a3ebef961fc7fbcc4bf", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "4d11da89364c6163e0c2034697a0a9d14f1e7f36cffe504fe4b774befd9fc46e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 61, + 61, + 3 + ], + "stage_sha256": "384c55f3535d2443939c0c22508935c6de3ec5a1af1a3b3e3d525631a8c1f894" + } + ], + "clip_sha256": "64dba25135e5965fd511247477f911b85aa5a9b2fae263b64e791b72c9c43397", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/image_up2_odd/stage0.bin b/wai/staged-conformance/cases/image_up2_odd/stage0.bin new file mode 100644 index 000000000..4b754aa32 Binary files /dev/null and b/wai/staged-conformance/cases/image_up2_odd/stage0.bin differ diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/L1.wai b/wai/staged-conformance/cases/inclusion_only_evidence/L1.wai new file mode 100644 index 000000000..c5b70bb9e Binary files /dev/null and b/wai/staged-conformance/cases/inclusion_only_evidence/L1.wai differ diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/base.wai b/wai/staged-conformance/cases/inclusion_only_evidence/base.wai new file mode 100644 index 000000000..89db69b79 Binary files /dev/null and b/wai/staged-conformance/cases/inclusion_only_evidence/base.wai differ diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/case.json b/wai/staged-conformance/cases/inclusion_only_evidence/case.json new file mode 100644 index 000000000..1f6b636a3 --- /dev/null +++ b/wai/staged-conformance/cases/inclusion_only_evidence/case.json @@ -0,0 +1,139 @@ +{ + "format": "wai-staged-case/1", + "name": "inclusion_only_evidence", + "about": "an attested base: the layer verified by inclusion only is refinement-unattested; the same layer delivered-verified applies", + "bases": [ + { + "file": "base.wai", + "object": "cccd9393d0329558e25ddefda3ac7a013c49873fd50e912be574d98b4ddad419", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": true, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "37c2525de082ab17d6d5470b382702053b6d7c2db4399b5ccf601b8d271f21fd", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "inclusion-verified" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "delivered-verified" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "envelope": "refinement-unattested", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "67d4c6297199350f0f53db1036c869026b9698209007793eb4c0bd6d70b06812" + }, + { + "stage": 1, + "producer": "c2df321aa4133a60d01dd78fcc3210669175cc93fab003c4e33a6785bec8b24c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e5cd07ceba0338fae1880e57026e7d3d466e9f0935a12706435e8ac69d378e6e" + } + ], + "clip_sha256": "b00fb4f9ce8446663603cd9b7911308473bc53b02daf6603cf68a3bca6eaf22d", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin b/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin new file mode 100644 index 000000000..2a0a3f4e0 --- /dev/null +++ b/wai/staged-conformance/cases/inclusion_only_evidence/stage0.bin @@ -0,0 +1 @@ +08((H8@H8@`@X`P`hX`xhx€ @H0HX@XXPXpXhphpxh€�€ˆ�8X`@Ph˜Èð`hxh€�p€˜ˆ˜ �˜¨P`x¨Èð°Ðÿ°àÿ€˜ �˜¨�¨° °¸°Øø¸èÿÀèÿÐøÿØÿÿ¨¨¸¨ÀȰÀØ€� ÈðÿØÿÿàÿÿ °Ð¨ÀÐÀÈØÈàà� ¨ ¨Àèÿÿ°ÀظÐàÈÐèØèèàðø¨¸À¸ÀȰÈÐÈØàÈàðàèÿàðÿèÿÿ08((H8@H8@`@X`P`hX`€px€ @H0HXHXXPXpXhphp€h€�€ˆ�8X`@XhPhx`hxh€ˆp€˜€˜ �˜¨P`x`hxhp‡h€—Èøÿˆ˜¨�¨¨ °¸hx€xˆ‡€ˆ—ÐøÿØÿÿèÿÿ¨ÀȰÀØ€� €�ŸØÿÿàÿÿèÿÿðÿÿÿÿÿÈàà� ¨ ¨À¨·ÇøÿÿÿÿÿÿÿÿØèèàðø¨¸À¸ÀȰÈÐÈØàÿÿÿàèÿàðÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/integer_spellings/L1_count_2.0.wai b/wai/staged-conformance/cases/integer_spellings/L1_count_2.0.wai new file mode 100644 index 000000000..31f3ebe6f Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_count_2.0.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai b/wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai new file mode 100644 index 000000000..7a3efc8aa Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_from_minus0.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai b/wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai new file mode 100644 index 000000000..061a3c93c Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_layer_1.0.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/L1_layer_1e0.wai b/wai/staged-conformance/cases/integer_spellings/L1_layer_1e0.wai new file mode 100644 index 000000000..b43904190 Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_layer_1e0.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai b/wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai new file mode 100644 index 000000000..7420755ea Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_layer_minus0.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/L1_span_null.wai b/wai/staged-conformance/cases/integer_spellings/L1_span_null.wai new file mode 100644 index 000000000..d942faab0 Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/L1_span_null.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/base.wai b/wai/staged-conformance/cases/integer_spellings/base.wai new file mode 100644 index 000000000..455e453b3 Binary files /dev/null and b/wai/staged-conformance/cases/integer_spellings/base.wai differ diff --git a/wai/staged-conformance/cases/integer_spellings/case.json b/wai/staged-conformance/cases/integer_spellings/case.json new file mode 100644 index 000000000..6a59c2f85 --- /dev/null +++ b/wai/staged-conformance/cases/integer_spellings/case.json @@ -0,0 +1,232 @@ +{ + "format": "wai-staged-case/1", + "name": "integer_spellings", + "about": "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", + "bases": [ + { + "file": "base.wai", + "object": "82cdc06e05a3ed9d9856e5d22ebce9567f759adda34ce24900aff98e50cef5e9", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "27d2f728364854312c97a8d99702d522c6fc7a547ad87f3bcfc51ed687f1e21f", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_layer_minus0.wai", + "object": "a0c9b9ea94e44ba08fe9bca0f90736b2289136309388a3fc4510430ce8c227ff", + "bytes": 592 + }, + { + "file": "L1_layer_1.0.wai", + "object": "27baed2e59910aa7549f88bb9dd43002202880325a1c3d250246edd10a57e4a0", + "bytes": 593 + }, + { + "file": "L1_layer_1e0.wai", + "object": "d76f6e0fe9858408003e11cf2bc3e60a3ada91998714af03737a57476fef7220", + "bytes": 593 + }, + { + "file": "L1_from_minus0.wai", + "object": "cfa01ee27764473b22f701609110f8658e1970bc2da14593189c3d3f917268ce", + "bytes": 620 + }, + { + "file": "L1_count_2.0.wai", + "object": "ae3aa692f44a11fc832395e2455fceadf33660652d61d81bf01a973a803158db", + "bytes": 621 + }, + { + "file": "L1_span_null.wai", + "object": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "bytes": 603 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_layer_minus0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_layer_1.0.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_layer_1e0.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_from_minus0.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_count_2.0.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_span_null.wai", + "at_ms": 6, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "a0c9b9ea94e44ba08fe9bca0f90736b2289136309388a3fc4510430ce8c227ff", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "27baed2e59910aa7549f88bb9dd43002202880325a1c3d250246edd10a57e4a0", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "d76f6e0fe9858408003e11cf2bc3e60a3ada91998714af03737a57476fef7220", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "cfa01ee27764473b22f701609110f8658e1970bc2da14593189c3d3f917268ce", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ae3aa692f44a11fc832395e2455fceadf33660652d61d81bf01a973a803158db", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e6cac22a7aa29381012e329ed828f793540034477072603c147ded204e3f3b41" + }, + { + "stage": 1, + "producer": "2070d413cac0b4c5a23aac07a59702dc0d6ba19495009fbabf10cb6324160db5", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fd114942eff4b23f04c193742486e1a08aafc4d1a78ada722d0bd9ed5a770860" + } + ], + "clip_sha256": "029bb6e4dd92a6ee2f6a4a52ab01beaa2e6959b43833915d40dd808a251aad6b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/integer_spellings/stage0.bin b/wai/staged-conformance/cases/integer_spellings/stage0.bin new file mode 100644 index 000000000..5f5ade247 --- /dev/null +++ b/wai/staged-conformance/cases/integer_spellings/stage0.bin @@ -0,0 +1 @@ +(8(8888P8HPHPhP`h`hx`x�08H0HPHHhH`pX`p`xˆx€ˆ€�˜@H`H`p¨Àø`x€hx�€��ˆ�¨ˆ¨°Xhp Èÿ¸Øÿ¸ðÿˆ�¨˜˜°˜°À¨°À°àøÀàÿÈèÿÈÿÿØÿÿ˜¸¸°¸È¸ÈÐx�˜Øøÿàÿÿèÿÿ¨¸À°ÈÈÀÈàÈØàˆ °˜°¸èÿÿ°ÈÐÀÈØÀØèÐèðèèÿ °È¨ÀиÈàÈÐðØàøàèøèøÿðÿÿ(8(8@88P8HPHPhP`h`hx`xˆ88P0HPHHhHhp`hp`xˆx€ˆ€�˜@H`H`p``€`x€h€�€��ˆ�¨ˆ¨°XhpXh‡px‡p��Ððÿ�˜¨˜¨À¨°Àh€€x€—ˆˆŸÈÿÿØÿÿàÿÿ°¸È¸ÀЀ���˜ŸàÿÿèÿÿðÿÿøÿÿÿÿÿÈØàˆ °�°¸ ·ÇøÿÿÿÿÿÿÿÿÐèðèèÿ °È¨¸Ð¸ÀàÈÐèÿÿÿàèøèøÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai b/wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai new file mode 100644 index 000000000..f9d657b42 Binary files /dev/null and b/wai/staged-conformance/cases/layer_gap_on_arrival/L2a.wai differ diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai b/wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai new file mode 100644 index 000000000..7ade8c437 Binary files /dev/null and b/wai/staged-conformance/cases/layer_gap_on_arrival/L2b.wai differ diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/base.wai b/wai/staged-conformance/cases/layer_gap_on_arrival/base.wai new file mode 100644 index 000000000..deeb999aa Binary files /dev/null and b/wai/staged-conformance/cases/layer_gap_on_arrival/base.wai differ diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/case.json b/wai/staged-conformance/cases/layer_gap_on_arrival/case.json new file mode 100644 index 000000000..6b92c1a51 --- /dev/null +++ b/wai/staged-conformance/cases/layer_gap_on_arrival/case.json @@ -0,0 +1,153 @@ +{ + "format": "wai-staged-case/1", + "name": "layer_gap_on_arrival", + "about": "a bound of one held envelope: layer 2a is held for both units, and layer 2b, which would be held too, is refused on arrival (layer-gap)", + "bases": [ + { + "file": "base.wai", + "object": "499faeba7778256d684a67a4139c4e1c26ef877ce9c48733571e33602a0612fd", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "36ae3b45a0a29adc8ba7b79c826cb3157c3fb38bf2d6d9892d2e017cfd766045", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2a.wai", + "object": "b9dce755bde1b70900ede6c9842b79766414402d1cb0ecd2e5597282e293b651", + "bytes": 931 + }, + { + "file": "L2b.wai", + "object": "2cc4b08fc40bb3895e3e1155d66930478279ec1a0024b2a7dec9eb77d20fdd4b", + "bytes": 935 + } + ], + "config": { + "bound": { + "max_envelopes": 1, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2b.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b9dce755bde1b70900ede6c9842b79766414402d1cb0ecd2e5597282e293b651", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "2cc4b08fc40bb3895e3e1155d66930478279ec1a0024b2a7dec9eb77d20fdd4b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-gap" + ], + [ + 1, + "layer-gap" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "499faeba7778256d684a67a4139c4e1c26ef877ce9c48733571e33602a0612fd", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fdff7562e0820c29961cebb194f8b13abbe765c5c13996d93b5b71775ac17a19" + }, + { + "stage": 0, + "producer": "499faeba7778256d684a67a4139c4e1c26ef877ce9c48733571e33602a0612fd", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "634eb2f2925380398a26257351604c76d828420171fe7f0a5c71e3a2914c1714" + } + ], + "clip_sha256": "b3b18bbe90363440a4a88eeb7d89efe60201069bc91d67dc9c304f90e5e4cc4e", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/layer_gap_on_arrival/stage0.bin b/wai/staged-conformance/cases/layer_gap_on_arrival/stage0.bin new file mode 100644 index 000000000..f99cb8eb5 --- /dev/null +++ b/wai/staged-conformance/cases/layer_gap_on_arrival/stage0.bin @@ -0,0 +1 @@ +((0 (0(0H8HP@HP@`hX`p (8 0H0HH0HX@X`Xhp`hppxˆ(@H@@Xˆ°ÐH`pXh€`pˆp€ˆ€ˆ˜@Xh˜Àà¨Àø¨Øøp€˜€ˆ˜ˆ� ˆ˜¸ Àð¨Ðÿ¸ØÿÀàÿÈðÿˆ˜° ¨¸ ¸Àpxˆ¸àÿÀðÿØøÿ˜°° °È°¸È¸À؈�˜��°àÿÿ¨¨À¨ÀȸÈиÐèÈØð�¨¸ ¨¸°°À¸ÈÐÀÐèÐààØèðàèÿ((0 (8(0H8HP@HX@`hX`p (8 0H0HH8H`H``Xhp`hxpxˆ(@H@HX@PXHhp`h€`xˆp€ˆ€ˆ˜@XhP`h``€`x€¸àÿ€ˆ˜ˆˆ ˆ˜¸X`x`p‡px‡ÀàÿÈèÿÐøÿ ¨° ¸Àpx€p€‡ÀðÿØøÿØÿÿèÿÿøÿÿ¸À؈�˜��°˜Ÿ·èÿÿðÿÿÿÿÿ¸ÐèÈØð�¨° ¨¸°°À¸ÈÐÿÿÿÈààØèèàèÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/layer_zero/L1.wai b/wai/staged-conformance/cases/layer_zero/L1.wai new file mode 100644 index 000000000..4d7c1032b Binary files /dev/null and b/wai/staged-conformance/cases/layer_zero/L1.wai differ diff --git a/wai/staged-conformance/cases/layer_zero/base.wai b/wai/staged-conformance/cases/layer_zero/base.wai new file mode 100644 index 000000000..bcb277889 Binary files /dev/null and b/wai/staged-conformance/cases/layer_zero/base.wai differ diff --git a/wai/staged-conformance/cases/layer_zero/case.json b/wai/staged-conformance/cases/layer_zero/case.json new file mode 100644 index 000000000..9056d4d46 --- /dev/null +++ b/wai/staged-conformance/cases/layer_zero/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "layer_zero", + "about": "refines.layer 0: refines-malformed", + "bases": [ + { + "file": "base.wai", + "object": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "925dae60c8857f8ced546f9f4833e2362a814356337e4931912a5aa30404c5d4", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "8af94471bee69da28154e7586a4f71668c82d23a07d92f85c05d6eb808343cc7", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "8af94471bee69da28154e7586a4f71668c82d23a07d92f85c05d6eb808343cc7", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "2d8629ca2dec91c1ae5516b6d5dcb1c61470ef765e7074290d92ed0881c33605" + }, + { + "stage": 0, + "producer": "bfc96bd6d03e8e8586fd76b661675bbe476c936df985521197eaa0e129c8463a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "67286c7bced5bd82702cf02941771d28d539e224fa1288b9942dcbf50e63430e" + } + ], + "clip_sha256": "cab5c1a48b2d4754278d448d7a67e55ffccce8355d648010cb20e2b9bcf718b3", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/layer_zero/stage0.bin b/wai/staged-conformance/cases/layer_zero/stage0.bin new file mode 100644 index 000000000..17c267fed --- /dev/null +++ b/wai/staged-conformance/cases/layer_zero/stage0.bin @@ -0,0 +1 @@ +0(8(08(HP@H`PX`PXxXpx(080@H@@P@PhH`hXhp`xˆx€ˆ@HX@H`�ÀèX`p`xˆhx�xˆ €˜¨PX` Àè¨Èø¸Øÿxˆ €˜¨� ¨ ¨¸°Ðð¸àøÀàÿÈðÿØøÿ�¨¸ °À°ÀÈhˆ�ÀøÿÈðÿØøÿ °À°ÀȸÀÐÈÐèˆ�¨� ¸èÿÿ ¸Ð°ÈиÈàÈàðÐè𠨸¨¸È¸ÀØÀÈØÀÐèØØðèèøððÿ008(08(HP@H`PX`PXxXpx(080@H@@P@PhP`h`hx`xˆx€ˆ@HX@H`H`pX`p`xˆhx�xˆ €˜¨PX`X`p`h€px‡Àèÿ€˜ � ¨ ¨¸hpxp€€x€—ÈðÿØøÿØÿÿ °¸°ÀÈpˆ�x˜ŸÈðÿØøÿàÿÿøÿÿøÿÿÈÐè€�¨� ° §¿èÿÿøÿÿÿÿÿÈàðÐè蘨¸¨¸È¸ÀØÀÈØÿÿÿØØðàèøððÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/live_multi_base/L1_second.wai b/wai/staged-conformance/cases/live_multi_base/L1_second.wai new file mode 100644 index 000000000..44d81a7dd Binary files /dev/null and b/wai/staged-conformance/cases/live_multi_base/L1_second.wai differ diff --git a/wai/staged-conformance/cases/live_multi_base/base_0.wai b/wai/staged-conformance/cases/live_multi_base/base_0.wai new file mode 100644 index 000000000..238530e33 Binary files /dev/null and b/wai/staged-conformance/cases/live_multi_base/base_0.wai differ diff --git a/wai/staged-conformance/cases/live_multi_base/base_1.wai b/wai/staged-conformance/cases/live_multi_base/base_1.wai new file mode 100644 index 000000000..8fa080e1d Binary files /dev/null and b/wai/staged-conformance/cases/live_multi_base/base_1.wai differ diff --git a/wai/staged-conformance/cases/live_multi_base/case.json b/wai/staged-conformance/cases/live_multi_base/case.json new file mode 100644 index 000000000..194a5c5a1 --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/case.json @@ -0,0 +1,198 @@ +{ + "format": "wai-staged-case/1", + "name": "live_multi_base", + "about": "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", + "bases": [ + { + "file": "base_0.wai", + "object": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "e978b07a3d5f6210391269b8dbd31f9febb236fd4a707984b99b54d79a684083", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "9e4ee50a011204c10f0538b8c89fdd41da51c85d970a6a54d850c55c255d5486", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 2, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "192742f69c3ee15dda4e25602a57ff43de8beb06d0c49b3374b5750938b561bb", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_second.wai", + "object": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "bytes": 535 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": { + "t0_ms": 0, + "unit_ms": 100, + "target_latency_ms": 0, + "lead_ms": [ + 30 + ] + } + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "base", + "index": 1, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_second.wai", + "at_ms": 150, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [], + [ + { + "object": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "bf96922af8a85885f0a6f2f1b7ed952f1b0e42df04674d974b6b6ddcb0e1a5ae" + }, + { + "stage": 0, + "producer": "8f3a701e2f904ad90663466c2c12cafed3410eeb84ad41db82acdc8f9a10adf7", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "cbad99676ac303b94115b0ab3759eae1e77417326c49ab440d3a7455c8b773b8" + } + ], + "clip_sha256": "aebc5f02725a6b51a50ed4f8eb29793bc14548f0e5bb79069aa1338c6eada40a", + "n_units": 2 + }, + { + "units": [ + { + "stage": 1, + "producer": "8332309f88f11b520341c80edae6626e5c61ef2b332b12c6fdba4d7c79871504", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "610c949f127f349e273a3f986f0b723f1091e40d72a866dd82e8687c5d54e4dc" + }, + { + "stage": 0, + "producer": "9e4ee50a011204c10f0538b8c89fdd41da51c85d970a6a54d850c55c255d5486", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "4660d70e9c4b1b14c317c496ea606437e2299dc9cf30b36c59b4825889990b48" + } + ], + "clip_sha256": "892935ee985334efbada8760d05d321b5f64e651ddd872af614885a21f487cab", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/live_multi_base/stage0_0.bin b/wai/staged-conformance/cases/live_multi_base/stage0_0.bin new file mode 100644 index 000000000..9df24ab99 --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/stage0_0.bin @@ -0,0 +1 @@ +((@(8@88P8P`PPhP`x`px 8808P8HPHP`X`p`hp`€ˆxx�0HXHH`�ÀàXhp`xˆxxˆ€ˆ�€�¨HXp Èè¨Øø¸Øÿ€ˆ�€�¨�¨¨˜¨¸¨Èð¨àøÈàÿÈèÿÐÿÿ˜°°¨¸À°¸Øxˆ�ÀðÿØðÿàÿÿ ¨¸°¸Ð°ÈظØèˆ˜°�¨°àÿÿ¨¸È°ÈØÈÐØÐØèØðø˜¨À°°È°¸ØÀÈàÈÐðØàðàðÿðøÿ((@(8@88P8P`PPhP`x`xx(8@08P8HPHP`X`p`hp`€ˆxx�0HXHH`H`h`hphxˆxxˆ€ˆ�€�¨HXpXhp`x€px‡Èèÿ€�¨�¨¨�¨¸`hx`€€€€—ÀèÿÐÿÿØÿÿ¨¸¸¨¸Ðxˆ�x�ŸØðÿàÿÿèÿÿøÿÿøÿÿ¸Øà€˜°�¨° ¯·ðÿÿøÿÿÿÿÿÐØàØèø˜¨À°°È°¸ØÀÈàÿÿÿÐàèØðÿðøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/live_multi_base/stage0_1.bin b/wai/staged-conformance/cases/live_multi_base/stage0_1.bin new file mode 100644 index 000000000..19b1b4a99 --- /dev/null +++ b/wai/staged-conformance/cases/live_multi_base/stage0_1.bin @@ -0,0 +1 @@ +((@(8@88P8P`PPhP`x`px 8808P8HPHP`X`p`hp`€ˆxx�0HXHH`H`hXhp`xˆxxˆ€ˆ�€�¨HXpXhp`x€pxˆ€ˆ�€�¨�¨¨˜¨¸`hx`€€€ˆ˜€ˆ¨� ¨˜°°èÿÿ°¸Øxˆ�x� ��°  ° ¨¸øÿÿøÿÿÿÿÿˆ˜°�¨° °¸¨¸Èøÿÿÿÿÿÿÿÿÿÿÿ˜¨À°°È°¸ØÀÈàÈÐðÿÿÿÿÿÿÿÿÿ((@(8@88P8P`PPhP`x`xx(8@08P8HPHP`X`p`hp`€ˆxx�0HXHH`H`h`hphxˆxxˆ€ˆ�€�¨HXpXhp`x€pxˆ€ˆ�€�¨�¨¨�¨¸hhxh€€€€˜€ˆ¨ˆ  �°°¨·¿¨¸Ðxˆ�x�˜��¨˜ ° ¨¸°·Ï°Ç×·×瀘°�¨° °¸¨¸È°Ç×ÇÏ×Ï×çÿÿÿ˜¨À°°È°¸ØÀÈàÈÐè×ßïÿÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/missing_layer/L2.wai b/wai/staged-conformance/cases/missing_layer/L2.wai new file mode 100644 index 000000000..60d3eb4b3 Binary files /dev/null and b/wai/staged-conformance/cases/missing_layer/L2.wai differ diff --git a/wai/staged-conformance/cases/missing_layer/base.wai b/wai/staged-conformance/cases/missing_layer/base.wai new file mode 100644 index 000000000..dad245526 Binary files /dev/null and b/wai/staged-conformance/cases/missing_layer/base.wai differ diff --git a/wai/staged-conformance/cases/missing_layer/case.json b/wai/staged-conformance/cases/missing_layer/case.json new file mode 100644 index 000000000..132602a51 --- /dev/null +++ b/wai/staged-conformance/cases/missing_layer/case.json @@ -0,0 +1,146 @@ +{ + "format": "wai-staged-case/1", + "name": "missing_layer", + "about": "a layer 2 whose layer 1 never arrives: held for each unit, then layer-gap at the bound", + "bases": [ + { + "file": "base.wai", + "object": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "0ce93a7aa092b57a582b90756adf68bb6da7b28ac3852f9d547de506116c641b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L2.wai", + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1002 + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "58939ab250220b6de7646a0d6c73bcdc073d210d258b987782a12bac52393a0d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-gap" + ], + [ + 1, + "layer-gap" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f8a641924c3a6c023b640b14f2995330c28bd60efe7fdb19de4b1983b6fd4ef7" + }, + { + "stage": 0, + "producer": "3a7ed8febfd72da1d2e9e21b06ff332771fb99549e7134f86528cf7dfb7bb20c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "345b86e77fcaef21b81cf8ccad9556e085914009f058f85a47620f14c60ea839" + } + ], + "clip_sha256": "d34c967288ab8c16232bd8db4552445490c0e316910f5169d39e23eb8f1eff6a", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/missing_layer/stage0.bin b/wai/staged-conformance/cases/missing_layer/stage0.bin new file mode 100644 index 000000000..1402d3a82 --- /dev/null +++ b/wai/staged-conformance/cases/missing_layer/stage0.bin @@ -0,0 +1 @@ +(8(880@P8H`H``P`pXhxppˆ0HH0HX@HhH`p`hp`x€xx�x� @XXH`h Èè`pxpx�p��ˆ�¨�¨¨Xhx¨Øø¸àÿÈèÿˆ�¨�¨¨˜°°¨¸À°Øÿ¸àÿÈøÿÈÿÿèÿÿ ¨È°¸ÐÀÐØ€˜ Øðÿàÿÿàÿÿ¨ÀÈÀÐØÀÐèÐØè� ¨ °¸ðÿÿ¸ÀÈÀÈØÐàèØàøØøÿ¨¸È¨ÀظÈàÀØèàèðØðÿðøÿøÿÿ(8(8@8@P8H`H``P`pXhxppˆ0HP0HX@PhH`p`hp`x€xx�x� @XXHhhXhp`pxpx�p��ˆ�¨�¨¨Xh€`x€p€‡€ˆ—Èðÿˆ¨¨˜°°¨¸¸hx‡p€—€˜—Èÿÿèÿÿèÿÿ°¸ÐÀÈØ€˜ ��§ØÿÿàÿÿèÿÿÿÿÿÿÿÿÐØè�˜¨ °°¨·ÏøÿÿÿÿÿÿÿÿÐàøØðÿ¨¸È¨ÀظÈàÀÐèÿÿÿØðÿðøÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/out_of_order/L1.wai b/wai/staged-conformance/cases/out_of_order/L1.wai new file mode 100644 index 000000000..bfed4011a Binary files /dev/null and b/wai/staged-conformance/cases/out_of_order/L1.wai differ diff --git a/wai/staged-conformance/cases/out_of_order/L2.wai b/wai/staged-conformance/cases/out_of_order/L2.wai new file mode 100644 index 000000000..cf7ee64b3 Binary files /dev/null and b/wai/staged-conformance/cases/out_of_order/L2.wai differ diff --git a/wai/staged-conformance/cases/out_of_order/base.wai b/wai/staged-conformance/cases/out_of_order/base.wai new file mode 100644 index 000000000..fd86bf609 Binary files /dev/null and b/wai/staged-conformance/cases/out_of_order/base.wai differ diff --git a/wai/staged-conformance/cases/out_of_order/case.json b/wai/staged-conformance/cases/out_of_order/case.json new file mode 100644 index 000000000..c67d557d0 --- /dev/null +++ b/wai/staged-conformance/cases/out_of_order/case.json @@ -0,0 +1,159 @@ +{ + "format": "wai-staged-case/1", + "name": "out_of_order", + "about": "layer 2 arrives before layer 1: held, and applied when layer 1 releases it; the stages equal an in-order arrival", + "bases": [ + { + "file": "base.wai", + "object": "153ed6436775f886d442787c64fe38f1a6955bafbfae4c5df31e0482350743a3", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "3012a36a676a8c7e3b3738446ce38203c929f7f3c19630602ac2600bd15238eb", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "da0bcb8350d785db361b705889ae72b5a5af61f20d9b8303cb45367fb24c9711", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "bytes": 791 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "da0bcb8350d785db361b705889ae72b5a5af61f20d9b8303cb45367fb24c9711", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f77d9b57d38184c06026341a650573517cb6159d5f6930197939553958dccd2a" + }, + { + "stage": 2, + "producer": "7cdf23c7c5f770ce83444dc97048bfa1a6ef867b4663d15b7a5a16543cb3e3b1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "0aff5da55b5d7caef17b53f885d6eb60ea6f2551c2ecf577e7cd07704623605d" + } + ], + "clip_sha256": "23f2853e24968ceb200465105f49e760b874bec4d5bb2b92cf477ee786316ae4", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/out_of_order/stage0.bin b/wai/staged-conformance/cases/out_of_order/stage0.bin new file mode 100644 index 000000000..5f908ddee --- /dev/null +++ b/wai/staged-conformance/cases/out_of_order/stage0.bin @@ -0,0 +1 @@ +(8H(8H0PPHXhH`pPhx`xˆpˆ�0HP@PXX`p`hp`hˆhx�x� ˆ� P`pPhp¨Èÿhx�p� ˆˆ ˜ ¨ ¨¸Xp€¨Øÿ¸èÿÈèÿ� ¨˜¨¸¨°È¨ÀÐÀØÿÀðÿÐøÿØÿÿèÿÿ¨ÀȸÀÐÈÐØˆ˜ Øÿÿèÿÿðÿÿ¸ÈÐÈÐàÈØðØàð °À °Èøÿÿ¸ÀàÈÐàØèðØøøðøÿ¨ÀÐÀÈàÐÐèØØðàèÿððÿðÿÿøÿÿ(8H(8P8PPHXhP`xPhx`xˆpˆ�0HP@PXX`p`hp`pˆh€�€� ˆ� P`pPhp`h‡hx�x�˜ˆˆ � ¨  ¸Xp€`x‡pˆ—€ˆ§Øÿÿ˜ ¸¨°È¨ÀÐxx�x�—�˜§Øÿÿàÿÿèÿÿ¸ÀÐÈÐØ€˜ �Ÿ·èÿÿðÿÿÿÿÿÿÿÿÿÿÿØàè ¨À °È°·×ÿÿÿÿÿÿÿÿÿØøøèøÿ¨ÀÐÀÈàÐÐèØØðÿÿÿððÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/payload_truncated/L1.wai b/wai/staged-conformance/cases/payload_truncated/L1.wai new file mode 100644 index 000000000..49b053cc7 Binary files /dev/null and b/wai/staged-conformance/cases/payload_truncated/L1.wai differ diff --git a/wai/staged-conformance/cases/payload_truncated/base.wai b/wai/staged-conformance/cases/payload_truncated/base.wai new file mode 100644 index 000000000..8535b6bcf Binary files /dev/null and b/wai/staged-conformance/cases/payload_truncated/base.wai differ diff --git a/wai/staged-conformance/cases/payload_truncated/case.json b/wai/staged-conformance/cases/payload_truncated/case.json new file mode 100644 index 000000000..4d06fcddf --- /dev/null +++ b/wai/staged-conformance/cases/payload_truncated/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "payload_truncated", + "about": "a WIR1 payload three bytes short: payload-malformed (stream_length)", + "bases": [ + { + "file": "base.wai", + "object": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "a61d13e8c653747697bcb2456cecade400ad5537dcdd8588dacc936244eb907d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "d6ffd0949d5830a14f34c4d0b964fe060bd69021e48e836ed40a83769d3c85bd", + "bytes": 584 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "d6ffd0949d5830a14f34c4d0b964fe060bd69021e48e836ed40a83769d3c85bd", + "envelope": "payload-malformed", + "detail": "stream_length", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "bc6bf016c17a3e64d9815a4006ac9d150ad88f764aa116a2ecc71acfd7b3c5bc" + }, + { + "stage": 0, + "producer": "55bf69e46b3009f10b05573eec75d5fdab08cdc89adbbd56226f0f2c4ec057ee", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "0b99849af91786185bce80c1de17b89f1a0914aeedadea425a5c8c4048068d75" + } + ], + "clip_sha256": "627035f9387f55f17561c73517c670fe1e9a473485d5b7e3991c8aa29959146c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/payload_truncated/stage0.bin b/wai/staged-conformance/cases/payload_truncated/stage0.bin new file mode 100644 index 000000000..f394311d2 --- /dev/null +++ b/wai/staged-conformance/cases/payload_truncated/stage0.bin @@ -0,0 +1 @@ +  0 0 0808H8H`HXXHXp(0(08(@H@HX@HhH`hX`xXp€(0@0@Xˆ¨Ø@`hX`pXp€hxˆx€ @PPˆÀà Àð Èø`€ˆpˆ˜ˆˆ˜ˆ °�Àè Ðð¨Øÿ¸àÿÈèÿˆ�¨˜ ° ¨¸hh€°àÿ¸èÿÈðÿ˜ °˜°À¨ÀȰ¸Øx��€�¨Øøÿ  À¨ÀÀ¸ÀȸÐàÀÐðˆ ¨˜¨À¨°À¸ÀÈÀÈÐÐØèØèèØðø  0 0 0808H8H`HX`HXp(0(08(@H@HX@HhH`hXhxXp€(0@0@X@H`@`h``pXp€hxˆx€˜@PX@`hX`xXh€¨àÿpˆ˜ˆˆ˜ˆ °H`pXpx`x‡¸ØÿÈèÿÐðÿ˜ ° ¨¸hp€p€�¸èÿÈðÿàÿÿàÿÿèÿÿ°¸Øx��€ˆ¨�˜¯èÿÿðÿÿÿÿÿ¸ÐØÀÐðˆ ¨˜¨À¨°¸¸ÀÈÿÿÿÈØàÐèèØðø \ No newline at end of file diff --git a/wai/staged-conformance/cases/pending_l1_l2/L1.wai b/wai/staged-conformance/cases/pending_l1_l2/L1.wai new file mode 100644 index 000000000..13fb40ecb Binary files /dev/null and b/wai/staged-conformance/cases/pending_l1_l2/L1.wai differ diff --git a/wai/staged-conformance/cases/pending_l1_l2/L2.wai b/wai/staged-conformance/cases/pending_l1_l2/L2.wai new file mode 100644 index 000000000..8fbae02c1 Binary files /dev/null and b/wai/staged-conformance/cases/pending_l1_l2/L2.wai differ diff --git a/wai/staged-conformance/cases/pending_l1_l2/base.wai b/wai/staged-conformance/cases/pending_l1_l2/base.wai new file mode 100644 index 000000000..490c43041 Binary files /dev/null and b/wai/staged-conformance/cases/pending_l1_l2/base.wai differ diff --git a/wai/staged-conformance/cases/pending_l1_l2/case.json b/wai/staged-conformance/cases/pending_l1_l2/case.json new file mode 100644 index 000000000..99a894e94 --- /dev/null +++ b/wai/staged-conformance/cases/pending_l1_l2/case.json @@ -0,0 +1,166 @@ +{ + "format": "wai-staged-case/1", + "name": "pending_l1_l2", + "about": "layers 2 and 1 both arrive before their base: released together, layer 1 applies and releases layer 2, which applies", + "bases": [ + { + "file": "base.wai", + "object": "aa52fc04d7312d8291fa1f8f2be262cd11ca113d706c09d5ad81a039958816cb", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "1d646761414563e2a25db990dde6d1e285251bd25459062d3048ef36b4b04790", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "bytes": 791 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L2.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + }, + { + "object": "993e2e2e0818acb65e8a5bb9638d642e5b4e44c1ad3e253aed012b4c86dc0ffe", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e86f1440ad1a1324cbf98f42cdc76a51e1331d64c6a205d6f929ead5d9ee83cc" + }, + { + "stage": 2, + "producer": "7fae2a51d3500637907d520a1773ec17b17e34b9934a4bae08d5a417628bc13a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "501ea9070aa9944871c02386298a3304def37b26ea2076a2adc2a15c10538a84" + } + ], + "clip_sha256": "63165f2901cceb0953886df8df37c7851157b8c4c8cbb54b9fdf871f7aa0dd8c", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/pending_l1_l2/stage0.bin b/wai/staged-conformance/cases/pending_l1_l2/stage0.bin new file mode 100644 index 000000000..96f5e60ec --- /dev/null +++ b/wai/staged-conformance/cases/pending_l1_l2/stage0.bin @@ -0,0 +1 @@ + 88 8H0HXHHhHhhPhx`xˆxx�0@X@H`@`hXhp`xˆpxˆx€˜x�¨@``P`p¨Øøpxˆx€�x� ˆ ¨˜ ÀXp€¸Øÿ¸àÿÐèÿˆ ¨�¨¸¨¨À¨¸ÀÀàÿÀðÿÈøÿØÿÿèÿÿ¨°Ð°ÈظØàˆ� ØÿÿØÿÿèÿÿ°ÈØÀÈØÐØàØè𘰰 °Àøÿÿ¸ÈÐÀØèÐØøàèÿèøÿ°¸ÐÀÀÐÀØàÈàðØðøàðÿèøÿÿÿÿ 88(8H0H`HHhHhhPhx`xˆxx�0HX@HhH`hXhp`xˆx€ˆx€˜€� @``P`p`x€pxˆx€�€� ˆ˜¨˜ ÀXp€px‡p€—ˆˆŸÈÿÿ�¨¸¨¨¸¨¸Àx€�x�—€˜§Øÿÿèÿÿðÿÿ°ÈظÐàˆ� �§¯ØÿÿèÿÿðÿÿÿÿÿÿÿÿØè𘰰 °À°¿Çøÿÿÿÿÿÿÿÿàèÿèøÿ¨¸È¸ÀиØàÈàðÿÿÿàðÿèøÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/q_zero/L1.wai b/wai/staged-conformance/cases/q_zero/L1.wai new file mode 100644 index 000000000..56ed2b4c0 Binary files /dev/null and b/wai/staged-conformance/cases/q_zero/L1.wai differ diff --git a/wai/staged-conformance/cases/q_zero/base.wai b/wai/staged-conformance/cases/q_zero/base.wai new file mode 100644 index 000000000..45707720c Binary files /dev/null and b/wai/staged-conformance/cases/q_zero/base.wai differ diff --git a/wai/staged-conformance/cases/q_zero/case.json b/wai/staged-conformance/cases/q_zero/case.json new file mode 100644 index 000000000..d0eeacd01 --- /dev/null +++ b/wai/staged-conformance/cases/q_zero/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "q_zero", + "about": "a step q of 0: payload-malformed (field_range)", + "bases": [ + { + "file": "base.wai", + "object": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5896afd7ae24197e11dfeb175fa3292c3802b0838cb1247a624f3193bbfffe6c", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "ff4495d5f5594c6c9f1a247bcb864d8ea056d69561fdc6c267a13fdae0b614cc", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ff4495d5f5594c6c9f1a247bcb864d8ea056d69561fdc6c267a13fdae0b614cc", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "051d74b233e79babef8457b02d9181caba57726215e1e7dd9247a4194c082234" + }, + { + "stage": 0, + "producer": "fd60503ddd8fe70b0a449a2d0c0508fc996a6d80e587e206cdfbd82af11675a8", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "6af52f9678efda5113e9f4a23b4f06e7ebc8873860095c7a05b5ff8c531a4477" + } + ], + "clip_sha256": "e1c0b26d1993566ed46788477da676478a9bc99ca9305de3e060e4df230869ee", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/q_zero/stage0.bin b/wai/staged-conformance/cases/q_zero/stage0.bin new file mode 100644 index 000000000..8350ee3fa --- /dev/null +++ b/wai/staged-conformance/cases/q_zero/stage0.bin @@ -0,0 +1 @@ +  008(0@0HP@PP@PpX`p(8(0@(8P0HXHX`X`pXhxhx€(8P@HPˆ¸ÐPXpPpxhp€px�xˆ˜@Ph˜¸à Èð°Ðøpˆˆxˆ˜ˆ�˜�¨¨ Àè¨Øø°Øÿ¸àÿÈðÿ� ¨˜¨°¨¨Àpxˆ¸èÿÈøÿÐðÿ  ¸ ¸À¨¸Ð°ÀØx�˜€˜¨àÿÿ ¨È°ÀȸÈÐÀÐØÈØð�¨¨˜°À°°À¸ÀÐÈÈàÐÐèÐèðàèø( 008(0@0HP@PP@PpXhp08(0@08P0HXHX`X`pXpxhx€(8P@HP@XXPXpPpxhx€xxˆxˆ˜@PhPXhXhxhp€¸èÿx€˜ˆ�˜�¨¨``p`x€hx�¸àÿÀèÿØÿÿ˜¨°¨¨Àpxˆpˆ—ÈøÿÈðÿàÿÿèÿÿðÿÿ°ÀØx�˜€�¨˜Ÿ¯èÿÿøÿÿÿÿÿÀÐØÈØðˆ¨¨˜°À°°À¸ÀÐÿÿÿÐÐèÐèðØèø \ No newline at end of file diff --git a/wai/staged-conformance/cases/rate_downsampling/L1.wai b/wai/staged-conformance/cases/rate_downsampling/L1.wai new file mode 100644 index 000000000..67f67c5e4 Binary files /dev/null and b/wai/staged-conformance/cases/rate_downsampling/L1.wai differ diff --git a/wai/staged-conformance/cases/rate_downsampling/base.wai b/wai/staged-conformance/cases/rate_downsampling/base.wai new file mode 100644 index 000000000..4db6c606e Binary files /dev/null and b/wai/staged-conformance/cases/rate_downsampling/base.wai differ diff --git a/wai/staged-conformance/cases/rate_downsampling/case.json b/wai/staged-conformance/cases/rate_downsampling/case.json new file mode 100644 index 000000000..c7e6a4907 --- /dev/null +++ b/wai/staged-conformance/cases/rate_downsampling/case.json @@ -0,0 +1,100 @@ +{ + "format": "wai-staged-case/1", + "name": "rate_downsampling", + "about": "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", + "bases": [ + { + "file": "base.wai", + "object": "2499eed2f0e6b6b25873cc52c269c93962d485a5d54c860e280a86e6fd2b9248", + "decoded_via": "wai.audio.flac", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 480, + 1, + 1 + ], + "elem": "i16" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "acdf62f8c667f5a5fe32a592d610af6a1f4fb52b9d8958dcc753809f8d43fdb3", + "decode": { + "kind": "flac" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "bytes": 16931 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "bba602e3e4512eda8c140010a0b044f8ae05f4a616426c76cf249a230e0afe79", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 160, + 1, + 1 + ], + "stage_sha256": "4c867e1b112dc3efdeaea8282058d2f5dd72faea0024c89164169327ef05db86" + } + ], + "clip_sha256": "c4edba1107169547cc6876a1b46a8a7526a29cc1ddac6698a92b5db51d0a902a", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/rate_downsampling/stage0.bin b/wai/staged-conformance/cases/rate_downsampling/stage0.bin new file mode 100644 index 000000000..dfa66518b Binary files /dev/null and b/wai/staged-conformance/cases/rate_downsampling/stage0.bin differ diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/L1.wai b/wai/staged-conformance/cases/rate_geometry_mismatch/L1.wai new file mode 100644 index 000000000..45d73e202 Binary files /dev/null and b/wai/staged-conformance/cases/rate_geometry_mismatch/L1.wai differ diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/base.wai b/wai/staged-conformance/cases/rate_geometry_mismatch/base.wai new file mode 100644 index 000000000..5ee396a23 Binary files /dev/null and b/wai/staged-conformance/cases/rate_geometry_mismatch/base.wai differ diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/case.json b/wai/staged-conformance/cases/rate_geometry_mismatch/case.json new file mode 100644 index 000000000..1e1848bcf --- /dev/null +++ b/wai/staged-conformance/cases/rate_geometry_mismatch/case.json @@ -0,0 +1,103 @@ +{ + "format": "wai-staged-case/1", + "name": "rate_geometry_mismatch", + "about": "a rate layer to 600 samples reads 300; the base holds 400: geometry-mismatch", + "bases": [ + { + "file": "base.wai", + "object": "1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8", + "decoded_via": "wai.audio.flac", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 400, + 1, + 2 + ], + "elem": "i16" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "bfd17b4865c3c7a982f18a397c05e5d1df483bbb98bd29e2e9e2a672afe3063f", + "decode": { + "kind": "flac" + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "9f2578f5a3d5d2a16514a432bf8b82915537cdc4d180ee917d09a894a0c8bc7d", + "bytes": 12639 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "9f2578f5a3d5d2a16514a432bf8b82915537cdc4d180ee917d09a894a0c8bc7d", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "geometry-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "1eb764beb4a8a739054341df8b5b7d065e490032b187a93787be15f1478080a8", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 400, + 1, + 2 + ], + "stage_sha256": "bfd17b4865c3c7a982f18a397c05e5d1df483bbb98bd29e2e9e2a672afe3063f" + } + ], + "clip_sha256": "aab0d06a5d8fc29f47bd107940bd88dfd7d588980bbb72df54d5b8e9a3222006", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/rate_geometry_mismatch/stage0.bin b/wai/staged-conformance/cases/rate_geometry_mismatch/stage0.bin new file mode 100644 index 000000000..60a9a7417 Binary files /dev/null and b/wai/staged-conformance/cases/rate_geometry_mismatch/stage0.bin differ diff --git a/wai/staged-conformance/cases/record_budget/L1a_0.wai b/wai/staged-conformance/cases/record_budget/L1a_0.wai new file mode 100644 index 000000000..7dd6786df Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1a_0.wai differ diff --git a/wai/staged-conformance/cases/record_budget/L1a_1.wai b/wai/staged-conformance/cases/record_budget/L1a_1.wai new file mode 100644 index 000000000..e46898e86 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1a_1.wai differ diff --git a/wai/staged-conformance/cases/record_budget/L1a_2.wai b/wai/staged-conformance/cases/record_budget/L1a_2.wai new file mode 100644 index 000000000..548a838c9 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1a_2.wai differ diff --git a/wai/staged-conformance/cases/record_budget/L1b_0.wai b/wai/staged-conformance/cases/record_budget/L1b_0.wai new file mode 100644 index 000000000..662a88b92 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1b_0.wai differ diff --git a/wai/staged-conformance/cases/record_budget/L1b_1.wai b/wai/staged-conformance/cases/record_budget/L1b_1.wai new file mode 100644 index 000000000..5294c254a Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1b_1.wai differ diff --git a/wai/staged-conformance/cases/record_budget/L1b_2.wai b/wai/staged-conformance/cases/record_budget/L1b_2.wai new file mode 100644 index 000000000..b955b3c0d Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/L1b_2.wai differ diff --git a/wai/staged-conformance/cases/record_budget/base_0.wai b/wai/staged-conformance/cases/record_budget/base_0.wai new file mode 100644 index 000000000..772bd1791 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/base_0.wai differ diff --git a/wai/staged-conformance/cases/record_budget/base_1.wai b/wai/staged-conformance/cases/record_budget/base_1.wai new file mode 100644 index 000000000..2a66f6786 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/base_1.wai differ diff --git a/wai/staged-conformance/cases/record_budget/base_2.wai b/wai/staged-conformance/cases/record_budget/base_2.wai new file mode 100644 index 000000000..241c849d7 Binary files /dev/null and b/wai/staged-conformance/cases/record_budget/base_2.wai differ diff --git a/wai/staged-conformance/cases/record_budget/case.json b/wai/staged-conformance/cases/record_budget/case.json new file mode 100644 index 000000000..b4f2dc289 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/case.json @@ -0,0 +1,354 @@ +{ + "format": "wai-staged-case/1", + "name": "record_budget", + "about": "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + "bases": [ + { + "file": "base_0.wai", + "object": "af699aa62e0c73746f529abac02aa628ce7413349b97d6b554af7fadd81f4cbf", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "c1ac8d671c3d32f6351ca7d04ef779a735488b5c858eb835cccd5918c0286bc7", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "c92a6d59d8499b8aeabff1282319af5beb0bd96c6e8165b3765eca434c560ff2", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_2.wai", + "object": "63f8b55093fc58ee1e2b2dc28317a4fbbbbdb755c2d2e00cc38e17a854d84a09", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_2.bin", + "stage0_sha256": "f0200358dfc0a2932366db121a6555f06dafa8bcbe2c06bdd1752da5e593b5c0", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a_0.wai", + "object": "729200a3f8b525f4699768200762fc53a43c57a4a11c5b91a5adb52bec5d2300", + "bytes": 587 + }, + { + "file": "L1b_0.wai", + "object": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "bytes": 595 + }, + { + "file": "L1a_1.wai", + "object": "26e5016c1a1bb0ae25cd80e99efd9a9ba1f8e4191544024eace94276fb95b2c5", + "bytes": 587 + }, + { + "file": "L1b_1.wai", + "object": "1989c68717ad8fe42777c35dcb52f2057e3b435308f94a3a8d59eeeb1fa33f64", + "bytes": 599 + }, + { + "file": "L1a_2.wai", + "object": "5310224c7997c28cb3e77e7db220ecea13da4512a08686f6183703cbc84690a0", + "bytes": 587 + }, + { + "file": "L1b_2.wai", + "object": "5a8b7053d9489eb4ec7afecd00d51a821dbf71bccf80291ccb0e6c9d54eb84a3", + "bytes": 595 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 112, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 2, + "at_ms": 0 + }, + { + "op": "remove", + "index": 2, + "at_ms": 1 + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + }, + { + "op": "offer", + "file": "L1a_0.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1a_1.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 6 + }, + { + "op": "remove", + "index": 1, + "at_ms": 7 + }, + { + "op": "base", + "index": 0, + "at_ms": 8 + }, + { + "op": "offer", + "file": "L1b_0.wai", + "at_ms": 9, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 10 + }, + { + "op": "offer", + "file": "L1b_1.wai", + "at_ms": 11, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [], + [], + [ + { + "object": "729200a3f8b525f4699768200762fc53a43c57a4a11c5b91a5adb52bec5d2300", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "26e5016c1a1bb0ae25cd80e99efd9a9ba1f8e4191544024eace94276fb95b2c5", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [], + [], + [ + { + "object": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "1989c68717ad8fe42777c35dcb52f2057e3b435308f94a3a8d59eeeb1fa33f64", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ffc3dcceeef14fc0975399adc5138ab09f3e29cced7ec9e30cc9fdaeae0fcb34" + }, + { + "stage": 1, + "producer": "99dbfe63b5a2aed2e544b1a7318ac11e4189d8638078cef682bc8d5e0dc3e935", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b0ed3da1b8e4ec2f679b7dee723458c445c0afbbdfe9a37ceda38fb78c7e764a" + } + ], + "clip_sha256": "e46d8b6cce68ded46f9d5a32739d0b2e1edb39c9b744e6761e33e509afb9308f", + "n_units": 2 + }, + { + "units": [ + { + "stage": 0, + "producer": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "62ffa82b6ccba510e4506f4824654e7d10db31f67e218e1463bc08e09590f3ce" + }, + { + "stage": 0, + "producer": "8c13e6e22a3f7ff0a4876d6c46273000edbe70111dbf389c34a5dc5483860541", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "89b674417dc9f28e3872c925de74a142c22ff56cc12da59280de1a509307ddc0" + } + ], + "clip_sha256": "7afc585abb5a585bc805778f132bd3267b28a40b8f2adceacaa82e9e37df0861", + "n_units": 2 + }, + null + ] + } +} diff --git a/wai/staged-conformance/cases/record_budget/stage0_0.bin b/wai/staged-conformance/cases/record_budget/stage0_0.bin new file mode 100644 index 000000000..4eee05424 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_0.bin @@ -0,0 +1 @@ +((8(0@(@@8@X@P`PXp``x (@(8@(HP@HXH``X`x`xx`x�88H@H`ˆÀØP`p`xˆ`xˆx€˜€�˜HXh˜¸ð Ðè°àÿx€ˆx�˜ˆ ¨˜¨°¨Àð°Ðð¸àÿÀðÿÐÿÿˆ °¨¨¸¨ÀÈpxˆÈèÿÐøÿÐøÿ ¨° °È¨ÀȸÐàˆˆ˜� ¨Øÿÿ ¨È°ÈиÀØÐØàØè𘨸 °¸¨ÀÈ¸ÈØÈÐèÐØèààÿèøÿ((8(0@(@@8@X@PhPXp``x (@(8@0HP@HXH`hX`x`xxhx�88HHH`@``P`x`xˆhxˆx€˜€�˜HXpXXxXpph€‡Ààÿ€��ˆ ¨˜¨°``xhpxp€�ÀðÿÐÿÿÐÿÿ ¨¸¨ÀÈpxˆ€ˆ—ÐøÿÐøÿèÿÿèÿÿðÿÿ¸Ðà€ˆ˜� ¨�¯·èÿÿøÿÿÿÿÿÈØàØà𘨸 °¸¨ÀȸÈÐÿÿÿÐØàààÿàøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/record_budget/stage0_1.bin b/wai/staged-conformance/cases/record_budget/stage0_1.bin new file mode 100644 index 000000000..eddf2d511 --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_1.bin @@ -0,0 +1 @@ + (8 (8 8P8H`8XXP`pXhp 0808H0HXHHhH`hX`x`pˆpx�0HH8P`ˆÀàXhphpˆhp�€ˆ�x�¨PP` Àà Èø°Ðÿxˆ�ˆ� �˜°� ¸¨Ðø°ØøÀàÿÈèÿÐøÿ�˜¸˜¸À¨¸Èh€ˆÀðÿÈøÿØøÿ¨°À¨¸È°ÀØÀÈЀ˜¨ˆ ¸àÿÿ¨¸È°¸ÈÀÐÐÀàèÐèð�¨¸¨¨À°ÀÈÀÀØÈÐàØàèÐèðèøÿ ((8 (8(8P8H`@XXP`pXhp 0808H0HXHHhH`h``x`pˆpxˆ0HH8P`@`hXhphpˆhxˆ€ˆ�x� PP`X`hXh€hp�Àèÿˆ� �˜°� ¸`p€px€x€�ÈèÿÐøÿØøÿ˜°À¨¸Àp€ˆx��ÈøÿØøÿèÿÿðÿÿðÿÿÀÈЀ˜¨ˆ °˜Ÿ¿èÿÿøÿÿÿÿÿÀØèÈèð�¨¸¨¨À°ÀÈÀÀØÿÿÿØàèÐèðèøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/record_budget/stage0_2.bin b/wai/staged-conformance/cases/record_budget/stage0_2.bin new file mode 100644 index 000000000..8f107525c --- /dev/null +++ b/wai/staged-conformance/cases/record_budget/stage0_2.bin @@ -0,0 +1 @@ +0 0 0@8@H0PXHPXPXp`hx (80@@8HXHPXHXpXhphpˆp€ˆ8@PHPX˜¸àPhpXx€pxˆxˆ�ˆ� H`h�Èè°Èð°Øøpˆ˜€˜ ˆ ¨˜¨¸ Èð¨Øÿ¸àÿÈàÿØøÿ˜ ¸¨°È¨ÀÀxˆ�¸èÿÐðÿØÿÿ °¸ ÀȸÀÐÈÐØ€ˆ ˆ˜¨àÿÿ¨°È¨ÈȸÈàÀÐèÐØø˜¨¸ °À°ÀÐÀÐÐÈØàÈèðØðøàøÿ0(0 0@8@H0PXPPXPXp`hx (80@H8PXHPXHXpXpphpˆp€ˆ8@PHPXPXhPhpXx€px€xˆ�ˆ� P`hHhphhxhx€¸èÿ€˜ € ¨˜ ¸Xhx`x�p€�ÈàÿØøÿàÿÿ °À¨ÀÀx€�xˆŸÐðÿØÿÿàÿÿèÿÿÿÿÿÈÐØ€ˆ ˆ˜¨˜¯¯ðÿÿðÿÿÿÿÿÀÐèÐØø˜¨° °À°ÀиÐÐÿÿÿÈàðØðøàøÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refinement_without_base/L1.wai b/wai/staged-conformance/cases/refinement_without_base/L1.wai new file mode 100644 index 000000000..8665eb0e3 Binary files /dev/null and b/wai/staged-conformance/cases/refinement_without_base/L1.wai differ diff --git a/wai/staged-conformance/cases/refinement_without_base/base.wai b/wai/staged-conformance/cases/refinement_without_base/base.wai new file mode 100644 index 000000000..3adf4d542 Binary files /dev/null and b/wai/staged-conformance/cases/refinement_without_base/base.wai differ diff --git a/wai/staged-conformance/cases/refinement_without_base/case.json b/wai/staged-conformance/cases/refinement_without_base/case.json new file mode 100644 index 000000000..a87edfc79 --- /dev/null +++ b/wai/staged-conformance/cases/refinement_without_base/case.json @@ -0,0 +1,99 @@ +{ + "format": "wai-staged-case/1", + "name": "refinement_without_base", + "about": "a layer whose base never arrives: held, then base-absent at the bound", + "bases": [ + { + "file": "base.wai", + "object": "76793cb2d9d368c511d8e2188b3d1748b99308d26d218e98424ce92880c3858e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "5bcbca1cf9487c63a9004ee51ef21693320981076eb7fe6c6d195ed6df49f0a5", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1001 + } + ], + "expect": { + "reports": [ + [ + { + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "267f0623e5b06746003c95ad5df5d029dcd556ca4c89b9ad539a94c8aeaac6a9", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + null + ] + } +} diff --git a/wai/staged-conformance/cases/refinement_without_base/stage0.bin b/wai/staged-conformance/cases/refinement_without_base/stage0.bin new file mode 100644 index 000000000..403b30a73 --- /dev/null +++ b/wai/staged-conformance/cases/refinement_without_base/stage0.bin @@ -0,0 +1 @@ + 0 8@08H8@XHXXHXp`hx`pˆ08H8HP8P`H``Xpxhx€pxˆxˆ˜8PXHXp Èèhp€px€x��ˆˆ˜�˜¨Hhp°Ðø¸Øø¸èÿˆˆ˜�˜ � ¸ ¨È¨Ðÿ°àÿÈèÿÈðÿØÿÿ °È°ÀȸÀØx€ ÈðÿØÿÿèÿÿ°ÀȸÀÐÀÈØÀØà�˜¸ ¨Àèÿÿ°ÀȸÀàÈÐèØèèàèÿ¨°È¸¸È°ÈÐÈØàÐàðààøèøÿðÿÿ 0 8@0@H8@XHX`PXp`hxhpˆ08H8PP8P`H``Xpxhx€pxˆxˆ˜8PXHXpXhphp€px€xˆ�ˆˆ˜�˜ Phphp€px€xˆ—Ðèÿ�˜ � ¸˜¨È`p‡h€�ˆˆ—ÈðÿØÿÿèÿÿ°Àȸ¸Øx€˜€�¯ØÿÿèÿÿðÿÿÿÿÿÿÿÿÀØà�˜¸ ¨À ¯ÇøÿÿÿÿÿÿÿÿØèèàèÿ¨°È¸¸È°ÈÐÈØàÿÿÿààøèøÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refines_against_base/L1_span_past.wai b/wai/staged-conformance/cases/refines_against_base/L1_span_past.wai new file mode 100644 index 000000000..2e650f851 Binary files /dev/null and b/wai/staged-conformance/cases/refines_against_base/L1_span_past.wai differ diff --git a/wai/staged-conformance/cases/refines_against_base/L2_runs_past.wai b/wai/staged-conformance/cases/refines_against_base/L2_runs_past.wai new file mode 100644 index 000000000..5356d3ad7 Binary files /dev/null and b/wai/staged-conformance/cases/refines_against_base/L2_runs_past.wai differ diff --git a/wai/staged-conformance/cases/refines_against_base/base.wai b/wai/staged-conformance/cases/refines_against_base/base.wai new file mode 100644 index 000000000..9d9e0ec89 Binary files /dev/null and b/wai/staged-conformance/cases/refines_against_base/base.wai differ diff --git a/wai/staged-conformance/cases/refines_against_base/case.json b/wai/staged-conformance/cases/refines_against_base/case.json new file mode 100644 index 000000000..c03957005 --- /dev/null +++ b/wai/staged-conformance/cases/refines_against_base/case.json @@ -0,0 +1,141 @@ +{ + "format": "wai-staged-case/1", + "name": "refines_against_base", + "about": "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", + "bases": [ + { + "file": "base.wai", + "object": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f50556c9e08ba2dda6d2c916489cc5743002f312dfddea52289eb453a71248ab", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_span_past.wai", + "object": "36a67289284c64e45a2756b0738443742d4bcfad352e6578b7ab199fc195d41c", + "bytes": 619 + }, + { + "file": "L2_runs_past.wai", + "object": "4b4b6a7559c238929a00fcaae6128b076a2c4cc353a996262bfd4bec5465d12b", + "bytes": 706 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_span_past.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2_runs_past.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "36a67289284c64e45a2756b0738443742d4bcfad352e6578b7ab199fc195d41c", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "4b4b6a7559c238929a00fcaae6128b076a2c4cc353a996262bfd4bec5465d12b", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "59061f33d463b93cff6bf3545709b95b189a3fc9b445e1c251a0d2e9122eeca7" + }, + { + "stage": 0, + "producer": "fe839f7653007183b35585d43b919af2cdfa0a3c1110aef73ddd15d36a869a5a", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "41bd3a0dca28cec04468df0253c78c18e97c437bb37afd08f3148802da09ae06" + } + ], + "clip_sha256": "b1338ed308113d939238b0e97c91901908731b1c2a599bf010b688d4e30b383b", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/refines_against_base/stage0.bin b/wai/staged-conformance/cases/refines_against_base/stage0.bin new file mode 100644 index 000000000..c9cc32420 --- /dev/null +++ b/wai/staged-conformance/cases/refines_against_base/stage0.bin @@ -0,0 +1 @@ +00 0H0HH@P`HXhX`pXppp€ˆ(@H8HP@XhX`hXh€`xˆxˆ�ˆ� HXhX`h Àø`pˆpxˆ€ˆ�€�¨�¨¨Phx Øø°àÿÀèÿ€�¨ˆ¨¨˜°¸°°À¨Ðÿ¸ðÿÀøÿÈÿÿèÿÿ¨°¸¨ÀظÀØ€˜ ØðÿØÿÿàÿÿ¨°Ð¸ÀÐÈØàÈàè� ° ¸¸øÿÿ¸ÈÐÈÐàÀØèØàðàðø¨ÀÀ°¸ÐÀÐØÈØðÐèðàøÿèðÿðÿÿ00 0H0HH@P`HXhX`pXpxp€ˆ0@H@HP@XhX`hXh€`xˆxˆ�ˆˆ HXhX`hX`€`pˆpxˆ€ˆ�€�¨� ¨Xhx`x€p€‡xˆ�Èèÿˆ ¨�°¸¨°À`p‡p�—x˜—Èÿÿàÿÿðÿÿ¨ÀиÀØ€� ��§ØÿÿàÿÿðÿÿøÿÿÿÿÿÈàè� ° °¸°·¿ÿÿÿÿÿÿÿÿÿØàèàðø¨¸À°¸ÐÀÐØÈØðÿÿÿØøÿèðÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refines_malformed/L1.wai b/wai/staged-conformance/cases/refines_malformed/L1.wai new file mode 100644 index 000000000..10b07f376 Binary files /dev/null and b/wai/staged-conformance/cases/refines_malformed/L1.wai differ diff --git a/wai/staged-conformance/cases/refines_malformed/base.wai b/wai/staged-conformance/cases/refines_malformed/base.wai new file mode 100644 index 000000000..1b54634bd Binary files /dev/null and b/wai/staged-conformance/cases/refines_malformed/base.wai differ diff --git a/wai/staged-conformance/cases/refines_malformed/case.json b/wai/staged-conformance/cases/refines_malformed/case.json new file mode 100644 index 000000000..bd0403d2e --- /dev/null +++ b/wai/staged-conformance/cases/refines_malformed/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "refines_malformed", + "about": "a base hash in upper case: refines-malformed", + "bases": [ + { + "file": "base.wai", + "object": "9d483626fd2c26622a252f5240d4bb73fa2a4a5f65a642a473a3de6f24d3a1ec", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f6d9b6ff48d9193c2e7a1a06e7e2dec2a5b0c65ae0a06f4ef44c8ea2a69c51f6", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "ff938124c0a6bfc876d89ddf4566b881bae628a63a8fcfbe4c85429e28952014", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "ff938124c0a6bfc876d89ddf4566b881bae628a63a8fcfbe4c85429e28952014", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "9d483626fd2c26622a252f5240d4bb73fa2a4a5f65a642a473a3de6f24d3a1ec", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "db563c7894a1b36a0cf6cfa28290f8c78bade3aed5fc329baab8a2d671b00534" + }, + { + "stage": 0, + "producer": "9d483626fd2c26622a252f5240d4bb73fa2a4a5f65a642a473a3de6f24d3a1ec", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "f44a3f1c38a4d80b155a1ac29abce298a6f6056c697318f2cda7811be8ee761e" + } + ], + "clip_sha256": "722c73d658ad74d2cf85551ab63181cf55b83aac12da71bb5f58ae06f7323720", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/refines_malformed/stage0.bin b/wai/staged-conformance/cases/refines_malformed/stage0.bin new file mode 100644 index 000000000..c5d9283dd --- /dev/null +++ b/wai/staged-conformance/cases/refines_malformed/stage0.bin @@ -0,0 +1 @@ +(((800H0HH@H`HX`X`p`px 0H(HH@@`@XhP`hXp€px€pˆ˜@@X@Xh ¸àX`€hp€p€˜x� � ¨P`h Àè Øø°àÿx€ ˆ�¨�¨¸ ¨¸¨Øð¸ØÿÀðÿÐøÿÐÿÿ�°°¨°À°¸Øpˆ�ÐèÿØÿÿàÿÿ ¨¸¸ÀÐ°ÈØ¸Øè��¨˜ ¨àÿÿ¨¸À¸ÈØÈÐØÐØàØðø °¸°°È°ÈиØàÈÐèÐàøàðÿðÿÿ(((808H0HHHH`HX`X`p`px 0H0HH@@`@`hX`pXx€px€p€˜@@X@XhXXhX`€hp€p€�x� ˆ ¨P`hX`pXx€h€�Ààÿˆ� � ¸ ¨¸`xxpx�x��ÐøÿÐÿÿØÿÿ¨¨À°¸Ðxˆˆˆˆ§Øÿÿàÿÿèÿÿøÿÿøÿÿ¸Øà��¨˜ ¨˜¯·ðÿÿøÿÿÿÿÿÐØàØèø °¸°°È°ÈиÐàÿÿÿÐàøØðÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refines_rules/L1_one_run.wai b/wai/staged-conformance/cases/refines_rules/L1_one_run.wai new file mode 100644 index 000000000..0c4854bb5 Binary files /dev/null and b/wai/staged-conformance/cases/refines_rules/L1_one_run.wai differ diff --git a/wai/staged-conformance/cases/refines_rules/L1_other_producer.wai b/wai/staged-conformance/cases/refines_rules/L1_other_producer.wai new file mode 100644 index 000000000..a4d3c0929 Binary files /dev/null and b/wai/staged-conformance/cases/refines_rules/L1_other_producer.wai differ diff --git a/wai/staged-conformance/cases/refines_rules/L2_adjacent_runs.wai b/wai/staged-conformance/cases/refines_rules/L2_adjacent_runs.wai new file mode 100644 index 000000000..2b848b4cb Binary files /dev/null and b/wai/staged-conformance/cases/refines_rules/L2_adjacent_runs.wai differ diff --git a/wai/staged-conformance/cases/refines_rules/L2_runs_not_span.wai b/wai/staged-conformance/cases/refines_rules/L2_runs_not_span.wai new file mode 100644 index 000000000..b05d80d9e Binary files /dev/null and b/wai/staged-conformance/cases/refines_rules/L2_runs_not_span.wai differ diff --git a/wai/staged-conformance/cases/refines_rules/base.wai b/wai/staged-conformance/cases/refines_rules/base.wai new file mode 100644 index 000000000..f90bf8e9a Binary files /dev/null and b/wai/staged-conformance/cases/refines_rules/base.wai differ diff --git a/wai/staged-conformance/cases/refines_rules/case.json b/wai/staged-conformance/cases/refines_rules/case.json new file mode 100644 index 000000000..eb7df09ef --- /dev/null +++ b/wai/staged-conformance/cases/refines_rules/case.json @@ -0,0 +1,185 @@ +{ + "format": "wai-staged-case/1", + "name": "refines_rules", + "about": "§3's rules, each broken once: a layer 1 naming a producer other than the base; prev an array of one run; two adjacent runs naming one producer; runs not summing to the span's count. Each is refines-malformed", + "bases": [ + { + "file": "base.wai", + "object": "a7b0586ed2b1c194d2fb39f1f6ff3f23ba43f7ef0004177408238647685048d9", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "45257d8b233b41630e87a5757f668dbbc0e795a5ff27b151d382f6c190bded70", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_other_producer.wai", + "object": "60416d233a4b8e1e16d78c48983a23d20d4f1b7d7182e4359c976a7357fdee12", + "bytes": 587 + }, + { + "file": "L1_one_run.wai", + "object": "6e7300e38d00f04ae6daa0ca86cb41fae5befede490a5b0edd7f5474edea0c97", + "bytes": 612 + }, + { + "file": "L2_adjacent_runs.wai", + "object": "ab68d30fa941304ff18a7dcb5dc982f3476b9a540493d6692a6d8ed9930ce8b5", + "bytes": 702 + }, + { + "file": "L2_runs_not_span.wai", + "object": "c03b7827702264bc257cacc57d284060821ddc24a82d046564d5af865af30428", + "bytes": 730 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_other_producer.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_one_run.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2_adjacent_runs.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2_runs_not_span.wai", + "at_ms": 4, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "60416d233a4b8e1e16d78c48983a23d20d4f1b7d7182e4359c976a7357fdee12", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "6e7300e38d00f04ae6daa0ca86cb41fae5befede490a5b0edd7f5474edea0c97", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ab68d30fa941304ff18a7dcb5dc982f3476b9a540493d6692a6d8ed9930ce8b5", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "c03b7827702264bc257cacc57d284060821ddc24a82d046564d5af865af30428", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "a7b0586ed2b1c194d2fb39f1f6ff3f23ba43f7ef0004177408238647685048d9", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "4a85f1966012086f74dbcf753c7cfb66b4050318eb3a046941571869cb9fe002" + }, + { + "stage": 0, + "producer": "a7b0586ed2b1c194d2fb39f1f6ff3f23ba43f7ef0004177408238647685048d9", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "5a1941a65f47a17f0108893615cb684bb1b08f9cc5ec62bd5122f3144dfd6e18" + } + ], + "clip_sha256": "27b600a505e95bb2bb0097854d98a42a0c77af1641c9090046412e52dd9fe7b2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/refines_rules/stage0.bin b/wai/staged-conformance/cases/refines_rules/stage0.bin new file mode 100644 index 000000000..575b3fbe6 --- /dev/null +++ b/wai/staged-conformance/cases/refines_rules/stage0.bin @@ -0,0 +1 @@ + (0 (@0@H8HP@X`P`hX`€hx€0@H@@X@PXPXhPhp`pxh€�€ˆ�@H`P`h˜Àð`hxh€�p€˜ˆ˜˜ˆ˜ P`x Èð°Ðÿ°àÿˆ˜˜�˜   ¸˜¸È¸ØøÀèÿÈèÿØðÿØÿÿ °È°¸È°ÀØp�˜ÈøÿØÿÿèÿÿ¨ÀȨÀÐÀÐØÈààˆ˜°˜¨Àèÿÿ¨ÀØ¸ÐØÈÐèØèèàðø¨°È°¸ÈÀÈÐÈØàÐàðàèÿàðÿøÿÿ (0 (H8@P@HP@XhX`hX`€px€0@H@@X@PXPXhPhp`p€h€�€ˆ�@H`P`hP`x`hxh€�p€˜ˆ˜˜ˆ� X`x`hxhp‡h€—Èøÿ��   ¸˜¸Àpx€xˆ‡€ˆŸÐðÿÐÿÿèÿÿ°¸À°ÀØx�˜€˜§ÐÿÿèÿÿðÿÿðÿÿÿÿÿÈØàˆ˜°˜¨¸ ¯ÇðÿÿÿÿÿÿÿÿØèèàðø °È°¸ÀÀÈÐÈÐàÿÿÿàèÿàðÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/refused_on_release/L1_cut.wai b/wai/staged-conformance/cases/refused_on_release/L1_cut.wai new file mode 100644 index 000000000..2dda3a9ca Binary files /dev/null and b/wai/staged-conformance/cases/refused_on_release/L1_cut.wai differ diff --git a/wai/staged-conformance/cases/refused_on_release/L1_f1.wai b/wai/staged-conformance/cases/refused_on_release/L1_f1.wai new file mode 100644 index 000000000..817f2bf82 Binary files /dev/null and b/wai/staged-conformance/cases/refused_on_release/L1_f1.wai differ diff --git a/wai/staged-conformance/cases/refused_on_release/base.wai b/wai/staged-conformance/cases/refused_on_release/base.wai new file mode 100644 index 000000000..889017d5d Binary files /dev/null and b/wai/staged-conformance/cases/refused_on_release/base.wai differ diff --git a/wai/staged-conformance/cases/refused_on_release/case.json b/wai/staged-conformance/cases/refused_on_release/case.json new file mode 100644 index 000000000..66ea51546 --- /dev/null +++ b/wai/staged-conformance/cases/refused_on_release/case.json @@ -0,0 +1,162 @@ +{ + "format": "wai-staged-case/1", + "name": "refused_on_release", + "about": "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", + "bases": [ + { + "file": "base.wai", + "object": "c6b41b09e3cb1a9a01dd94d7029fade9766caa66db3c07c02e2d7bb9b62c0e4c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "e85c44dac2df52bf8c564248e1b8463d1515c70ff520c4446468562618ed3b75", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_cut.wai", + "object": "81bfbc10766a375e17eb2076b2497e0d9c824420f4f306d0e4dbdeb8cf7c84a6", + "bytes": 527 + }, + { + "file": "L1_f1.wai", + "object": "06f50140e7b585e912a06018f0da38027cc15d855cce8164e677d7fe318cce38", + "bytes": 535 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1_cut.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + } + ], + "expect": { + "reports": [ + [ + { + "object": "81bfbc10766a375e17eb2076b2497e0d9c824420f4f306d0e4dbdeb8cf7c84a6", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "06f50140e7b585e912a06018f0da38027cc15d855cce8164e677d7fe318cce38", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "81bfbc10766a375e17eb2076b2497e0d9c824420f4f306d0e4dbdeb8cf7c84a6", + "envelope": "payload-malformed", + "detail": "stream_length", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + }, + { + "object": "06f50140e7b585e912a06018f0da38027cc15d855cce8164e677d7fe318cce38", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "c6b41b09e3cb1a9a01dd94d7029fade9766caa66db3c07c02e2d7bb9b62c0e4c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d6a408a7e8b880e1c42039e56c4872ac053f9d12a34d00b80533bbcbda7fc00a" + }, + { + "stage": 1, + "producer": "06f50140e7b585e912a06018f0da38027cc15d855cce8164e677d7fe318cce38", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ffbc5cc8f02d219dfb255f152aa14d1ea845e45f4ebb1061482f61a515b06c72" + } + ], + "clip_sha256": "f7efabb2f5641fb662d3e5fcbcd77c1a1b0106dd91d0f42c2d7a722f455dd1c1", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/refused_on_release/stage0.bin b/wai/staged-conformance/cases/refused_on_release/stage0.bin new file mode 100644 index 000000000..d5493f423 --- /dev/null +++ b/wai/staged-conformance/cases/refused_on_release/stage0.bin @@ -0,0 +1 @@ +(0 (8(@H@@P@H`XXhX`xhp€(@H8@P8P`HX`X`xXpxx€�x€ 8H`HXp Èè`hx`€ˆx��ˆˆ ˆ °PXx Èø¨ØÿÀàÿx˜˜€ ¨�¨¸ ¸À°Ðÿ°ØÿÀðÿÈøÿàÿÿ °¸¨¸È¸ÀЀˆ�ÐøÿØÿÿàÿÿ°¸È°ÀÈÀÐØÐÐ蘠¨�¨¸èÿÿ°ÀиÈàÈàèÐàèØðÿ ¸À¨ÀÈ°ÈØÈØàØèèàðøèðÿøÿÿ00(08(@H@@P@H`XXhX`xpp€(@H8@P8P`P``X`x`pxx€ˆx€ 8H`HXpXhp`hx`€ˆxˆˆˆˆ € °PXxXh€`x‡x€—Àøÿ€ ¨�¨¸ ¸Àhp�hx�x�—Èøÿàÿÿèÿÿ¨°È¸¸Ð€ˆ�ˆ˜ŸØÿÿàÿÿøÿÿðÿÿÿÿÿÐÐ蘠¨�¨° ·ÇðÿÿÿÿÿÿÿÿÐàèØðÿ˜¸À¨ÀȰÈÐÈØàÿÿÿàðøèèÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai new file mode 100644 index 000000000..a019b0a97 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1a.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai new file mode 100644 index 000000000..d42a616c5 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_batch_frozen/L1b.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai b/wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai new file mode 100644 index 000000000..d692037b1 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_batch_frozen/base.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json b/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json new file mode 100644 index 000000000..c0631fc1a --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_batch_frozen/case.json @@ -0,0 +1,216 @@ +{ + "format": "wai-staged-case/1", + "name": "remove_readd_batch_frozen", + "about": "layers 1a and 1b, held for the base, conflict in one batch and freeze the units with nothing applied; the chain is dropped and the base decoded again: the frozen flags are part of the record, so 1a is refused", + "bases": [ + { + "file": "base.wai", + "object": "c8c6e0ce60db3e9e62e7e5c8691c3e4a12a1039d48acc7412575fd75ba5292ca", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "c3d1bad040784f0a676bbd10bd71982fd541981426ad0d3d829b1fa2863e4561", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "ba44dde2d0b94bb6e7dd3bcbf40bbc377bb83a2d41b8a4a1c3f580a31ef7d3ca", + "bytes": 587 + }, + { + "file": "L1b.wai", + "object": "9ca9c452cbbf0767646dc9c93d6ab7a1ec61f2c0b13b74877747cba3083d550e", + "bytes": 587 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 0, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 2 + }, + { + "op": "remove", + "index": 0, + "at_ms": 3 + }, + { + "op": "base", + "index": 0, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 5, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [ + { + "object": "ba44dde2d0b94bb6e7dd3bcbf40bbc377bb83a2d41b8a4a1c3f580a31ef7d3ca", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "9ca9c452cbbf0767646dc9c93d6ab7a1ec61f2c0b13b74877747cba3083d550e", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ba44dde2d0b94bb6e7dd3bcbf40bbc377bb83a2d41b8a4a1c3f580a31ef7d3ca", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + }, + { + "object": "9ca9c452cbbf0767646dc9c93d6ab7a1ec61f2c0b13b74877747cba3083d550e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [], + [], + [ + { + "object": "ba44dde2d0b94bb6e7dd3bcbf40bbc377bb83a2d41b8a4a1c3f580a31ef7d3ca", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "c8c6e0ce60db3e9e62e7e5c8691c3e4a12a1039d48acc7412575fd75ba5292ca", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "787bcad6437497c83771bb66305be5debc188ccd373fe4e3a9fd5bc9cf5af6a4" + }, + { + "stage": 0, + "producer": "c8c6e0ce60db3e9e62e7e5c8691c3e4a12a1039d48acc7412575fd75ba5292ca", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "1a71a29498a39551b0825a503e76afe9a0fa928c28725a6e5bb59a4fd192016c" + } + ], + "clip_sha256": "059c584015e4d63f5adbe512f820431e611ccb8ca31e661c66498bf5ee9047ce", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/remove_readd_batch_frozen/stage0.bin b/wai/staged-conformance/cases/remove_readd_batch_frozen/stage0.bin new file mode 100644 index 000000000..a9c8c8c01 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_batch_frozen/stage0.bin @@ -0,0 +1 @@ +  0 8 0808H8H`HP`XXp(0(0@(8H0HXHPXP`h``p`pˆ(0H0@P�°ÐH``X`pXpˆhˆ�xˆ�@Ph�°è ¸è°Ððh€�pˆ�x˜˜�¨¨˜Àè¨Øø¨àÿÀèÿÈøÿˆ˜° ¨¸ ¸Àhxˆ¸èÿÀàÿØøÿ�¨°˜¨À°ÀÀ°ÈÐx��ˆ˜ Øÿÿ ¨¸¨¸È¸ÀÐÀÈØÐØè�¨¨˜°° ¸¸°ÀظÐÐÈÐàØàøàðø  0 8 0@08H8H`HP`XXx(8(0@(8H8PXHPXP`h``x`pˆ(8H8@XHPXH``Xhp`pˆh€�xˆ�@PhHPpXXphpx°àÿpˆ�x˜˜� ¨P`p`x€`€�ÀàÿÈøÿÐøÿ˜¨¸˜¸Àh€ˆxˆ‡ÀàÿØøÿØÿÿàÿÿðÿÿ°ÈÐx��ˆ˜ �Ÿ§èÿÿðÿÿÿÿÿÀÈØÐØè� ¨˜°° °¸¨ÀØÿÿÿÀÐàØàøàðø \ No newline at end of file diff --git a/wai/staged-conformance/cases/remove_readd_diverge/L1a.wai b/wai/staged-conformance/cases/remove_readd_diverge/L1a.wai new file mode 100644 index 000000000..5139e067e Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_diverge/L1a.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_diverge/L1b.wai b/wai/staged-conformance/cases/remove_readd_diverge/L1b.wai new file mode 100644 index 000000000..fca610e16 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_diverge/L1b.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_diverge/base.wai b/wai/staged-conformance/cases/remove_readd_diverge/base.wai new file mode 100644 index 000000000..68acc818e Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_diverge/base.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_diverge/case.json b/wai/staged-conformance/cases/remove_readd_diverge/case.json new file mode 100644 index 000000000..dbe18658b --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_diverge/case.json @@ -0,0 +1,195 @@ +{ + "format": "wai-staged-case/1", + "name": "remove_readd_diverge", + "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", + "bases": [ + { + "file": "base.wai", + "object": "28280c24e00d831db61d63e95fbb8133a76b94942083496685078f4d3f8ec536", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "d1cc70b637cb23a0a6e56e037c9a41aee5cae0abebec22eea822522f4ce9084e", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", + "bytes": 591 + }, + { + "file": "L1b.wai", + "object": "04acaa2a8736e83c7a84440a246a834bd241c99367f26825b201b004e8b8fcfc", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 2 + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 4 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 5, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "04acaa2a8736e83c7a84440a246a834bd241c99367f26825b201b004e8b8fcfc", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "04acaa2a8736e83c7a84440a246a834bd241c99367f26825b201b004e8b8fcfc", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [ + { + "object": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "65683884222d31f89d1b13d5cd178158071354f1477d537c6389fe93d4343cc0" + }, + { + "stage": 1, + "producer": "4f5ffeb91bb801a82c79d43268eee3ae2ca54bcbcf1f69b5e2e844866aa060da", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ef184ab186741db82845c9d1f50d6aa4756603bd548dfe106e9892ad19cde9dc" + } + ], + "clip_sha256": "0ffaf3f861041a87d543be9f70f90d805aacfe0a072f74deb7c16ae8415f96e0", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/remove_readd_diverge/stage0.bin b/wai/staged-conformance/cases/remove_readd_diverge/stage0.bin new file mode 100644 index 000000000..e0443d7ab Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_diverge/stage0.bin differ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L1a.wai b/wai/staged-conformance/cases/remove_readd_frozen/L1a.wai new file mode 100644 index 000000000..c2adbb1ad Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_frozen/L1a.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L1b.wai b/wai/staged-conformance/cases/remove_readd_frozen/L1b.wai new file mode 100644 index 000000000..c0c3fe39d Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_frozen/L1b.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/L2.wai b/wai/staged-conformance/cases/remove_readd_frozen/L2.wai new file mode 100644 index 000000000..6004ed9db Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_frozen/L2.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/base.wai b/wai/staged-conformance/cases/remove_readd_frozen/base.wai new file mode 100644 index 000000000..67e86163f Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_frozen/base.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_frozen/case.json b/wai/staged-conformance/cases/remove_readd_frozen/case.json new file mode 100644 index 000000000..572af4465 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_frozen/case.json @@ -0,0 +1,278 @@ +{ + "format": "wai-staged-case/1", + "name": "remove_readd_frozen", + "about": "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", + "bases": [ + { + "file": "base.wai", + "object": "873f2bc3ce457ccd4638107023230b3a6174b11de6376e7fd91ce0494960b9e7", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "3c9e9d33593d40eec9c5bb6030e46b8b7d33e20ba140540cc67b7f764e802050", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1a.wai", + "object": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", + "bytes": 583 + }, + { + "file": "L1b.wai", + "object": "882c09668e4f8f0189c3c5ae75ef795fb43fc4d8589c3ec7ed752f0c76eac663", + "bytes": 587 + }, + { + "file": "L2.wai", + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "bytes": 791 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 3 + }, + { + "op": "offer", + "file": "L1b.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "base", + "index": 0, + "at_ms": 5 + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1a.wai", + "at_ms": 7, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 8, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "882c09668e4f8f0189c3c5ae75ef795fb43fc4d8589c3ec7ed752f0c76eac663", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [], + [ + { + "object": "882c09668e4f8f0189c3c5ae75ef795fb43fc4d8589c3ec7ed752f0c76eac663", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "882c09668e4f8f0189c3c5ae75ef795fb43fc4d8589c3ec7ed752f0c76eac663", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [ + { + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ], + [ + { + "object": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3abecc787ecd987c604975c1dae9d2b035905974e845d1d630120141b19124c1", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "layer-conflict" + ], + [ + 1, + "layer-conflict" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "91348dfeff7a5954435db4ced092bd7bb2a95b641b4eb91baa264d83db6547d4" + }, + { + "stage": 1, + "producer": "12ddd697eec9c58f3556d6668849548f1eb9ca035ddc31f44c0699599a903bbb", + "frozen": true, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "57d6b4afb7d33af0f3866b6b1525619ade8b944ba7873bd94d517c5a83b93acc" + } + ], + "clip_sha256": "30836e3ff57924a86260a38c7031f85283cb6a93d581b535c790ad3b6797e75f", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/remove_readd_frozen/stage0.bin b/wai/staged-conformance/cases/remove_readd_frozen/stage0.bin new file mode 100644 index 000000000..418632010 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_frozen/stage0.bin @@ -0,0 +1 @@ + 8@0@X0@`HP`Php`pp`p�x€�0PXHP`PXpXh€hx€x€�x��ˆ� HXp`hp¨Øðxx�p�˜ˆ�¨�¨¨  À`pˆ¸ØÿÀèÿÐèÿ� ¨�°¸¨°¸¨ÀÈÀàÿÈøÿÈðÿàÿÿðÿÿ¨¸Ð¸ÈÐÈØà�˜¨Øÿÿàÿÿðÿÿ¸ÐÐÀØàØØðØðð¨°À ¸ÈÿÿÿÀÐØÐØèØèèèðÿèøÿ°¸Ð¸ÈàÀÐèØàðèèøèðÿðÿÿøÿÿ 8@8@X8@`HP`Php`pp`pˆx€�0P`HP`PXpXh€hx€x€�xˆ�ˆ� HXp`hp`xxxx�p�˜ˆ�¨�¨¨  ¸`p€px‡xˆ—ˆˆ—Øÿÿ�°¸¨°¸¨ÀÈx€‡€˜—€�¯àÿÿðÿÿðÿÿ¸ÀÐÈØàˆ˜ �§·àÿÿðÿÿÿÿÿÿÿÿÿÿÿØðð °¸ ¸È·¿×ÿÿÿÿÿÿÿÿÿèèÿàøÿ°¸È¸ÀàÀÐàØàðÿÿÿèðÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/remove_readd_replay/L1.wai b/wai/staged-conformance/cases/remove_readd_replay/L1.wai new file mode 100644 index 000000000..4147ebca9 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_replay/L1.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_replay/L2.wai b/wai/staged-conformance/cases/remove_readd_replay/L2.wai new file mode 100644 index 000000000..ac7789af4 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_replay/L2.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_replay/L4.wai b/wai/staged-conformance/cases/remove_readd_replay/L4.wai new file mode 100644 index 000000000..a187fc122 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_replay/L4.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_replay/base.wai b/wai/staged-conformance/cases/remove_readd_replay/base.wai new file mode 100644 index 000000000..7f95ded97 Binary files /dev/null and b/wai/staged-conformance/cases/remove_readd_replay/base.wai differ diff --git a/wai/staged-conformance/cases/remove_readd_replay/case.json b/wai/staged-conformance/cases/remove_readd_replay/case.json new file mode 100644 index 000000000..eadebd04b --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_replay/case.json @@ -0,0 +1,234 @@ +{ + "format": "wai-staged-case/1", + "name": "remove_readd_replay", + "about": "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", + "bases": [ + { + "file": "base.wai", + "object": "b8b27bf7832cef28853f286fb136f9764d5283bc6f40c6adeecb43275248971c", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "e2b162dbeb0b85c28c386c43164fba9c3b08af92a5258a543361e44019e23fd7", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "bytes": 579 + }, + { + "file": "L2.wai", + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "bytes": 795 + }, + { + "file": "L4.wai", + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L4.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "remove", + "index": 0, + "at_ms": 4 + }, + { + "op": "base", + "index": 0, + "at_ms": 5 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 7, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [ + 0, + 1 + ], + "refused": [] + } + ], + [ + { + "object": "97faccae6d0a0cb9c9a5d0126541fee463126fd3a1b5c2026d3338a34cca48d7", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "0d4b410f0897fb01e4c69ca05a9679ce3ca43db7243335995bc71fbd3772383c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "47a7b508abab4691faaf1552d6ce426d5146b4de94ad9d7ad76a0ddf3e160029" + }, + { + "stage": 2, + "producer": "62c1b72aaee563d60027a4be9554c61de2d7035068fb5239869aed8a75485469", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "132d8b8ca6b0c1061b2d809b42de3fe64c1acd97a61ebb97ac0eadf7b8667186" + } + ], + "clip_sha256": "fd2937f18c3732ca0fa24424bfaf5feedb1fbd7e95732fe2bbc32c41b71e62d0", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/remove_readd_replay/stage0.bin b/wai/staged-conformance/cases/remove_readd_replay/stage0.bin new file mode 100644 index 000000000..7658c82b6 --- /dev/null +++ b/wai/staged-conformance/cases/remove_readd_replay/stage0.bin @@ -0,0 +1 @@ + 0H88H@HXHP`XXhXp€hpˆh€˜0@X@X`PXhXpxhx€p€ˆxˆ ˆ  HXhX`x°Ðøh€ˆxˆ˜€˜ �˜¨  ¸`xx°ØÿÀàÿÀøÿ�˜¨˜ ¸ °À°¸È¸àÿÈèÿÐðÿØøÿàÿÿ°¸À°ÈàÀÈà€�°Ðÿÿèÿÿðÿÿ°ÀØÀÈØÐàèÐèø ¨¸°ÀÀÿÿÿÀÈØÐØèÈàøàðøèðÿ¸¸È¸ÐÐÈØàÐàèØàÿèðÿðÿÿøÿÿ 0H88H@HXHX`XXhXp€hp€p€˜0@X@XhPXhXpxhx€p€ˆxˆ €˜ PXhX`xhp€p€ˆxˆ˜€˜ �˜¨  ¸hxxpx�x€—x˜ŸØøÿ˜ ¸ ¨À°¸Èp€—€ˆ—ˆ�§Øøÿàÿÿøÿÿ°ÈØÀÈà€�¨ˆŸ·èÿÿèÿÿøÿÿÿÿÿÿÿÿÐè𠨸°ÀÀ·¿Ïÿÿÿÿÿÿÿÿÿàèøèðÿ°¸È¸ÐÐÀØØÐàèÿÿÿèðÿðÿÿøÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/splat_attr_1l/L1.wai b/wai/staged-conformance/cases/splat_attr_1l/L1.wai new file mode 100644 index 000000000..87db05341 Binary files /dev/null and b/wai/staged-conformance/cases/splat_attr_1l/L1.wai differ diff --git a/wai/staged-conformance/cases/splat_attr_1l/base.wai b/wai/staged-conformance/cases/splat_attr_1l/base.wai new file mode 100644 index 000000000..76c814a8d Binary files /dev/null and b/wai/staged-conformance/cases/splat_attr_1l/base.wai differ diff --git a/wai/staged-conformance/cases/splat_attr_1l/case.json b/wai/staged-conformance/cases/splat_attr_1l/case.json new file mode 100644 index 000000000..a969e0123 --- /dev/null +++ b/wai/staged-conformance/cases/splat_attr_1l/case.json @@ -0,0 +1,103 @@ +{ + "format": "wai-staged-case/1", + "name": "splat_attr_1l", + "about": "a pinned wai.splat.int_codec base, then an identity layer over its attribute tensor", + "bases": [ + { + "file": "base.wai", + "object": "bc283dfb0eb717c129db5a1cadad67daefc788e4f6880ecf4c7ac8a2d35204d8", + "decoded_via": "wai.splat.int_codec", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 14, + 32, + 32 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "f07679de87ff7c3d4fc4890b80919ef857fb85bd4d9e4cfa16105211d580427f", + "decode": { + "kind": "int", + "set": { + "g_s.whm": "wai-rs/tests/vectors/int_splat/splat_gs.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "bytes": 5836 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "68e006b588f94e686b17ef64f96ed0a2f7073d6a2276c68c589aa83f54a56094", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 14, + 32, + 32 + ], + "stage_sha256": "3a2afaf39022a3e4fc1244f10c221bab5275e86be8ec78094dd1165dee8c40b4" + } + ], + "clip_sha256": "26681e76f0371922e180e05b31d7f8fb99644bfcebd8ce40433d3287bedacac3", + "n_units": 1 + } + ] + } +} diff --git a/wai/staged-conformance/cases/splat_attr_1l/stage0.bin b/wai/staged-conformance/cases/splat_attr_1l/stage0.bin new file mode 100644 index 000000000..e898b6688 Binary files /dev/null and b/wai/staged-conformance/cases/splat_attr_1l/stage0.bin differ diff --git a/wai/staged-conformance/cases/step1_refusals/L1_fallback.wai b/wai/staged-conformance/cases/step1_refusals/L1_fallback.wai new file mode 100644 index 000000000..abe2f3014 Binary files /dev/null and b/wai/staged-conformance/cases/step1_refusals/L1_fallback.wai differ diff --git a/wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai b/wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai new file mode 100644 index 000000000..7cdd60f5d Binary files /dev/null and b/wai/staged-conformance/cases/step1_refusals/L1_no_refines.wai differ diff --git a/wai/staged-conformance/cases/step1_refusals/base.wai b/wai/staged-conformance/cases/step1_refusals/base.wai new file mode 100644 index 000000000..cd6fedce9 Binary files /dev/null and b/wai/staged-conformance/cases/step1_refusals/base.wai differ diff --git a/wai/staged-conformance/cases/step1_refusals/case.json b/wai/staged-conformance/cases/step1_refusals/case.json new file mode 100644 index 000000000..26ac2af7d --- /dev/null +++ b/wai/staged-conformance/cases/step1_refusals/case.json @@ -0,0 +1,141 @@ +{ + "format": "wai-staged-case/1", + "name": "step1_refusals", + "about": "a refinement-only envelope with no refines (refines-malformed), and one whose manifest names a fallback (capability-not-refinement)", + "bases": [ + { + "file": "base.wai", + "object": "a7eac3bd1ff2ad18d7ede3ddf7b2b3132195add5c3dbfc8331e419a8b24b2417", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "7abb2255823c2a6ea8c92e40d241a46f43a55c86b809c7667d5a4ae29c34bc1a", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_no_refines.wai", + "object": "7fd4e54b8705fa218c420f44c2e4d05ccc6f7fb6d214c3a35fd297d89da40280", + "bytes": 421 + }, + { + "file": "L1_fallback.wai", + "object": "58160c055ecda0f778f69cfb2a57d64465dbce18e442f01770678f36a85c867b", + "bytes": 625 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_no_refines.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_fallback.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "7fd4e54b8705fa218c420f44c2e4d05ccc6f7fb6d214c3a35fd297d89da40280", + "envelope": "refines-malformed", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "58160c055ecda0f778f69cfb2a57d64465dbce18e442f01770678f36a85c867b", + "envelope": "capability-not-refinement", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "a7eac3bd1ff2ad18d7ede3ddf7b2b3132195add5c3dbfc8331e419a8b24b2417", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b44dad4def5e93b8ee0783a07fada7977508cd0702dc1040c989207320e92b52" + }, + { + "stage": 0, + "producer": "a7eac3bd1ff2ad18d7ede3ddf7b2b3132195add5c3dbfc8331e419a8b24b2417", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7d8ee019f3c739abc42ddf788e072c53d1db6a57bd445c0f95e49e5c1fd2d6fd" + } + ], + "clip_sha256": "636def3e3165e6f3ee4d812c793c5c86cb5f10763a37cf86fcf8ed472a9c3299", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/step1_refusals/stage0.bin b/wai/staged-conformance/cases/step1_refusals/stage0.bin new file mode 100644 index 000000000..18e2b89a8 --- /dev/null +++ b/wai/staged-conformance/cases/step1_refusals/stage0.bin @@ -0,0 +1 @@ +(0 08(0P8@P@X`H`hXhp`xˆ(@P(@P@H`HX```xXxxp€ˆ€ˆ˜8HP@Xh˜ÀèXhphx�pˆ�x� � ¨Xhh Èø°àø°èÿxˆ �˜¨� ¸¨°À¨Øø°ØÿÀèÿÐøÿØøÿ¨¨À Àȸ¸Ðp� ÐðÿÐÿÿØÿÿ ¸Ð¸ÀȸÈàÀØè�˜¨  °ðÿÿ¨¸Ð¸ÈàÈØØÐàøØèÿ¨°¸°ÀÈÀÐÐÀØàØØðààøàøÿèÿÿ(0(08(8P8HP@X`H`hXhp`xˆ(@P0HP@H`HX```x`xxp€ˆ€ˆ˜@HP@`hP`pXpxhx�pˆ�x� ˆ ¨XhhXh€p€€hˆ�Àèÿ�˜¨�˜¸ ¨À`x€hx�€ˆ—ÐøÿØøÿèÿÿ Àȸ¸Ðp� ˆ�§ÐÿÿØÿÿèÿÿÿÿÿøÿÿÀÐè�˜¨  °¨§¿ðÿÿÿÿÿÿÿÿÐàøØèÿ¨°¸°ÀȸÐÐÀØàÿÿÿààøàøÿèÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/stream_exhausted/L1.wai b/wai/staged-conformance/cases/stream_exhausted/L1.wai new file mode 100644 index 000000000..a7e9a5d13 Binary files /dev/null and b/wai/staged-conformance/cases/stream_exhausted/L1.wai differ diff --git a/wai/staged-conformance/cases/stream_exhausted/base.wai b/wai/staged-conformance/cases/stream_exhausted/base.wai new file mode 100644 index 000000000..461dccef4 Binary files /dev/null and b/wai/staged-conformance/cases/stream_exhausted/base.wai differ diff --git a/wai/staged-conformance/cases/stream_exhausted/case.json b/wai/staged-conformance/cases/stream_exhausted/case.json new file mode 100644 index 000000000..b443c4f9e --- /dev/null +++ b/wai/staged-conformance/cases/stream_exhausted/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "stream_exhausted", + "about": "unit 0's stream codes no symbol: it runs out at the first, payload-malformed (stream_exhausted)", + "bases": [ + { + "file": "base.wai", + "object": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "1296c291d5d09baed6c85b4d667a9fbf5ceba83b6725c78375c24e2d30ae206d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "6c774d497f78fdcb4b749f05cad21e8d0804577fece4bc2722209696f75298d7", + "bytes": 523 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6c774d497f78fdcb4b749f05cad21e8d0804577fece4bc2722209696f75298d7", + "envelope": "payload-malformed", + "detail": "stream_exhausted", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "16da7e243b54905531cea9ebd321f83e7185cde180f14067b0856ad9ee9bc5f9" + }, + { + "stage": 0, + "producer": "f26597cbf487451efbca1be1c4d12a5afbee09a48097bb62b7789cf9ccce0daf", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "49c8c17836a437bfe8a29608326b2747ff810a4701967d2e4414fdcbec7e2419" + } + ], + "clip_sha256": "eb8da278015a66532af62551560d52a3ca545a3def8e711f6322c2f6c50dcb9e", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/stream_exhausted/stage0.bin b/wai/staged-conformance/cases/stream_exhausted/stage0.bin new file mode 100644 index 000000000..b05fe6bce --- /dev/null +++ b/wai/staged-conformance/cases/stream_exhausted/stage0.bin @@ -0,0 +1 @@ +  8 88(8P8HX8XXPXhXhp 0808H0@X@H`PXhX`xXp€ppˆ0HX0PPˆ¸ØPXxXp€ppˆx€ˆ€ˆ HP`˜¸à Ðð°Ðÿxˆˆx� �� �¨° Àè°Øø°àÿÀàÿÐðÿ�˜°¨¨° ÀÈp€ˆ¸èÿÐèÿÐøÿ ¨À °È¸ÀÈÀÐØ€˜˜ˆ�°àÿÿ¨°¸¨ÀиÀàÈÐèÐàè�¨¸˜¨À¨ÀÈ°ÈØÀØàÈØðààðèðÿ( 8 88(8P8PX8XXXXhXhp 0808H0HX@H`PXhX`xXx€pxˆ0HX8PP@X`P`xXp€ppˆx€ˆ€ˆ PP`PXhXpxhp‡Ààÿx� �� �¨°X`phxxh€�ÀàÿÐèÿØøÿ ¨° ÀÈp€ˆpˆ—ÐèÿÈøÿèÿÿèÿÿÿÿÿÀÐØ€�˜ˆ�°˜¯¯ðÿÿðÿÿÿÿÿÈÐèÐàè� °˜¨¸¨ÀÈ°ÈØÿÿÿÈÐðààðèðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/stream_noncanonical/L1.wai b/wai/staged-conformance/cases/stream_noncanonical/L1.wai new file mode 100644 index 000000000..52f009e00 Binary files /dev/null and b/wai/staged-conformance/cases/stream_noncanonical/L1.wai differ diff --git a/wai/staged-conformance/cases/stream_noncanonical/base.wai b/wai/staged-conformance/cases/stream_noncanonical/base.wai new file mode 100644 index 000000000..18cfe3863 Binary files /dev/null and b/wai/staged-conformance/cases/stream_noncanonical/base.wai differ diff --git a/wai/staged-conformance/cases/stream_noncanonical/case.json b/wai/staged-conformance/cases/stream_noncanonical/case.json new file mode 100644 index 000000000..c068e3cf9 --- /dev/null +++ b/wai/staged-conformance/cases/stream_noncanonical/case.json @@ -0,0 +1,119 @@ +{ + "format": "wai-staged-case/1", + "name": "stream_noncanonical", + "about": "a word after unit 0's last symbol: payload-malformed (stream_noncanonical)", + "bases": [ + { + "file": "base.wai", + "object": "725876cb2698bed8e5803aa184a63bca10362a26476d525d414edb52cae6d927", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "63645d8bf056cccd91c41a0532a4a5282b40120444f6fc474b56f61e9b5210fb", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "d74319b6365ea0e96b669bac2bf6ecc760748da1289daa8d90448adb58fbef0c", + "bytes": 583 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "d74319b6365ea0e96b669bac2bf6ecc760748da1289daa8d90448adb58fbef0c", + "envelope": "payload-malformed", + "detail": "stream_noncanonical", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "725876cb2698bed8e5803aa184a63bca10362a26476d525d414edb52cae6d927", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "b61c1f1b840d0ab2e72b441a5a9b68979da0253a2f903fda3171367f2baa152b" + }, + { + "stage": 0, + "producer": "725876cb2698bed8e5803aa184a63bca10362a26476d525d414edb52cae6d927", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "10ab38e0133a6bf1d7910121489749f2558445ac4d5aa5ef7faf51c41160dade" + } + ], + "clip_sha256": "7b98f9d249aa3a3e5197afd3e0c537a687db9b47d67c02c050f88a8edf845903", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/stream_noncanonical/stage0.bin b/wai/staged-conformance/cases/stream_noncanonical/stage0.bin new file mode 100644 index 000000000..ac7d9773c --- /dev/null +++ b/wai/staged-conformance/cases/stream_noncanonical/stage0.bin @@ -0,0 +1 @@ + (0 (0 @H8HP@H`@`hX`x (0(8@(@P8PXPPhPhx`x€`xˆ8@P8HP�°ØHhp`h€hx€xx�€ˆ˜@X`�Àà˜Èø¨Ðÿxxˆ€ˆ˜ˆ˜ � ¨ Àð°Èø¸Øÿ¸àÿÈèÿ˜˜¨˜°¸ °Èhpˆ¸àÿÈøÿÈÿÿ� À °À°ÀÈ¸ÐØ€ˆ �˜°Øÿÿ °¸°¸Ð¸ÀÐÈÐàÈØð˜ °¨°¸ ÀÀ°ÈØÀÈØÈàèØèðàðÿ (0 (8(@H@HP@H`H`hX`x (0(8@(@P8PXPPhPhx`x€`xˆ8@P8PPHP`Hhp`h€hx€xxˆ€ˆ˜@X`H``Phx`p‡ÀØÿ€ˆ�ˆ˜ � ¨X`xhhxpx‡¸àÿÈèÿàøÿ˜°¸ °Èhpˆp€ŸÈðÿÈÿÿØÿÿàÿÿøÿÿ¸ÐØ€ˆ �˜°�§·èÿÿðÿÿÿÿÿÈÐàÈØð˜ ¨¨¨¸ ¸À¨ÈÐÿÿÿÈàèØèðàðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/unreadable_envelopes/1e400.wai b/wai/staged-conformance/cases/unreadable_envelopes/1e400.wai new file mode 100644 index 000000000..30454f5e2 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/1e400.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/base.wai b/wai/staged-conformance/cases/unreadable_envelopes/base.wai new file mode 100644 index 000000000..0c56a09ad Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/base.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/bom.wai b/wai/staged-conformance/cases/unreadable_envelopes/bom.wai new file mode 100644 index 000000000..179b62b66 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/bom.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/case.json b/wai/staged-conformance/cases/unreadable_envelopes/case.json new file mode 100644 index 000000000..f27f6f43f --- /dev/null +++ b/wai/staged-conformance/cases/unreadable_envelopes/case.json @@ -0,0 +1,248 @@ +{ + "format": "wai-staged-case/1", + "name": "unreadable_envelopes", + "about": "envelopes a reader of SPEC §2 and §3 refuses, so a sink never reaches §4.1: a member named twice (in refines, and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media; an encrypted layer; then the layer with a manifest nested 127 deep, which is read and applies", + "bases": [ + { + "file": "base.wai", + "object": "0c415118cc2f297d23dc31c11e5ecbd63b3c3dad605b715c101c4d4b9befeb92", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "4aecd199feb88f0107733bf13c8e38d76199270e5c89322a19ecc73d1553bd3d", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "dup_in_refines.wai", + "object": "0f0a75e14ff4e7bd4d557e586605ab79c24070423eac11ac82ccf4208cdfaf3b", + "bytes": 593 + }, + { + "file": "dup_top.wai", + "object": "53c771a4d9087abd78b9d2abd6b2d2c690d1e5a7413c76f1351f3db792485122", + "bytes": 597 + }, + { + "file": "nan.wai", + "object": "077eb3328f4f8bd46df6bd6c921ed8aa46a2df02acd89e3f93a050bb6d046909", + "bytes": 592 + }, + { + "file": "1e400.wai", + "object": "f71902b19339376abcbb47bd2f5e3df517a4f93d8fc8358dc4892e94a4b9b735", + "bytes": 594 + }, + { + "file": "lone_surrogate.wai", + "object": "0d7078f22f67f990b46f626de36cb2280085789166e7d02e9e21eff2ac78e748", + "bytes": 597 + }, + { + "file": "bom.wai", + "object": "c3886765584e2bdbd705b8032ca11025ae787e012ea4e1ad01682bfd9382e9b9", + "bytes": 586 + }, + { + "file": "deep_128.wai", + "object": "332f77f14b28aaf2a04a85098c5f2658ca4c00d29d1e7d8a60301eeff86869f0", + "bytes": 843 + }, + { + "file": "no_media.wai", + "object": "7e2dd695603969dc50ebf6f23c98885bc0a991aa43dd71e266ce895d702b22d0", + "bytes": 567 + }, + { + "file": "encrypted.wai", + "object": "a314b84d317cdcc745396dbf26026e98e3db24233e84b0fd6066c85a3dc3e6ca", + "bytes": 631 + }, + { + "file": "deep_127.wai", + "object": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "bytes": 841 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "dup_in_refines.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "dup_top.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "nan.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "1e400.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "lone_surrogate.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "bom.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "deep_128.wai", + "at_ms": 7, + "evidence": "none" + }, + { + "op": "offer", + "file": "no_media.wai", + "at_ms": 8, + "evidence": "none" + }, + { + "op": "offer", + "file": "encrypted.wai", + "at_ms": 9, + "evidence": "none" + }, + { + "op": "offer", + "file": "deep_127.wai", + "at_ms": 10, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "not-wai1" + }, + { + "error": "encrypted" + }, + [ + { + "object": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "beaeff60e70afe20466916ce1802b0a889be287762795f7e5ca2a500b806ff06" + }, + { + "stage": 1, + "producer": "e00515d669f9cc14511d771638c6f976689832a450f747854f5f94b6901d2056", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "e9ec935d0a73543fd7d28c4cea664e7bbddb9f13bdecb87559c7bba5ac3249c9" + } + ], + "clip_sha256": "f0b1480ddabbfb085acd12f2697ce2f033169c8e34e1c044298c03ce975573a2", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai b/wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai new file mode 100644 index 000000000..5f4fc4e77 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/deep_127.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/deep_128.wai b/wai/staged-conformance/cases/unreadable_envelopes/deep_128.wai new file mode 100644 index 000000000..902ad29a8 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/deep_128.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/dup_in_refines.wai b/wai/staged-conformance/cases/unreadable_envelopes/dup_in_refines.wai new file mode 100644 index 000000000..c9565dd13 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/dup_in_refines.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai b/wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai new file mode 100644 index 000000000..02d97e4b8 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/dup_top.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/encrypted.wai b/wai/staged-conformance/cases/unreadable_envelopes/encrypted.wai new file mode 100644 index 000000000..39306e378 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/encrypted.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai b/wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai new file mode 100644 index 000000000..a97bc8411 Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/lone_surrogate.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/nan.wai b/wai/staged-conformance/cases/unreadable_envelopes/nan.wai new file mode 100644 index 000000000..003fceaec Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/nan.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/no_media.wai b/wai/staged-conformance/cases/unreadable_envelopes/no_media.wai new file mode 100644 index 000000000..f51260dcc Binary files /dev/null and b/wai/staged-conformance/cases/unreadable_envelopes/no_media.wai differ diff --git a/wai/staged-conformance/cases/unreadable_envelopes/stage0.bin b/wai/staged-conformance/cases/unreadable_envelopes/stage0.bin new file mode 100644 index 000000000..f9c1dbcf0 --- /dev/null +++ b/wai/staged-conformance/cases/unreadable_envelopes/stage0.bin @@ -0,0 +1 @@ +0800@08P@PXHPhP`h`h€p€ˆ(8X@HXHPhH`hXh€h€€x€˜€�˜HP`H`h˜Èð`x€hx�€ˆ ˆ˜°˜ °X`x Øø°ØÿÀèÿ€˜¨�¨°�¨¸ °È¸Øÿ¸èÿÈèÿØÿÿèÿÿ°°È°ÀÐ°ÈØˆ� Øøÿàÿÿèÿÿ¨ÀÐÀÈØÀØàÐØð�¨°¨¸¸øÿÿ¸ÀØÀÐàÐÐèØàðàøÿ¨¸È¸ÀÐÀÐàÈØðÐèøèðøðøÿðÿÿ0@00@08P@PXHPhP`h`h€p€ˆ08X@HXHPhH`hXh€h€€x€˜€�˜HP`H`hPhx`€€px�€ˆ ˆ˜°˜˜°``€Xx€hx‡xˆ�Èøÿ�¨°�¨¸ °Èpx‡pˆ—€ˆŸØÿÿèÿÿðÿÿ°ÀÐ°ÈØˆ�˜�˜§ØÿÿèÿÿðÿÿÿÿÿÿÿÿÐØðˆ¨°¨¸¸°¯¿ÿÿÿÿÿÿÿÿÿØàðàøÿ¨¸À°ÀÐÀÐàÈØðÿÿÿèðøðøÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/L1.wai b/wai/staged-conformance/cases/up2_geometry_mismatch/L1.wai new file mode 100644 index 000000000..46be10803 Binary files /dev/null and b/wai/staged-conformance/cases/up2_geometry_mismatch/L1.wai differ diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/base.wai b/wai/staged-conformance/cases/up2_geometry_mismatch/base.wai new file mode 100644 index 000000000..3cab6b9ea Binary files /dev/null and b/wai/staged-conformance/cases/up2_geometry_mismatch/base.wai differ diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/case.json b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json new file mode 100644 index 000000000..063dcaf08 --- /dev/null +++ b/wai/staged-conformance/cases/up2_geometry_mismatch/case.json @@ -0,0 +1,128 @@ +{ + "format": "wai-staged-case/1", + "name": "up2_geometry_mismatch", + "about": "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", + "bases": [ + { + "file": "base.wai", + "object": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "01fb56ae4042c285da793fa207694e3e1fe9a3b79b3904ebb0fb20ac95744267", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "b07672a2d4466e54d919865cce12d8fa5d5aeca2625491f1408e47ef8945698e", + "bytes": 807 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "b07672a2d4466e54d919865cce12d8fa5d5aeca2625491f1408e47ef8945698e", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "geometry-mismatch" + ], + [ + 1, + "geometry-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "09259e3aa9a258bb6b236c91ef21fa633699fcc88651e2030f4a48e438a092bf" + }, + { + "stage": 0, + "producer": "40d319e7fe9030ae136da54e22e418583f06d4dfee7b3e15835f9ab9ab94b89e", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "fe52ce42109cdaf3aed41625a482dd31869b5125eb7a13fe85b6338fcf5a5445" + } + ], + "clip_sha256": "ef4e67d1cc2b756fb5221906fa60f561072ecc606f9b0c1f7002672814d22519", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin b/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin new file mode 100644 index 000000000..3d451e832 --- /dev/null +++ b/wai/staged-conformance/cases/up2_geometry_mismatch/stage0.bin @@ -0,0 +1 @@ + ( (88@08H8PXHXXHXp`hx8@08H0@X8P`P`h`hx`pxpxˆ0@XHPX�ÀèXpxhx€px�x��ˆˆ¨HXh ¸è Ðø°Øøxˆ�x˜ �˜¨�¨°¨Èø°Øø°ØÿÈèÿØøÿ�¨¸ °¸¨ÀÈp€�ÀðÿÐðÿØÿÿ ¸¸¨¸ÈÀÀØÈÈà�˜ � °Øÿÿ¨°È°ÀÈÀÐàÈØðÐØð˜°¸ ¸È¸¸È¸ÀØÈààÐèèØèðððÿ ( (@ 8H08H8PXPXXHXp`hx8@08H8@X8P`P`h`hx`pxpxˆ0@XHPXH`pXpxhx€px�x��ˆˆ¨PXhXXp`p€px€Àèÿx˜ �˜¨�¨°`h€hx€px—ÈèÿØøÿØÿÿ ¨¸¨ÀÈx€ˆx��ÈðÿØÿÿèÿÿèÿÿÿÿÿÈÈàˆ˜ � °�§·ðÿÿøÿÿÿÿÿÀØèÐØð˜°¸ ¸È¸¸È¸ÀØÿÿÿÐèèÐèðèðÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/L1.wai b/wai/staged-conformance/cases/video_int_hyper_keyframe/L1.wai new file mode 100644 index 000000000..65342c292 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_hyper_keyframe/L1.wai differ diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/base.wai b/wai/staged-conformance/cases/video_int_hyper_keyframe/base.wai new file mode 100644 index 000000000..1028c3ec9 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_hyper_keyframe/base.wai differ diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json new file mode 100644 index 000000000..b29de415c --- /dev/null +++ b/wai/staged-conformance/cases/video_int_hyper_keyframe/case.json @@ -0,0 +1,189 @@ +{ + "format": "wai-staged-case/1", + "name": "video_int_hyper_keyframe", + "about": "a pinned wai.video.int_hyper clip (keyframe 1), then one layer over its five frames with a table per unit", + "bases": [ + { + "file": "base.wai", + "object": "85f50eb426d09ee3a5006544e10cbba572fc5e05cb62f3992bd095d9cb3c7e8f", + "decoded_via": "wai.video.int_hyper", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": true, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "8594d78eba936c12db0b58c4ba5f50379d03e1d0f6f44e78ff6c0103ca520382", + "decode": { + "kind": "int", + "set": { + "entropy.json": "wai-rs/tests/vectors/int_hyper/tiny_entropy.json", + "g_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_g_s.bin", + "h_s.whm": "wai-rs/tests/vectors/int_hyper/tiny_h_s.bin" + } + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "bytes": 5967 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1, + 2, + 3, + 4 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "e66e51fcff1987b3f222bf7b9f72a32afb53dce35d8097b9b73db09046e6b363" + }, + { + "stage": 1, + "producer": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "eaf9b61fdbec13b60cf6140d267e78494e48204c8d6796bc9822f5a0bad91a41" + }, + { + "stage": 1, + "producer": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "df629f07ebe398f46ef9bac67c82ca1d3ed065b090d2d36e5b19bb5b6012d887" + }, + { + "stage": 1, + "producer": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "1fe352546e6a4a4a141731b7dc857a132fda28d68e08bdc1f438e4ea94f5cab6" + }, + { + "stage": 1, + "producer": "70c00dc6fde256eccf21e4c2fa6ff3f2b63b04ec5cc4b012b5b92815261387fa", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "d92fd4dcd8569a25c7137a62516f67b9e3ff08fdd4d3edf30b7aa66556ced810" + } + ], + "clip_sha256": "48ee337a6950548f9b5a1ec5db453f724bec3a9cabe41975d27d1d0f4af727cb", + "n_units": 5 + } + ] + } +} diff --git a/wai/staged-conformance/cases/video_int_hyper_keyframe/stage0.bin b/wai/staged-conformance/cases/video_int_hyper_keyframe/stage0.bin new file mode 100644 index 000000000..0e51a4724 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_hyper_keyframe/stage0.bin differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai new file mode 100644 index 000000000..26e919713 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_0.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai new file mode 100644 index 000000000..0fc522d19 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_1.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_2.wai new file mode 100644 index 000000000..99135cabd Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/base_2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json new file mode 100644 index 000000000..3197f5ac1 --- /dev/null +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/case.json @@ -0,0 +1,726 @@ +{ + "format": "wai-staged-case/1", + "name": "video_int_motion_gop_perframe", + "about": "three closed-GOP wai.video.int_motion bases of four frames; a layer 1 per frame; a layer 2 per GOP whose prev is the run of its frames' producers", + "bases": [ + { + "file": "base_0.wai", + "object": "a42481ab67a88ed0042aeaad6839f772c6da08e02efc20a7e12d085169590c22", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_0.bin", + "stage0_sha256": "b75b4aa90cdc25925d44166e60dc370ff2af585f4471f4645a596f1badc73b1d", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_1.wai", + "object": "6d14b8f14808f3ba5a92d5141bcb380ccc097288231f9d91cf36ce199cbf13f4", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_1.bin", + "stage0_sha256": "609e9e729a7750bff598dab6ed21213ac2e94642f312c5b0138ff1402070aed2", + "decode": { + "kind": "int", + "set": {} + } + }, + { + "file": "base_2.wai", + "object": "8124f82b0bd295aea2e4d7622bba87350da346ce2137c97581a15657d15d05c3", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0_2.bin", + "stage0_sha256": "fa8a086a9b865b2ca385ffa6fa7e1368178b12f5016b1f40e77db00f260a0cf9", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "g0_L1_f0.wai", + "object": "7b5438a83e6f65c47dfc76670ff630fab31c676c621c9bf7ecccd7b2e0b8f544", + "bytes": 587 + }, + { + "file": "g0_L1_f1.wai", + "object": "0fb395d52e18647ad234e753545c26683479dbb8dfd4cb78af3b9e91fb21cfd3", + "bytes": 587 + }, + { + "file": "g0_L1_f2.wai", + "object": "a25ae4bf15ec37c711dbeefe0d16720c0ef373e523bd358510634b9cff48b7d6", + "bytes": 583 + }, + { + "file": "g0_L1_f3.wai", + "object": "1ad14f1354c4b9ace3440f9e069053298a9b69c6808be3bc2c87259d55a2013a", + "bytes": 583 + }, + { + "file": "g0_L2.wai", + "object": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "bytes": 1518 + }, + { + "file": "g1_L1_f0.wai", + "object": "d7539bf3a008c747fbbad67acdb94f0afd9a266ba48c699f9e26e12c4a258d37", + "bytes": 583 + }, + { + "file": "g1_L1_f1.wai", + "object": "5311340b1e01c9502d7ad5dcf1f721fae1c08f52806e32babbb6cd049c91cb04", + "bytes": 587 + }, + { + "file": "g1_L1_f2.wai", + "object": "3b6d310f6c9495503d60b3a65533c845e2c21ccfb961663e130e53a16f8c4968", + "bytes": 591 + }, + { + "file": "g1_L1_f3.wai", + "object": "5a185bd4905e57390f8c52497bb8af932c3270b8e5d5faf8ce1b96aec355c817", + "bytes": 591 + }, + { + "file": "g1_L2.wai", + "object": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "bytes": 1542 + }, + { + "file": "g2_L1_f0.wai", + "object": "81754d656f7b5ed9c371a54619cb08fb1a27a70c571d6df7a004ae164b78534a", + "bytes": 591 + }, + { + "file": "g2_L1_f1.wai", + "object": "0ff40ff3dc8db22c2dde2ee3c282adf16946661736784b8bc22399995cf4bf29", + "bytes": 587 + }, + { + "file": "g2_L1_f2.wai", + "object": "a5efeec13377bf894a150325c794ae42901714bacb4618d3ab371f11df74525c", + "bytes": 591 + }, + { + "file": "g2_L1_f3.wai", + "object": "7a0bb314dfe1270abc6c290b1d699fd85aa1c5297360222b5cbcf8902d72949f", + "bytes": 587 + }, + { + "file": "g2_L2.wai", + "object": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "bytes": 1566 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "g0_L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g0_L1_f1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g0_L1_f2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g0_L1_f3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g0_L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "base", + "index": 1, + "at_ms": 0 + }, + { + "op": "offer", + "file": "g1_L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g1_L1_f1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g1_L1_f2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g1_L1_f3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g1_L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "base", + "index": 2, + "at_ms": 0 + }, + { + "op": "offer", + "file": "g2_L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g2_L1_f1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g2_L1_f2.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g2_L1_f3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "g2_L2.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "7b5438a83e6f65c47dfc76670ff630fab31c676c621c9bf7ecccd7b2e0b8f544", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "0fb395d52e18647ad234e753545c26683479dbb8dfd4cb78af3b9e91fb21cfd3", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "a25ae4bf15ec37c711dbeefe0d16720c0ef373e523bd358510634b9cff48b7d6", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 2 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "1ad14f1354c4b9ace3440f9e069053298a9b69c6808be3bc2c87259d55a2013a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 3 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1, + 2, + 3 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "d7539bf3a008c747fbbad67acdb94f0afd9a266ba48c699f9e26e12c4a258d37", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5311340b1e01c9502d7ad5dcf1f721fae1c08f52806e32babbb6cd049c91cb04", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "3b6d310f6c9495503d60b3a65533c845e2c21ccfb961663e130e53a16f8c4968", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 2 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5a185bd4905e57390f8c52497bb8af932c3270b8e5d5faf8ce1b96aec355c817", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 3 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1, + 2, + 3 + ], + "held": [], + "refused": [] + } + ], + [], + [ + { + "object": "81754d656f7b5ed9c371a54619cb08fb1a27a70c571d6df7a004ae164b78534a", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "0ff40ff3dc8db22c2dde2ee3c282adf16946661736784b8bc22399995cf4bf29", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "a5efeec13377bf894a150325c794ae42901714bacb4618d3ab371f11df74525c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 2 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "7a0bb314dfe1270abc6c290b1d699fd85aa1c5297360222b5cbcf8902d72949f", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 3 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1, + 2, + 3 + ], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "1d6d55c81168fb1caf731b14f1a35eafad389741eadd71687c4515b2d37b3529" + }, + { + "stage": 2, + "producer": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "3a21f4dbaecb8b67d570c9bcc1bdf39b3335a99cb77ad26d3fed1e0dfefa915a" + }, + { + "stage": 2, + "producer": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "cb8d6416bb3f969f666f101536ef557ac4e74c2d3e700983a264c71056304a6f" + }, + { + "stage": 2, + "producer": "a0e853ef2a2e2f71bdb0a5db71f980b1d7b7ae1f935f24a6c293c780e9325955", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "9a95501a0d07b49dc5a8a7cc878ddf4d985e400c746e0ea8e116760f5625ff6d" + } + ], + "clip_sha256": "4423cde72c6a314df503f5698532daa9917673ab871867b0ab5aa07c6d933694", + "n_units": 4 + }, + { + "units": [ + { + "stage": 2, + "producer": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "490dcfe9c522ba973aaee01ce4b99651bac2101e72f8664eb5661bdc01b8a85c" + }, + { + "stage": 2, + "producer": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "e961d43c7653de1ccf3cf9f7ffa44bbaf8b549275fcfe00d171707cbc2d9ac23" + }, + { + "stage": 2, + "producer": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "5c401d60fa81b044ae72e851d5443c4f5ba4f682762f0ac20f44ed384a0ca707" + }, + { + "stage": 2, + "producer": "9783e9954b7c8d9f3ec2a71906e8175a4b8c7f9cd2b513a356f448ede8a69c1b", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "319480ca2e6aa0e5ab77f9e19f9341d1f8a2edc3aa9875043b17e26a64a870c8" + } + ], + "clip_sha256": "71b0a7a362611beb304a637aaa53a09a19bc2a4c5e8ffe365291051e089c3deb", + "n_units": 4 + }, + { + "units": [ + { + "stage": 2, + "producer": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "b66651cbe1d0397ec890ae1356ceb9cab6660ebb0ea67c56b8f6d55db45768ab" + }, + { + "stage": 2, + "producer": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "b66651cbe1d0397ec890ae1356ceb9cab6660ebb0ea67c56b8f6d55db45768ab" + }, + { + "stage": 2, + "producer": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "b66651cbe1d0397ec890ae1356ceb9cab6660ebb0ea67c56b8f6d55db45768ab" + }, + { + "stage": 2, + "producer": "211e0a912b9393caa2d957ccd3b9fad5fbf5514204e9b70a89caf248bf696d7c", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "b66651cbe1d0397ec890ae1356ceb9cab6660ebb0ea67c56b8f6d55db45768ab" + } + ], + "clip_sha256": "613c31366b3112228afe87689ec4101bf8eeb400866f2c8646b476fb22b535be", + "n_units": 4 + } + ] + } +} diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f0.wai new file mode 100644 index 000000000..269678aa4 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f0.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai new file mode 100644 index 000000000..6d73e1e77 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f1.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai new file mode 100644 index 000000000..ac10aedda Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai new file mode 100644 index 000000000..f9ba211b7 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L1_f3.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai new file mode 100644 index 000000000..e012f80a0 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g0_L2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f0.wai new file mode 100644 index 000000000..b62912a4e Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f0.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f1.wai new file mode 100644 index 000000000..9d0356e3e Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f1.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f2.wai new file mode 100644 index 000000000..c797bcef9 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f3.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f3.wai new file mode 100644 index 000000000..61f714b8d Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L1_f3.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L2.wai new file mode 100644 index 000000000..7ce307ad6 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g1_L2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai new file mode 100644 index 000000000..0fa5fa01a Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f0.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai new file mode 100644 index 000000000..275cc90a4 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f1.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f2.wai new file mode 100644 index 000000000..847deda0c Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai new file mode 100644 index 000000000..951a7f150 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L1_f3.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L2.wai b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L2.wai new file mode 100644 index 000000000..8629b1128 Binary files /dev/null and b/wai/staged-conformance/cases/video_int_motion_gop_perframe/g2_L2.wai differ diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_0.bin b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_0.bin new file mode 100644 index 000000000..d815a11e9 --- /dev/null +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_0.bin @@ -0,0 +1 @@ +0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbЍÚDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX€žÐŠ²ÚŠ²äž¼ä”Æî¨Æî¨ÐîlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbŠ¨ÐŠ¼Ú”¼ä”Æîž¼î¨Æî¨Ðø²Ðÿ²Ðÿl€ŠvŠŠv””Š”žŠ”¨Šž¨NNb”¼ÚŠ¼î”¼ø¨Æî¨Úø¨Ðÿ²Úø²äÿ¼îÿ€””ŠŠ”€”¨Š”²”ž¼”¨²XblžÆä¨ÐîžÐø¨Úø¨äÿ¼Úÿ¼ÚÿÆîÿÆîÿŠ”¨Š”ž”ž¼”¨²ž²Æž²¼žÐø¨Ðî¨Úÿ²Úÿ²äÿ²äÿ¼äÿ¼øÿÐøÿÐøÿÐÿÿ”¨¨”²¼ž²Æž²Æ²ÆÆbvЍÚÿ¼Úÿ¼äÿÆäÿ¼îÿÐîÿÚøÿÚøÿÚÿÿž²¼ž¨²¨²¼²²Æ¨ÆÐ²ÆÚvvвÚÿ²äÿ¼îÿÐîÿÆøÿÐÿÿÚøÿÚÿÿäÿÿž²¼¨¼Æ¼¼Ð²¼Ð¼ÐÚ¼Æä€Š”ÆîÿÐîÿÐîÿÐøÿÐÿÿÚÿÿîÿÿîÿÿîÿÿ²¼Æ²ÆÆ²ÐÐÆÚÚÆÚîÐÚ䀔”€žžÚøÿÚøÿÚÿÿÚÿÿäÿÿîÿÿøÿÿ²¼Ð¼¼Ð¼ÆäÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼ž¨Æäÿÿ²¼Æ²¼Æ²ÆÚ¼ÐÚÆÆäÆÚîÐÚäÚÚøÚäøäøÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐ¼ÆÐ²ÆäÆÚÚÆÚäÚÚîÚäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÐääÚäøÚîøäîÿîøÿøøÿîÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDNb:XlNXlNbv¨ÆîbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblŠÆîž¼î¨Æî¨Ðø²Ðÿ²Ðÿ²äÿvŠŠv””Š”žŠ”¨Š”¨NNbNXbDXv”¼î¨Æî¨Úø¨Ðÿ²Úø²äÿ¼îÿÆîÿÐîÿ€”¨Š”²”ž²”¨²XblXllblvžÐøžÚø¨äÿ¼Úÿ¼ÚÿÆîÿÆîÿÐîÿÐøÿ”ž²”ž²ž²Æž²¼Xl€blvbv‡¨Úÿ²Úÿ²äÿ¼äÿ¼øÿÆøÿÐøÿÐÿÿÚÿÿ”¨¼ž²Æž²Æ²ÆÆbvŠbv‡¼Úÿ¼äÿÆäÿ¼äÿÐîÿÚîÿÚøÿÚÿÿäÿÿäÿÿîÿÿ²²Æ¨ÆÐ²ÆÚvvŠl€‘lŠ‘¼îÿÐîÿÆîÿÐÿÿÚøÿÚÿÿäÿÿäÿÿîÿÿ²¼Ð²¼Ð¼ÐÚ¼Æä€Š”€”›Š”›ÐîÿÐøÿÐÿÿÚÿÿîÿÿîÿÿîÿÿøÿÿøÿÿ²ÐÐÆÚÚÆÚîÆÐ䀔”€žžŠ”¥ÚøÿÚÿÿÚÿÿäÿÿîÿÿøÿÿøÿÿÿÿÿÿÿÿÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼äÿÿäÿÿøÿÿøÿÿøÿÿÿÿÿÿÿÿÆÚîÐÚäÚÚîÚäøäîÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¼ÆÐøÿÿ¼ÚÚÆÚäÐÚîÐäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÆÚîÐääÐÚøÐîøäîÿîøÿøøÿîÿÿ0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX:DXDXbDXlXblNbvblvbvvlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbDNXDXbNblDbvXbvblvbl€lv‡²Ðÿl€‡vŠŠv””Š”žŠ”¨Šž¨NNbNXbDXvNbvbbvbv€¨Ðÿ²Úø²äÿ¼îÿÆîÿÆîÿÆøÿŠ”²”ž¼”¨²XblXllblvXl€Xv€¨äÿ¼Úÿ¼ÚÿÆîÿÆîÿÐîÿÐøÿÚÿÿÚÿÿž²Æž²¼Xl€blvbv‡bv‘l€‘²äÿ¼äÿ¼øÿÐøÿÐøÿÐÿÿÚÿÿÚÿÿäÿÿž²Æ²ÆÆbvŠbv‡vv‘v€‘€Š‘¼îÿÐîÿÚøÿÚøÿÚÿÿäÿÿäÿÿäÿÿøÿÿ¨ÆÐ²ÆÚvvŠl€‘lŠ‘v”‘ÐîÿÆøÿÐÿÿÚøÿÚÿÿäÿÿäÿÿîÿÿÿÿÿøÿÿÿÿÿ¼Æä€Š”€”›Š”›Š”¥Š”¯ÐÿÿÚÿÿîÿÿîÿÿîÿÿøÿÿøÿÿøÿÿÿÿÿÆÚîÐÚ䀔”€žž””¥”ž¥”¥¹ÚÿÿäÿÿîÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÐäøÚä²Š¨²”¨²”¨¼ž¥ÃäÿÿøÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÚäøäøÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐÿÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÿÿÿÚäøÚîøäîÿîøÿøøÿîÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDNb:XlNXlNbvblvbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblNbvXbvblvbv€lv‡lv‘l€‘vŠŠv””Š”žŠ”¨Š”¨NNbNXbDXvNb€blvbv€bl‡lv€l€›vŠ›€”‘Ðîÿ€”¥Š”²”ž²”¨²XblXllblvXv€bv€b€‘vv‡v€‘ÆîÿÆîÿÐîÿÐøÿÚÿÿÚÿÿäÿÿž²¼Xl€blvbv‡lv‘l€‘l€›vŠ›¼øÿÆøÿÐøÿÐÿÿÚÿÿÚÿÿäÿÿäÿÿøÿÿbvŠbv‡vv‘v€‘€Š›vБД›ÚîÿÚøÿÚÿÿäÿÿäÿÿîÿÿøÿÿîÿÿøÿÿvvŠl€‘lŠ‘v”‘ŠŠ¥€”¥Š¥¯ÚøÿÚÿÿäÿÿäÿÿîÿÿÿÿÿøÿÿÿÿÿÿÿÿ€Š”€”›Š”›Š”¥Šž¯Š›¯Úÿÿîÿÿîÿÿîÿÿøÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿ€””€žžŠ”¥”ž¥”¥¹”¯¹ž¯ÃîÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž²Š¨²”¨²”¨¼ž¥Ãž¹Í²¹ÍøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¹ÃÍøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_1.bin b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_1.bin new file mode 100644 index 000000000..e380770f3 --- /dev/null +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_1.bin @@ -0,0 +1 @@ +0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX:DXDNb:XlNXlNbvblvbvvlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbDNXDXbNblDbvXbvblvbl€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Šž¨NNbNXbDXvNbvbbvbv€blŠlv€l€”vŠ”€””ŠŠ”€”¨Š”²”ž¼”¨²XblXllblvXl€Xv€b€ŠvvŠv€”€”ž€”¨Š”¨Š”ž”ž¼Úÿÿž²Æž²¼Xl€blvbvŠbv”l€”l€žvŠžv”žŠžžŠž¨ÐÿÿÚÿÿÚÿÿäÿÿäÿÿøÿÿbvŠbvŠvv”v€”€Š”vДДž””¨””²ÚÿÿäÿÿäÿÿîÿÿøÿÿîÿÿøÿÿvvŠl€”lŠ”v””Š”ž€”¨Š¨¨”ž²”¨¼äÿÿäÿÿîÿÿÿÿÿøÿÿÿÿÿÿÿÿ€Š”€”žŠ”žŠ”¨Šž²Šž²”¨²¨²¼¨²¼îÿÿøÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿ€””€žž””¨”ž¨”¨¼”²¼ž²Æ¨²¼øÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž²Š¨²”¨²”¨¼ž¨Æž²Ð²¼Æ²¼Æ²ÆÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐ¼ÆÐ²ÆäÆÚÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDXbDXlXblNbvblvbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblNbvXbvblvbv€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Š”¨NNbNXbDXvNb€blvbv€blŠl€€l€”vŠ”€””€Š”€”¨Š”²”ž²”¨²XblXllblvXv€bv€b€ŠvvŠv€”€”ž€ŠžŠ”¨Š”ž”ž²”¥¯ž²Æž²¼Xl€blvbvŠlvŠl€”l€”vŠžv”ž€žžŠžžŠ¥¹”¥¯”¯Ãž¯Ãž¹ÃøÿÿbvŠbvŠvv”v€”€Š”vДДž””¨””²”›¯ž¯¹ž¥¹äÿÿøÿÿîÿÿøÿÿvvŠl€”lŠ”v””ŠŠž€”¨Šž¨”ž²”¨¼ž¯Ãž¹Ãîÿÿÿÿÿøÿÿÿÿÿÿÿÿ€Š”€””Š”žŠ”¨Š”²Šž²”¨²¨¨¼¨²¼¨¹Ã²¹Ãøÿÿøÿÿÿÿÿÿÿÿÿÿÿ€””€žžŠ”¨”ž¨”¨¼”²²ž²Æ¨²¼²¹Í²¹×¹ÃÍÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž²Š¨²”¨²”¨¼ž¨Æž²Æ²¼Æ²¼Æ²ÆÚ¹ÍáÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¼ÆÐ²ÆÚ¼ÚÚÃáá××õÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÆÚîÍáë×áõÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX:DXDNb:XlNXlNbvblvbvvlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbDNXDXbNblDbvXbvblvbl€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Šž¨NNbNXbDXvNbvbbvbv€blŠlv€l€”vŠ”€””ŠŠ”€”¨Š”²”ž¼”¨²XblXllblvXl€Xv€b€ŠvvŠv€”€”ž€”¨Š”¨Š”ž”ž¼”¥¯ž²Æž²¼Xl€blvbvŠbv”l€”l€žvŠžv”žŠžžŠž¨Š¥¹”¥¯”¯Ãž¯Ãž¹Í²ÃÃbvŠbvŠvv”v€”€Š”vДДž””¨””²”›¯ž¯¹ž¯¹ž¯¹²¹Ã¨ÃͲÍávvŠl€”lŠ”v””Š”ž€”¨Š¨¨”ž²”¨¼ž¯Ãž¹Ã¨Ãù¹Í²Ã×ÿÿÿÿÿÿ€Š”€”žŠ”žŠ”¨Šž²Šž²”¨²¨²¼¨²¼¨¹Ã²¹Í²ÃͲÍÍÿÿÿÿÿÿÿÿÿ€””€žž””¨”ž¨”¨¼”²¼ž²Æ¨²¼²¹Í²Ã×¹Ã×¹ÃáÍÍáÿÿÿÿÿÿÿÿÿŠž²Š¨²”¨²”¨¼ž¨Æž²Ð²¼Æ²¼Æ²ÆÚ¹ÍáÍÍáÃ×ëÍ×ëÿÿÿÿÿÿÿÿÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐ¼ÆÐ²ÆäÆÚÚÃáë××õ×ëëÿÿÿÿÿÿÿÿÿÿÿÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÍáë×áÿ×ëõëõÿÿÿÿÿÿÿÿÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDXbDXlXblNbvblvbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblNbvXbvblvbv€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Š”¨NNbNXbDXvNb€blvbv€blŠl€€l€”vŠ”€””€Š”€”¨Š”²”ž²”¨²XblXllblvXv€bv€b€ŠvvŠv€”€”ž€ŠžŠ”¨Š”ž”ž²”¥¯ž²Æž²¼Xl€blvbvŠlvŠl€”l€”vŠžv”ž€žžŠžžŠ¥¹”¥¯”¯Ãž¯Ãž¹Ã²ÃÍbvŠbvŠvv”v€”€Š”vДДž””¨””²”›¯ž¯¹ž¥¹¨¯¹²¹Ã¨ÃͲÍávvŠl€”lŠ”v””ŠŠž€”¨Šž¨”ž²”¨¼ž¯Ãž¹Ã¨Ã͹¹Í²Ã×¹Í×ÃÍည”€””Š”žŠ”¨Š”²Šž²”¨²¨¨¼¨²¼¨¹Ã²¹Ã²ÃͲÍÍÃ××Ã×ëÍ×န”€žžŠ”¨”ž¨”¨¼”²²ž²Æ¨²¼²¹Í²¹×¹Ã͹ÃáÍ×áÃáëÍáõÿÿÿŠž²Š¨²”¨²”¨¼ž¨Æž²Æ²¼Æ²¼Æ²ÆÚ¹ÍáÍÍáÃ×ëÍ×ë×áõ×áÿÿÿÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¼ÆÐ²ÆÚ¼ÚÚÃáá××õ×ëõÍëõ×õÿëõÿÿÿÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÆÚîÍáë×áõ×ëÿëõÿëõÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_2.bin b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_2.bin new file mode 100644 index 000000000..31301c37b --- /dev/null +++ b/wai/staged-conformance/cases/video_int_motion_gop_perframe/stage0_2.bin @@ -0,0 +1 @@ +0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX:DXDNb:XlNXlNbvblvbvvlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbDNXDXbNblDbvXbvblvbl€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Šž¨NNbNXbDXvNbvbbvbv€blŠlv€l€”vŠ”€””ŠŠ”€”¨Š”²”ž¼”¨²XblXllblvXl€Xv€b€ŠvvŠv€”€”ž€”¨Š”¨Š”ž”ž¼”¨²ž²Æž²¼Xl€blvbvŠbv”l€”l€žvŠžv”žŠžžŠž¨Š¨¼”¨¨”²¼ž²Æž²Æ²ÆÆbvŠbvŠvv”v€”€Š”vДДž””¨””²”ž²ž²¼ž¨²¨²¼²²Æ¨ÆÐ²ÆÚvvŠl€”lŠ”v””Š”ž€”¨Š¨¨”ž²”¨¼ž²¼ž²¼¨¼Æ¼¼Ð²¼Ð¼ÐÚ¼Æä€Š”€”žŠ”žŠ”¨Šž²Šž²”¨²¨²¼¨²¼¨²Æ²¼Æ²ÆÆ²ÐÐÆÚÚÆÚîÐÚ䀔”€žž””¨”ž¨”¨¼”²¼ž²Æ¨²¼²²Ð²¼Ð¼¼Ð¼ÆäÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼ž¨Æž²Ð²¼Æ²¼Æ²ÆÚ¼ÐÚÆÆäÆÚîÐÚäÚÚøÚäøäøÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐ¼ÆÐ²ÆäÆÚÚÆÚäÚÚîÚäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÐääÚäøÚîøäîÿîøÿøøÿîÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDXbDXlXblNbvblvbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblNbvXbvblvbv€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Š”¨NNbNXbDXvNb€blvbv€blŠl€€l€”vŠ”€””€Š”€”¨Š”²”ž²”¨²XblXllblvXv€bv€b€ŠvvŠv€”€”ž€ŠžŠ”¨Š”ž”ž²”ž²ž²Æž²¼Xl€blvbvŠlvŠl€”l€”vŠžv”ž€žžŠžžŠ¨²”ž¨”¨¼ž²Æž²Æ²ÆÆbvŠbvŠvv”v€”€Š”vДДž””¨””²”ž²ž²¼ž¨²ž²¼²²Æ¨ÆÐ²ÆÚvvŠl€”lŠ”v””ŠŠž€”¨Šž¨”ž²”¨¼ž¨¼ž²¼¨¼Æ²¼Ð²¼Ð¼ÐÚ¼Æä€Š”€””Š”žŠ”¨Š”²Šž²”¨²¨¨¼¨²¼¨²Æ²¼Æ²ÆÆ²ÐÐÆÚÚÆÚîÆÐ䀔”€žžŠ”¨”ž¨”¨¼”²²ž²Æ¨²¼²²Æ²¼Ð¼¼Ð¼ÆäÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼ž¨Æž²Æ²¼Æ²¼Æ²ÆÚ¼ÐÚÆÆäÆÚîÐÚäÚÚîÚäøäîÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¼ÆÐ²ÆÚ¼ÚÚÆÚäÐÚîÐäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÆÚîÐääÐÚøÐîøäîÿîøÿøøÿîÿÿ0&&:00&&:&::00D0:X:NN0DXDXbNNbDblXllNbvblv:00&&D:D&0D&DN:DN:NNDDb:XXNblXblNlvXlvblŠbvŠ&&:0D00N&DN&DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”&:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠbvŠlŠŠl€”€€ž0DX:DX:DXDNb:XlNXlNbvblvbvvlvvlvŠlŠ”vŠŠv””ŠŠ¨€ž¨:DbDNXDXbNblDbvXbvblvbl€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Šž¨NNbNXbDXvNbvbbvbv€blŠlv€l€”vŠ”€””ŠŠ”€”¨Š”²”ž¼”¨²XblXllblvXl€Xv€b€ŠvvŠv€”€”ž€”¨Š”¨Š”ž”ž¼”¨²ž²Æž²¼Xl€blvbvŠbv”l€”l€žvŠžv”žŠžžŠž¨Š¨¼”¨¨”²¼ž²Æž²Æ²ÆÆbvŠbvŠvv”v€”€Š”vДДž””¨””²”ž²ž²¼ž¨²¨²¼²²Æ¨ÆÐ²ÆÚvvŠl€”lŠ”v””Š”ž€”¨Š¨¨”ž²”¨¼ž²¼ž²¼¨¼Æ¼¼Ð²¼Ð¼ÐÚ¼Æä€Š”€”žŠ”žŠ”¨Šž²Šž²”¨²¨²¼¨²¼¨²Æ²¼Æ²ÆÆ²ÐÐÆÚÚÆÚîÐÚ䀔”€žž””¨”ž¨”¨¼”²¼ž²Æ¨²¼²²Ð²¼Ð¼¼Ð¼ÆäÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼ž¨Æž²Ð²¼Æ²¼Æ²ÆÚ¼ÐÚÆÆäÆÚîÐÚäÚÚøÚäøäøÿЍ²”ž¼ž²¼¨²Æ²¼Æ¼ÆÐ¼ÆÐ²ÆäÆÚÚÆÚäÚÚîÚäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ú¼ÐÚ¼ÐÚÆÚäÆÚîÐääÚäøÚîøäîÿîøÿøøÿîÿÿ0&&:00&0:&::00D0DX:NN0DbDXlNNbDblXllNbvblv:00&&D:D00D0DN:DN:NNDDbDXXNblXblXl€XlvblŠbvŠ&&:&0D00N&DN0DN:Nb:XbNXbDXlXbbNblXb€Xl€l€€lv€lv”0:D&0N:DN0NN:DbDNbDXlNXvNbvblvbl€lvŠlvŠv€Šl€”€€ž0DX:DX:DXDXbDXlXblNbvblvbvvlvvlvŠl€”vŠŠv””€Š¨€ž¨:NbDNXDXbNblNbvXbvblvbv€lvŠlv”l€ŠvŠŠv””Š”žŠ”¨Š”¨NNbNXbDXvNb€blvbv€blŠl€€l€”vŠ”€””€Š”€”¨Š”²”ž²”¨²XblXllblvXv€bv€b€ŠvvŠv€”€”ž€ŠžŠ”¨Š”ž”ž²”ž²ž²Æž²¼Xl€blvbvŠlvŠl€”l€”vŠžv”ž€žžŠžžŠ¨²”ž¨”¨¼ž²Æž²Æ²ÆÆbvŠbvŠvv”v€”€Š”vДДž””¨””²”ž²ž²¼ž¨²ž²¼²²Æ¨ÆÐ²ÆÚvvŠl€”lŠ”v””ŠŠž€”¨Šž¨”ž²”¨¼ž¨¼ž²¼¨¼Æ²¼Ð²¼Ð¼ÐÚ¼Æä€Š”€””Š”žŠ”¨Š”²Šž²”¨²¨¨¼¨²¼¨²Æ²¼Æ²ÆÆ²ÐÐÆÚÚÆÚîÆÐ䀔”€žžŠ”¨”ž¨”¨¼”²²ž²Æ¨²¼²²Æ²¼Ð¼¼Ð¼ÆäÐÐäÆÚäÐäøÚä²Š¨²”¨²”¨¼ž¨Æž²Æ²¼Æ²¼Æ²ÆÚ¼ÐÚÆÆäÆÚîÐÚäÚÚîÚäøäîÿŠž²”ž¼ž²¼¨¨Æ²²Æ²ÆÐ¼ÆÐ²ÆÚ¼ÚÚÆÚäÐÚîÐäîÐäøÚîøäøÿäøÿ¨¨¼ž²Æ¨¼Æ²²Ð²¼Ð¼ÐÚ¼ÆÚÆÚÚÆÚîÐääÐÚøÐîøäîÿîøÿøøÿîÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f0.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f0.wai new file mode 100644 index 000000000..12e3ee39f Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f0.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai new file mode 100644 index 000000000..3c5080479 Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f1.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f2.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f2.wai new file mode 100644 index 000000000..2fe9fc456 Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f2.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai new file mode 100644 index 000000000..e6b7cbd05 Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/L1_f3.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai new file mode 100644 index 000000000..bf443df7a Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/L2.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/base.wai b/wai/staged-conformance/cases/video_mixed_stage_l2/base.wai new file mode 100644 index 000000000..af3eb82cb Binary files /dev/null and b/wai/staged-conformance/cases/video_mixed_stage_l2/base.wai differ diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/case.json b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json new file mode 100644 index 000000000..747861ff0 --- /dev/null +++ b/wai/staged-conformance/cases/video_mixed_stage_l2/case.json @@ -0,0 +1,262 @@ +{ + "format": "wai-staged-case/1", + "name": "video_mixed_stage_l2", + "about": "frame 2's layer 1 is withheld: the run-form layer 2 applies to frames 0, 1 and 3, and frame 2 holds it to the bound (layer-gap)", + "bases": [ + { + "file": "base.wai", + "object": "2c3e89c96108c89ae9b1e2372970aafb931e95213c9b025326fbb3c8e12d0478", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 16, + 16, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "dbd1edfbac1e8c3bfc3a51b0adc5c82a23239d6b5af7533a038786d3035df5b5", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_f0.wai", + "object": "751ec9254133839e0d9db60014038975ea8e4a85b7097195258cdcce9ea25189", + "bytes": 591 + }, + { + "file": "L1_f1.wai", + "object": "0debc989b7453a2a6c2737b18d0457b11500694d9e7d72d3f3716505bc9acfe7", + "bytes": 587 + }, + { + "file": "L1_f2.wai", + "object": "a9cce9e927fc962de8e1214b362dfa1b709fc0c88efde9790ad7c3fb97eb9049", + "bytes": 587 + }, + { + "file": "L1_f3.wai", + "object": "dd734fcfd18e303d51742e97d9f3f1d6ec69b48a0a153f1a05599d997a76a3fb", + "bytes": 583 + }, + { + "file": "L2.wai", + "object": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "bytes": 1546 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_f0.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_f3.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1003 + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "751ec9254133839e0d9db60014038975ea8e4a85b7097195258cdcce9ea25189", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "0debc989b7453a2a6c2737b18d0457b11500694d9e7d72d3f3716505bc9acfe7", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "dd734fcfd18e303d51742e97d9f3f1d6ec69b48a0a153f1a05599d997a76a3fb", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 3 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1, + 3 + ], + "held": [ + 2 + ], + "refused": [] + } + ], + [ + { + "object": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 2, + "layer-gap" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 2, + "producer": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "06b8941043bee45a6840c6111477430a1c92ce95dec3a69176a7ad9aab34d803" + }, + { + "stage": 2, + "producer": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "aa667d4e8e494d67e8f9643c32e1a24d8b2f4f9e4f8c8c2bc931b5a11d4752d7" + }, + { + "stage": 0, + "producer": "2c3e89c96108c89ae9b1e2372970aafb931e95213c9b025326fbb3c8e12d0478", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "74f0273edf6f4e063e74d5a7c1f0c7bdd0bbc600001d24336ba36bead44531b2" + }, + { + "stage": 2, + "producer": "81693c0b1f7dedc4cd75d6eccfa701b794850463675f9b28440a177bfbc77750", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 16, + 16, + 3 + ], + "stage_sha256": "1b17f8e9ce3abb77c96567a063c1306fde28493e7e356134374c027564821558" + } + ], + "clip_sha256": "a508b7b814d2bab836de80707fa4ce73c08e9f6261f4e77a24074d14bea1c875", + "n_units": 4 + } + ] + } +} diff --git a/wai/staged-conformance/cases/video_mixed_stage_l2/stage0.bin b/wai/staged-conformance/cases/video_mixed_stage_l2/stage0.bin new file mode 100644 index 000000000..94500d06e --- /dev/null +++ b/wai/staged-conformance/cases/video_mixed_stage_l2/stage0.bin @@ -0,0 +1 @@ +0000:0D00D0:N0DNDDX:NbDNbDXlNbvNbvXllbv€&0&00D&0:&DN::N:DNDDX:NbDNlNXlXXvNlvbvvblŠllŠ&0D&0N&DN::N0NN:DX:NX:NbDbbNblXbvXl€bl€lv€lv”v€”&0D::D0DX:DN:NX”²ÚNXlNbvXlvblvXl€blŠv€ŠvvŠv€”ŠŠž:DN0NXŠ¨ÚŠ²äŠ¼ä”Æä”¼î¨ÆîžÐøblŠl€€l€ŠvŠžv€ž€ŠžŠ”ž:NXŠ²äŠ¼ä”²äžÆäžÆîžÆø¨Ðÿ²Úø²äÿv€”vŠž€Šž€”¨Šžžžž¨:Xlм䔼îžÐîžÐî¨Ðî¨Úÿ²äÿ¼äÿ¼äÿ€Šž€Š¨ŠžžŠž²Š¨²”²¼Nblž¼î¨Ðî²Æø²Ðø¼Úÿ¼Úÿ¼äÿÆøÿÆøÿŠž²Šž²Š¨²”²¼ž¨Æ¨²¼¨Ðî¨Úî¨Ðø²Úø¼äÿ¼äÿÆîÿÐäÿÚîÿÚøÿÚøÿž¨²ž²²¨¨¼¨²Æ²¼Ðbv€¨Ðÿ²äÿ¼äÿ¼îÿÆøÿÆøÿÐøÿäøÿäÿÿ¨¨¼ž²Æ¨¼Æ²¼Ð¼¼Ð¼ÆÚl€”¼Úÿ¼äÿÆîÿÆøÿÐÿÿÚÿÿÚÿÿÚÿÿîÿÿ¨²¼²²Ð¨ÆÚ¼ÆÐÆÐÚÆÚÚvŠ”ÆäÿÆîÿÚÿÿÐÿÿÚÿÿäÿÿîÿÿäÿÿîÿÿ²ÆÆ²¼Ú¼ÆäÆÐäÆÚäÐä䊊žŠ”²ÐÿÿÚøÿäÿÿäÿÿîÿÿîÿÿøÿÿ²ÆÐ¼ÆÚ¼ÐÚÆÚäÐÚäÐÚäÚä¨žž²”ž¼ž²Æž²¼îÿÿ¨¼Ð²ÆÐ¼ÐÚ¼ÐäÆÐÚÆÐäÐÚîÚäîÚäÿÚîÿ”¨²ž¨¼¨¨Æ¨²Æ²¼Æ²ÆÚ¼ÆÚÆÐÚÆÐäÐÚäÐÚîÚÚîäîøäîøäîøäÿÿž²Æ¨¼¼²²Æ²ÆÚ¼ÆÐ¼Æä¼ÚÚÆÚäÐäøÚÚîÐäøÚîøîøÿäÿÿäÿÿøÿÿ&&000&0:0D00D0:N0DNDDX:NbDNbDXlNbvNbvXllbv€&0&00D&0:&DN::N:DNDDX:NlDNlNblXXvNlvbvvblŠll€&0D0:N&DN::N0NN:DX:NX:NlDbbNllXb€Xl€bl€lv€lvŠv€”&0D::D0DX:DN:NXNNbNXlNbvXlvblvXl€blŠv€ŠvvŠv€”€Šž:NN0NXDNbDNlDXlNblNXlžÆîXl€blŠl€€l€ŠvŠžv€ž€ŠžŠ”ž:NX:NbDXlNXlžÆäžÆîžÆø¨Æÿ²Úø²äÿ¼äÿvŠž€Šž€Š¨Šžž”ž¨DXlDXlNXvžÆîžÐî¨Ðî¨Úÿ¨äÿ¼äÿ¼äÿÆäÿÆîÿŠžžŠž²Š¨²”²¼NblXbvbvv¨Æø²Ðø¼Úÿ¼Úÿ¼äÿÆîÿÆøÿÐøÿÐøÿЍ²”²¼ž¨¼ž²¼bvvbvvbl€²Úø¼Úÿ¼äÿÆîÿÐäÿÐîÿÚîÿÚøÿäÿÿž²²¨¨¼¨²Æ²²Ðbv€bl‘²äÿ²äÿ¼îÿÆøÿÆøÿÆøÿäøÿäÿÿîÿÿäÿÿîÿÿ²¼Ð¼¼Ð¼ÆÐl€”v€‘v€›ÆîÿÆøÿÆÿÿÚÿÿÚÿÿÚÿÿîÿÿîÿÿøÿÿ¨ÆÚ¼ÆÐÆÐÚÆÚÚvŠ”€Š¥€”›ÚÿÿÐÿÿÚÿÿäÿÿîÿÿäÿÿîÿÿøÿÿøÿÿ¼ÆÚÆÐäÆÚäÐä䊊žŠ”²Š¥¯ÚøÿäÿÿäÿÿäÿÿîÿÿîÿÿøÿÿÿÿÿÿÿÿÆÐäÐÚäÐÚäÚä¨žž²”ž¼ž²¼äÿÿîÿÿîÿÿøÿÿÿÿÿÿÿÿÿÿÿÆÐäÐÚîÚäîÐäÿÚîÿ”¨¨ž¨²¨¨Æ¨²Æ²²Æ²¼Ú²¼ÚÿÿÿÆÐäÐÚäÐÚîÚÚîäîøäîøäîøäÿÿž²¼ž¼¼²²Æ¨¼Ú¼ÆÐ¼Æä¼ÚÚÆÚäÐäøÚÚîÐäøÚîøîøÿäÿÿäÿÿîÿÿ0000:0D00D0:N0DNDDX:NbDNbDXlNbvNbvXllbv€&0&00D&0:&DN::N:DNDDX:NbDNlNXlXXvNlvbvvblŠllŠ&0D&0N&DN::N0NN:DX:NX:NbDbbNblXbvXl€bl€lv€lv”v€”&0D::D0DX:DN:NXNNbNXlNbvXlvblvXl€blŠv€ŠvvŠv€”ŠŠž:DN0NXDNbDNlDXlNblNXvXlvXl€blŠl€€l€ŠvŠžv€ž€ŠžŠ”ž:NXDXlDXlNXlXblXlvXl€bl‡lv€²äÿv€‘vŠž€Šž€”¨Šžžžž¨:XlDXlNXvXlvXvvbvv¨Úÿ²äÿ¼äÿ¼äÿÆäÿÆîÿÐÿÿŠž²Š¨²”²¼NblXbvbvvbl€lv€¼Úÿ¼Úÿ¼äÿÆøÿÆøÿÐøÿÐøÿÐÿÿÚÿÿž¨Æ¨²¼bvvbvvbl€l€€v€‘¼äÿÆîÿÐäÿÚîÿÚøÿÚøÿäÿÿäÿÿîÿÿ¨²Æ²¼Ðbv€bv‘l€‘lŠ‘v”›ÆøÿÆøÿÐøÿäøÿäÿÿäÿÿäÿÿäÿÿøÿÿ¼¼Ð¼ÆÚl€”v€‘v€›€”‘ÆøÿÐÿÿÚÿÿÚÿÿÚÿÿîÿÿîÿÿøÿÿîÿÿÿÿÿÿÿÿÆÚÚvŠ”€Š¥€”›”›¯Š¥¯ÚÿÿäÿÿîÿÿäÿÿîÿÿøÿÿøÿÿÿÿÿÿÿÿÆÚäÐä䊊žŠ”²Š¥¯”ž¯ž¯¯äÿÿîÿÿîÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÐÚäÚä¨žž²”ž¼ž²Æž¯¹îÿÿîÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÚäÿÚîÿ”¨²ž¨¼¨¨Æ¨²Æ²¼Æ²ÆÚÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿäîøäîøäÿÿž²Æ¨¼¼²²Æ²ÆÚ¼ÆÐ¼Æä¼ÚÚÆÚäÐäøÿÿÿÐäøÚîøîøÿäÿÿäÿÿøÿÿ&&000&0:0D00D0:N0DNDDX:NbDNbDXlNbvNbvXllbv€&0&00D&0:&DN::N:DNDDX:NlDNlNblXXvNlvbvvblŠll€&0D0:N&DN::N0NN:DX:NX:NlDbbNllXb€Xl€bl€lv€lvŠv€”&0D::D0DX:DN:NXNNbNXlNbvXlvblvXl€blŠv€ŠvvŠv€”€Šž:NN0NXDNbDNlDXlNblNXlblvXl€blŠl€€l€ŠvŠžv€ž€ŠžŠ”ž:NX:NbDXlNXlXblXlvXl€bl‡lv€l€‘v€‘vŠž€Šž€Š¨Šžž”ž¨DXlDXlNXvXlvXvvbvvb€‡b€‡vЇvŠ›€Š¥ÆîÿŠ››Šž²Š¨²”²¼NblXbvbvvll€lv€vv‘v€‘v€‘ÆîÿÆøÿÐøÿÐøÿÐÿÿÚÿÿäÿÿž²¼bvvbvvbl€l€€v€‘vŠ›€Š›ÐäÿÐîÿÚîÿÚøÿäÿÿäÿÿîÿÿîÿÿøÿÿbv€bl‘l€‘vŠ‘v”›€”›€”¯Æøÿäøÿäÿÿîÿÿäÿÿîÿÿøÿÿÿÿÿÿÿÿl€”v€‘v€›€”›€ž›€›¥”›¥ÚÿÿÚÿÿîÿÿîÿÿøÿÿîÿÿÿÿÿÿÿÿÿÿÿvŠ”€Š¥€”›”›¯Š¥¯”¥¯äÿÿîÿÿäÿÿîÿÿøÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠŠžŠ”²Š¥¯”ž¹ž¯¯ž¥¹ž¯ÃîÿÿîÿÿøÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿŠž¨žž²”ž¼ž²¼ž¯¹¨¹Ã¨¹×øÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ”¨¨ž¨²¨¨Æ¨²Æ²²Æ²¼Ú¹Ã×ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿž²¼ž¼¼²²Æ¨¼Ú¼ÆÐ¼Æä¼ÚÚÆÚäÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿîÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai b/wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai new file mode 100644 index 000000000..fccb29688 Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_bad_magic.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_elem.wai b/wai/staged-conformance/cases/wir1_faults/L1_elem.wai new file mode 100644 index 000000000..3c4468159 Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_elem.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_reserved.wai b/wai/staged-conformance/cases/wir1_faults/L1_reserved.wai new file mode 100644 index 000000000..d83af1b50 Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_reserved.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_too_large.wai b/wai/staged-conformance/cases/wir1_faults/L1_too_large.wai new file mode 100644 index 000000000..b89a8252a Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_too_large.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_trailing.wai b/wai/staged-conformance/cases/wir1_faults/L1_trailing.wai new file mode 100644 index 000000000..073d0dffd Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_trailing.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_unit_count.wai b/wai/staged-conformance/cases/wir1_faults/L1_unit_count.wai new file mode 100644 index 000000000..79a341906 Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_unit_count.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/L1_version.wai b/wai/staged-conformance/cases/wir1_faults/L1_version.wai new file mode 100644 index 000000000..9f24d637b Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/L1_version.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/base.wai b/wai/staged-conformance/cases/wir1_faults/base.wai new file mode 100644 index 000000000..624c3f2d1 Binary files /dev/null and b/wai/staged-conformance/cases/wir1_faults/base.wai differ diff --git a/wai/staged-conformance/cases/wir1_faults/case.json b/wai/staged-conformance/cases/wir1_faults/case.json new file mode 100644 index 000000000..f1d29f609 --- /dev/null +++ b/wai/staged-conformance/cases/wir1_faults/case.json @@ -0,0 +1,251 @@ +{ + "format": "wai-staged-case/1", + "name": "wir1_faults", + "about": "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", + "bases": [ + { + "file": "base.wai", + "object": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "de810c5d3cb16230bbe543b17bfbe39b33d45fd60b3d43ee7c323980a347396b", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_unit_count.wai", + "object": "640aa8c0b9908c32945648da6f0485c7e0da11519fe9307792222610a521c280", + "bytes": 615 + }, + { + "file": "L1_bad_magic.wai", + "object": "234645d4b51e53af57bc62bb2c67122792c578b1e96f30e1ab5bad042ee7711f", + "bytes": 587 + }, + { + "file": "L1_version.wai", + "object": "ee6bfe4eb4de3d8332d318b766edfa559f2bd1b6f92f8c8b9d80c62071fe9445", + "bytes": 587 + }, + { + "file": "L1_reserved.wai", + "object": "f2ab56f3d89717d2becadbb448e17c4731c7988dbf51405de8a1061bd36a5db1", + "bytes": 587 + }, + { + "file": "L1_elem.wai", + "object": "5a5ac824bc750d07504b78717c76bb688ae00d1aee2e10c07746ead5656f6657", + "bytes": 587 + }, + { + "file": "L1_too_large.wai", + "object": "16abad5a9070fd7be41edb8ed74a22de1693e43e5e4019a9be49a127cf3a2aef", + "bytes": 587 + }, + { + "file": "L1_trailing.wai", + "object": "5a74b598df4caa7010df5b8e64ba96f366225e65fb2588f6bea971b85d7e10b3", + "bytes": 588 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_unit_count.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_bad_magic.wai", + "at_ms": 2, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_version.wai", + "at_ms": 3, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_reserved.wai", + "at_ms": 4, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_elem.wai", + "at_ms": 5, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_too_large.wai", + "at_ms": 6, + "evidence": "none" + }, + { + "op": "offer", + "file": "L1_trailing.wai", + "at_ms": 7, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "640aa8c0b9908c32945648da6f0485c7e0da11519fe9307792222610a521c280", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "234645d4b51e53af57bc62bb2c67122792c578b1e96f30e1ab5bad042ee7711f", + "envelope": "payload-malformed", + "detail": "bad_magic", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "ee6bfe4eb4de3d8332d318b766edfa559f2bd1b6f92f8c8b9d80c62071fe9445", + "envelope": "payload-malformed", + "detail": "version", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "f2ab56f3d89717d2becadbb448e17c4731c7988dbf51405de8a1061bd36a5db1", + "envelope": "payload-malformed", + "detail": "reserved_nonzero", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5a5ac824bc750d07504b78717c76bb688ae00d1aee2e10c07746ead5656f6657", + "envelope": "payload-malformed", + "detail": "field_range", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "16abad5a9070fd7be41edb8ed74a22de1693e43e5e4019a9be49a127cf3a2aef", + "envelope": "payload-malformed", + "detail": "too_large", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "5a74b598df4caa7010df5b8e64ba96f366225e65fb2588f6bea971b85d7e10b3", + "envelope": "payload-malformed", + "detail": "trailing", + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7142cdba98ba22a116ce4487ffdacc69631c71f9824304efa84f3b5af9927261" + }, + { + "stage": 0, + "producer": "ed5412690ea1fb0774f634a2272f34c95b07b54e5bbebc6b83e801693778c2f1", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "7a0805ea8887f8f73e5fa8725c8c089ec9131cd54b88d41626ae44f7c7cc3dd3" + } + ], + "clip_sha256": "1d1f19c330ee7162da72c4af9c6f543d5faa4592e1afa441fe669e2f8ff348aa", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/wir1_faults/stage0.bin b/wai/staged-conformance/cases/wir1_faults/stage0.bin new file mode 100644 index 000000000..692f7a959 --- /dev/null +++ b/wai/staged-conformance/cases/wir1_faults/stage0.bin @@ -0,0 +1 @@ + @ @H0@P8P`@X`X`xhp€hxˆ0@H8@`PX`XXpXh€hpˆh€˜xˆ¨HX`PXx˜Èøhx€x€�xˆ ˆ  ˜˜¸Xpx¨Øø°ØÿÀàÿˆ˜ ˆ¨¸ ¨¸ ¸Ð¸ØÿÀèÿÀøÿØøÿèÿÿ °È°¸ØÀÐØ€ˆ˜ÈÿÿØÿÿèÿÿ°ÀÈ°ÈØÈÈèÐØè˜¨¸ °Àèÿÿ¸ÈÐÀÐØÈàðÐèøàøÿ¨°È°ÀظÈàÈÐèØèðàøøðÿÿðÿÿ (@ @H0@P8X`@X`X`xhx€px€0@P8@`PX`XXpXh€hpˆp€˜xˆ¨PX`PXxPh€hx€x€�xˆ ˆ  ˜˜¸Xpx`xxhx�x€ŸÈøÿˆ¨° ¨¸ ¸Ðpx�xˆ�x˜ŸØðÿèÿÿèÿÿ°¸Ø¸ÐЀˆ˜ˆŸŸØÿÿèÿÿøÿÿøÿÿÿÿÿÐØè˜¨° °À ·ÏÿÿÿÿÿÿÿÿÿÐèøàøÿ¨°È°¸Ø¸ÈàÈÐèÿÿÿàøøðÿÿðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/wrong_base/L1_other.wai b/wai/staged-conformance/cases/wrong_base/L1_other.wai new file mode 100644 index 000000000..3c0464a54 Binary files /dev/null and b/wai/staged-conformance/cases/wrong_base/L1_other.wai differ diff --git a/wai/staged-conformance/cases/wrong_base/base.wai b/wai/staged-conformance/cases/wrong_base/base.wai new file mode 100644 index 000000000..c68651035 Binary files /dev/null and b/wai/staged-conformance/cases/wrong_base/base.wai differ diff --git a/wai/staged-conformance/cases/wrong_base/case.json b/wai/staged-conformance/cases/wrong_base/case.json new file mode 100644 index 000000000..2fc1d4f5e --- /dev/null +++ b/wai/staged-conformance/cases/wrong_base/case.json @@ -0,0 +1,134 @@ +{ + "format": "wai-staged-case/1", + "name": "wrong_base", + "about": "a layer of another stage set, whose base this sink never decodes: held for its base, then base-absent at the bound", + "bases": [ + { + "file": "base.wai", + "object": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "6620a5f72b982626e0b60f299c86edbc659001db5522bc6ba5a57caf3c03933c", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1_other.wai", + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "bytes": 591 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1_other.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "expire", + "at_ms": 1002 + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "envelope": null, + "detail": null, + "held_for_base": true, + "applied": [], + "held": [], + "refused": [] + } + ], + [ + { + "object": "6cd79d9e2b416d87bed384d14aa5b30934487118f4c9a200c3bf165193bc3629", + "envelope": "base-absent", + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 0, + "producer": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3d1ef19796ebd5b762a6c36d63c19bea1fff96b0e1479e4d101501e17eed69e7" + }, + { + "stage": 0, + "producer": "490b4d6126bcf59ea28b8b9562d0a5f64aee20802071ec86870c9e20331011d2", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "3a26cc2ec09c0b32314c0ae704bbec97a6cc450a570e22244d3e2ec086f743e3" + } + ], + "clip_sha256": "67b9c039ec748b3c1b0e38822db0a185936ead9d711fc08b814918cecda6cf3a", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/wrong_base/stage0.bin b/wai/staged-conformance/cases/wrong_base/stage0.bin new file mode 100644 index 000000000..8a434ce4f --- /dev/null +++ b/wai/staged-conformance/cases/wrong_base/stage0.bin @@ -0,0 +1 @@ + 0 0@0@@8@HHP`PPpPpp`px08H0@H8PXPPpX`p`pˆpxˆp€˜8@XPP` ¸èXp€px€px˜x� ˆ  P`p¨Èð°Øø¸Øÿ€ˆ ˆ˜ ˜ ¨ ¨¸¨ÐÿÀØÿ¸ðÿÈøÿØÿÿ  ¸¨°À¸¸Ðx€�ÐøÿÐÿÿØÿÿ °À°¸ÈÀÈàÀàè� °˜¨°èÿÿ°ÀиÀØÈØØÐàèÐèø˜°¸¨¸Ð°ÀظÈàØØèØàÿèðøðÿÿ 0 8@0@@8@PHX`PPpPpp`p€08H0@H8PXPXpX`p`pˆpxˆp€˜8HXPP`XXpXp€p€€x€˜xˆ ˆ  P`p`hxhx€px—Èèÿˆ˜ ˜ ¨ ¨¸`p‡xx�x��ÈøÿØÿÿèÿÿ¨°À¸¸Ðx€�ˆ˜ŸÐÿÿØÿÿèÿÿøÿÿÿÿÿÀàè�˜°˜¨° ·ÇøÿÿÿÿÿÿÿÿÐàèÐèø˜°¸¨¸Ð°ÀиÈàÿÿÿØàÿèðøðÿÿ \ No newline at end of file diff --git a/wai/staged-conformance/cases/wrong_prev/L1.wai b/wai/staged-conformance/cases/wrong_prev/L1.wai new file mode 100644 index 000000000..436a1b20d Binary files /dev/null and b/wai/staged-conformance/cases/wrong_prev/L1.wai differ diff --git a/wai/staged-conformance/cases/wrong_prev/L2_wrong_prev.wai b/wai/staged-conformance/cases/wrong_prev/L2_wrong_prev.wai new file mode 100644 index 000000000..2b15be934 Binary files /dev/null and b/wai/staged-conformance/cases/wrong_prev/L2_wrong_prev.wai differ diff --git a/wai/staged-conformance/cases/wrong_prev/base.wai b/wai/staged-conformance/cases/wrong_prev/base.wai new file mode 100644 index 000000000..3d4bba18d Binary files /dev/null and b/wai/staged-conformance/cases/wrong_prev/base.wai differ diff --git a/wai/staged-conformance/cases/wrong_prev/case.json b/wai/staged-conformance/cases/wrong_prev/case.json new file mode 100644 index 000000000..ca188a2d5 --- /dev/null +++ b/wai/staged-conformance/cases/wrong_prev/case.json @@ -0,0 +1,153 @@ +{ + "format": "wai-staged-case/1", + "name": "wrong_prev", + "about": "a layer 2 whose prev names the base, not the layer 1 the units were refined by: prev-mismatch", + "bases": [ + { + "file": "base.wai", + "object": "a969be8e4a855e150f5e0459e19fe7ff2571eb7f91bf61bcdae634efe154a58a", + "decoded_via": "wai.video.int_motion", + "via_fallback": false, + "attested": false, + "first_unit": 0, + "pin_resolved": false, + "tier": "decode-equivalence", + "units": [ + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + }, + { + "shape": [ + 8, + 8, + 3 + ], + "elem": "u8" + } + ], + "stage0": "stage0.bin", + "stage0_sha256": "47758afd8a5c17036b99f578cf72c421627d01aca040d307c9b284d1f3deadad", + "decode": { + "kind": "int", + "set": {} + } + } + ], + "files": [ + { + "file": "L1.wai", + "object": "6e0d7c9535fdecacb1c3331d820dea83624b4ca3dc5b84ef96d67ce6bf4fb732", + "bytes": 583 + }, + { + "file": "L2_wrong_prev.wai", + "object": "424282c0abb7d9af53b76417d1011f6671dd7e700275f18640fa2e5eee624a91", + "bytes": 795 + } + ], + "config": { + "bound": { + "max_envelopes": 16, + "max_age_ms": 1000 + }, + "cost_limit": null, + "budget": 1073741824, + "record_budget": 1048576, + "live": null + }, + "steps": [ + { + "op": "base", + "index": 0, + "at_ms": 0 + }, + { + "op": "offer", + "file": "L1.wai", + "at_ms": 1, + "evidence": "none" + }, + { + "op": "offer", + "file": "L2_wrong_prev.wai", + "at_ms": 2, + "evidence": "none" + } + ], + "expect": { + "reports": [ + [], + [ + { + "object": "6e0d7c9535fdecacb1c3331d820dea83624b4ca3dc5b84ef96d67ce6bf4fb732", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [ + 0, + 1 + ], + "held": [], + "refused": [] + } + ], + [ + { + "object": "424282c0abb7d9af53b76417d1011f6671dd7e700275f18640fa2e5eee624a91", + "envelope": null, + "detail": null, + "held_for_base": false, + "applied": [], + "held": [], + "refused": [ + [ + 0, + "prev-mismatch" + ], + [ + 1, + "prev-mismatch" + ] + ] + } + ] + ], + "bases": [ + { + "units": [ + { + "stage": 1, + "producer": "6e0d7c9535fdecacb1c3331d820dea83624b4ca3dc5b84ef96d67ce6bf4fb732", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "ce43a5bead61d7b60e9ed4fafa841088e71314e5dd8d78596c3788b0f0f48097" + }, + { + "stage": 1, + "producer": "6e0d7c9535fdecacb1c3331d820dea83624b4ca3dc5b84ef96d67ce6bf4fb732", + "frozen": false, + "tier": "decode-equivalence", + "shape": [ + 8, + 8, + 3 + ], + "stage_sha256": "d1b53fc96106a63b6956fa37c670e5867c4e45ff170ba8216015d4408ca97955" + } + ], + "clip_sha256": "4a6936471adbc31b49ca561ebe459c36d37fd4eaf4e301afa75796d348606090", + "n_units": 2 + } + ] + } +} diff --git a/wai/staged-conformance/cases/wrong_prev/stage0.bin b/wai/staged-conformance/cases/wrong_prev/stage0.bin new file mode 100644 index 000000000..d21fa0297 --- /dev/null +++ b/wai/staged-conformance/cases/wrong_prev/stage0.bin @@ -0,0 +1 @@ + 0 0H0@H@@X@XhXXhXhxhx€(@@8@X@PXPXhPhxhpˆhˆ�€€˜8P`PPh˜Èè`pˆh€�€€�ˆ�˜ˆ˜°P`p¨Ðð°àÿÀàÿˆ�˜˜˜° °° ¸À°ÐÿÀèÿÐèÿÈðÿØÿÿ ¸À°¸È¸ÀØ€�˜Èøÿàøÿèÿÿ¨¸À°ÈØÀÐØÐà舠¸˜°¸ðÿÿ°ÈÈÀÐàÈØðØàøàðø °È¨ÀÐ¸ÐØÀØèÐØðØèÿððÿøøÿ 8 0H0@H@@X@XpXXpXh€hx€0@H8@X@PXPXhPhxhpˆh€�€€˜8PhPXhPhp`pˆh€�€€�ˆ�˜ˆ˜°P`x`pxh€‡x€�Ððÿ˜˜°˜°° °Àhp‡xˆ�ˆˆŸÈðÿØÿÿàÿÿ°¸È¸ÀØ€�˜€˜ŸàøÿèÿÿðÿÿøÿÿÿÿÿÐà舠°˜°¸¨¿ÇøÿÿÿÿÿÿÿÿØàøàðø °È¨ÀÐ¸ÐØÀØèÿÿÿØèÿððÿøøÿ \ No newline at end of file diff --git a/wai/staged-conformance/index.json b/wai/staged-conformance/index.json new file mode 100644 index 000000000..a73311241 --- /dev/null +++ b/wai/staged-conformance/index.json @@ -0,0 +1,489 @@ +{ + "format": "wai-staged-vectors/1", + "about": "The staged-delivery corpus (extensions/staged-delivery.md §12). Each case's case.json states its bases, layers, sink configuration and steps, and what a conforming sink reports and holds. Written by wai_staged_vectors gen.", + "cases": [ + { + "name": "image_int_hyper_2l", + "about": "a pinned wai.neural.int_hyper base, then two identity layers", + "clip_sha256": [ + "6f693b31869a008c460dfd0563ca14f213cf3634776362841a398da7b8369caa" + ] + }, + { + "name": "image_int_hyper_unpinned", + "about": "an unpinned wai.neural.int_hyper base: the layer applies, and no unit holds a tier, so no stage hash is stated", + "clip_sha256": [ + null + ] + }, + { + "name": "image_up2_from_int_hyper", + "about": "a 16×16 base, an up2 layer to 32×32, then an identity layer", + "clip_sha256": [ + "095a966d4a0e7f066e7c8cd5e7593c77951c6f554d87d402e32225dc55f7109d" + ] + }, + { + "name": "image_up2_odd", + "about": "up2 to an odd size, then up2 from it: 16×16 → 31×31 → 61×61", + "clip_sha256": [ + "64dba25135e5965fd511247477f911b85aa5a9b2fae263b64e791b72c9c43397" + ] + }, + { + "name": "image_png_up2", + "about": "a PNG base (a stage-0 fixture here), an up2 layer at q = 1", + "clip_sha256": [ + "d90170c38e38b10a4c6c4ebf129dc5aa577521373ecdf020b6b66207027f004e" + ] + }, + { + "name": "video_int_motion_gop_perframe", + "about": "three closed-GOP wai.video.int_motion bases of four frames; a layer 1 per frame; a layer 2 per GOP whose prev is the run of its frames' producers", + "clip_sha256": [ + "4423cde72c6a314df503f5698532daa9917673ab871867b0ab5aa07c6d933694", + "71b0a7a362611beb304a637aaa53a09a19bc2a4c5e8ffe365291051e089c3deb", + "613c31366b3112228afe87689ec4101bf8eeb400866f2c8646b476fb22b535be" + ] + }, + { + "name": "video_mixed_stage_l2", + "about": "frame 2's layer 1 is withheld: the run-form layer 2 applies to frames 0, 1 and 3, and frame 2 holds it to the bound (layer-gap)", + "clip_sha256": [ + "a508b7b814d2bab836de80707fa4ce73c08e9f6261f4e77a24074d14bea1c875" + ] + }, + { + "name": "video_int_hyper_keyframe", + "about": "a pinned wai.video.int_hyper clip (keyframe 1), then one layer over its five frames with a table per unit", + "clip_sha256": [ + "48ee337a6950548f9b5a1ec5db453f724bec3a9cabe41975d27d1d0f4af727cb" + ] + }, + { + "name": "audio_int_codec_1l", + "about": "a pinned wai.audio.int_codec base, then an identity layer", + "clip_sha256": [ + "ec7a4ad312d40a6ebdb33a5b55df02967415c3eb8f065c72e427d1a189b8810d" + ] + }, + { + "name": "audio_flac_rate_ladder", + "about": "a stereo FLAC base at 8 kHz (a stage-0 fixture here), a rate layer to 16 kHz, then one to 48 kHz", + "clip_sha256": [ + "0408de38e23d42bd6af8064e109c73ab90582eaad588c68fbcf8b25fe9cf4c29" + ] + }, + { + "name": "splat_attr_1l", + "about": "a pinned wai.splat.int_codec base, then an identity layer over its attribute tensor", + "clip_sha256": [ + "26681e76f0371922e180e05b31d7f8fb99644bfcebd8ce40433d3287bedacac3" + ] + }, + { + "name": "wrong_base", + "about": "a layer of another stage set, whose base this sink never decodes: held for its base, then base-absent at the bound", + "clip_sha256": [ + "67b9c039ec748b3c1b0e38822db0a185936ead9d711fc08b814918cecda6cf3a" + ] + }, + { + "name": "refinement_without_base", + "about": "a layer whose base never arrives: held, then base-absent at the bound", + "clip_sha256": [ + null + ] + }, + { + "name": "wrong_prev", + "about": "a layer 2 whose prev names the base, not the layer 1 the units were refined by: prev-mismatch", + "clip_sha256": [ + "4a6936471adbc31b49ca561ebe459c36d37fd4eaf4e301afa75796d348606090" + ] + }, + { + "name": "missing_layer", + "about": "a layer 2 whose layer 1 never arrives: held for each unit, then layer-gap at the bound", + "clip_sha256": [ + "d34c967288ab8c16232bd8db4552445490c0e316910f5169d39e23eb8f1eff6a" + ] + }, + { + "name": "conflict_held", + "about": "two different layers 1 held for the base and released together: neither applies, and both units are frozen", + "clip_sha256": [ + "3dda0036e74dc29f95ae738b5eeb45e9028444716482de142cadc69f05d7b99c" + ] + }, + { + "name": "conflict_after_apply", + "about": "layer 1a applies; a different layer 1b conflicts and freezes the units at stage 1; a layer 2 over 1a is then refused", + "clip_sha256": [ + "4a856cfc54c1f88820a2319f3e995165e3ae3821541149342e2b65d148c48a2b" + ] + }, + { + "name": "pending_l1_l2", + "about": "layers 2 and 1 both arrive before their base: released together, layer 1 applies and releases layer 2, which applies", + "clip_sha256": [ + "63165f2901cceb0953886df8df37c7851157b8c4c8cbb54b9fdf871f7aa0dd8c" + ] + }, + { + "name": "out_of_order", + "about": "layer 2 arrives before layer 1: held, and applied when layer 1 releases it; the stages equal an in-order arrival", + "clip_sha256": [ + "23f2853e24968ceb200465105f49e760b874bec4d5bb2b92cf477ee786316ae4" + ] + }, + { + "name": "base_via_fallback", + "about": "the base was decoded through its manifest's fallback: base-via-fallback", + "clip_sha256": [ + "ceebade3184f55da1c234a64e00d29d7177669551c8f0a3691177790545a0ac5" + ] + }, + { + "name": "geometry_mismatch", + "about": "a layer coded for 4×4 frames over 8×8 frames: geometry-mismatch for each unit", + "clip_sha256": [ + "313eabaa3073a65f3307278dd1ef33d59b156aee8a4748eda0fa9571a1e6ac41" + ] + }, + { + "name": "payload_truncated", + "about": "a WIR1 payload three bytes short: payload-malformed (stream_length)", + "clip_sha256": [ + "627035f9387f55f17561c73517c670fe1e9a473485d5b7e3991c8aa29959146c" + ] + }, + { + "name": "cdf_malformed", + "about": "a table entry set to 0 in mid-table: payload-malformed (cdf_malformed)", + "clip_sha256": [ + "a691f382b2ad20d285b30a25c07273f155b5c0e73245c1ae76a4b4a7e86810ff" + ] + }, + { + "name": "q_zero", + "about": "a step q of 0: payload-malformed (field_range)", + "clip_sha256": [ + "e1c0b26d1993566ed46788477da676478a9bc99ca9305de3e060e4df230869ee" + ] + }, + { + "name": "stream_noncanonical", + "about": "a word after unit 0's last symbol: payload-malformed (stream_noncanonical)", + "clip_sha256": [ + "7b98f9d249aa3a3e5197afd3e0c537a687db9b47d67c02c050f88a8edf845903" + ] + }, + { + "name": "stream_exhausted", + "about": "unit 0's stream codes no symbol: it runs out at the first, payload-malformed (stream_exhausted)", + "clip_sha256": [ + "eb8da278015a66532af62551560d52a3ca545a3def8e711f6322c2f6c50dcb9e" + ] + }, + { + "name": "clamp_extremes", + "about": "targets at 0 and 255 with q = 200: residuals that overshoot are clamped to the u8 range", + "clip_sha256": [ + "bf3594e190c527a1126118071829dde7d08b149b4f1cd5e060ce6c770005af13" + ] + }, + { + "name": "bypass_extremes", + "about": "a table of one symbol at q = 1: nearly every residual takes the bypass escape, and the layer is exact", + "clip_sha256": [ + "cb6d3b466bf10bb01752c1379a6ca1f6bcd9961eb27d0cb57542a901b71fc5c7" + ] + }, + { + "name": "layer_zero", + "about": "refines.layer 0: refines-malformed", + "clip_sha256": [ + "cab5c1a48b2d4754278d448d7a67e55ffccce8355d648010cb20e2b9bcf718b3" + ] + }, + { + "name": "refines_malformed", + "about": "a base hash in upper case: refines-malformed", + "clip_sha256": [ + "722c73d658ad74d2cf85551ab63181cf55b83aac12da71bb5f58ae06f7323720" + ] + }, + { + "name": "capability_not_refinement", + "about": "refines on a wai.video.int_motion envelope: capability-not-refinement", + "clip_sha256": [ + "39d743f21b782cc4ea1d1bacb50e0244af72459a21746ef19e6571a9a712c3ad" + ] + }, + { + "name": "base_not_admitted", + "about": "a wai.image.int_refine layer over a wai.video.int_motion base: base-not-admitted", + "clip_sha256": [ + "e532573c38be3eeb5dddc8c5ce7c0af82beb3a5bfd71556f4581acdd7a47508c" + ] + }, + { + "name": "inclusion_only_evidence", + "about": "an attested base: the layer verified by inclusion only is refinement-unattested; the same layer delivered-verified applies", + "clip_sha256": [ + "b00fb4f9ce8446663603cd9b7911308473bc53b02daf6603cf68a3bca6eaf22d" + ] + }, + { + "name": "cost_over_limit", + "about": "a sink whose cost limit is 100 output bytes: the 384-byte layer is cost-over-limit", + "clip_sha256": [ + "2b8ae031e774ec0b8efbd0a1332185e8395689213fd113dcf38d6ad8e9236a7a" + ] + }, + { + "name": "deadline_missed", + "about": "units presented at 140 and 180 with a 30 ms lead: frame 0's layer arrives at 120 (late), frame 1's at 120 (in time)", + "clip_sha256": [ + "1bdf1771136b8f541afd56214d709a0f3e0dde069b41422c9003d0c29e18c771" + ] + }, + { + "name": "hold_bound_count", + "about": "a bound of one held envelope: the first layer is held for its base, the second is base-absent on arrival", + "clip_sha256": [ + "ac1fa9507d1004bcc1eaaf366da0013acd8c196dd5fdc2448232847348d75058" + ] + }, + { + "name": "step1_refusals", + "about": "a refinement-only envelope with no refines (refines-malformed), and one whose manifest names a fallback (capability-not-refinement)", + "clip_sha256": [ + "636def3e3165e6f3ee4d812c793c5c86cb5f10763a37cf86fcf8ed472a9c3299" + ] + }, + { + "name": "refines_rules", + "about": "§3's rules, each broken once: a layer 1 naming a producer other than the base; prev an array of one run; two adjacent runs naming one producer; runs not summing to the span's count. Each is refines-malformed", + "clip_sha256": [ + "27b600a505e95bb2bb0097854d98a42a0c77af1641c9090046412e52dd9fe7b2" + ] + }, + { + "name": "integer_spellings", + "about": "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", + "clip_sha256": [ + "029bb6e4dd92a6ee2f6a4a52ab01beaa2e6959b43833915d40dd808a251aad6b" + ] + }, + { + "name": "unreadable_envelopes", + "about": "envelopes a reader of SPEC §2 and §3 refuses, so a sink never reaches §4.1: a member named twice (in refines, and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media; an encrypted layer; then the layer with a manifest nested 127 deep, which is read and applies", + "clip_sha256": [ + "f0b1480ddabbfb085acd12f2697ce2f033169c8e34e1c044298c03ce975573a2" + ] + }, + { + "name": "refines_against_base", + "about": "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", + "clip_sha256": [ + "b1338ed308113d939238b0e97c91901908731b1c2a599bf010b688d4e30b383b" + ] + }, + { + "name": "wir1_faults", + "about": "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", + "clip_sha256": [ + "1d1f19c330ee7162da72c4af9c6f543d5faa4592e1afa441fe669e2f8ff348aa" + ] + }, + { + "name": "cost_over_limit_symbols", + "about": "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", + "clip_sha256": [ + "e4561786406a2ad52a46f3814fe516cd07745917fa5150b2b25b49f71218d2db" + ] + }, + { + "name": "cost_over_limit_working", + "about": "a sink whose cost limit is 100 working bytes: a layer that holds its tables, its stream index and one 192-byte frame is cost-over-limit (working bytes)", + "clip_sha256": [ + "08b88ef3b0bd8d6cd033ffe1dc09cb6e95bf6bd592ef7ac497fbf01b99d71fae" + ] + }, + { + "name": "chain_over_limit", + "about": "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", + "clip_sha256": [ + "b139902d21228247e2a7901db2077ca0e346c6f34c398b62be71d4516a1dcdf5" + ] + }, + { + "name": "chain_budget_retained", + "about": "a budget that holds the base and either layer, not both: layer 2, held, is retained, so layer 1 is chain-over-limit; once layer 2 expires and is dropped, layer 1 applies", + "clip_sha256": [ + "9b359fd5c3a4e4212911fdd0c0cbec31d00d46474656b49a655b4ef122bbded2" + ] + }, + { + "name": "budget_partly_applied", + "about": "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", + "clip_sha256": [ + "b74036d5d3befc48255321629b144b058dd516a33adb48bd65762b789ccad475" + ] + }, + { + "name": "frozen_refuses_held", + "about": "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", + "clip_sha256": [ + "98e342cc102fba975a75e08ca3e3fdfb83c271443b66516eb7b6c3ad35579c2c" + ] + }, + { + "name": "batch_conflict_release", + "about": "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", + "clip_sha256": [ + "6620d5f7e2d50dc37635c3881cd29a06cf6de2444320e0159a203fc600cc8aae" + ] + }, + { + "name": "layer_gap_on_arrival", + "about": "a bound of one held envelope: layer 2a is held for both units, and layer 2b, which would be held too, is refused on arrival (layer-gap)", + "clip_sha256": [ + "b3b18bbe90363440a4a88eeb7d89efe60201069bc91d67dc9c304f90e5e4cc4e" + ] + }, + { + "name": "held_then_prev_mismatch", + "about": "a layer 2 over layer 1a is held; layer 1b applies instead and releases it: the units' producer is 1b, so layer 2 is prev-mismatch", + "clip_sha256": [ + "83265622ac3f3f335c5d4df9231ba2c4ba27617722a2a9b446084740cee13fb2" + ] + }, + { + "name": "up2_geometry_mismatch", + "about": "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", + "clip_sha256": [ + "ef4e67d1cc2b756fb5221906fa60f561072ecc606f9b0c1f7002672814d22519" + ] + }, + { + "name": "rate_geometry_mismatch", + "about": "a rate layer to 600 samples reads 300; the base holds 400: geometry-mismatch", + "clip_sha256": [ + "aab0d06a5d8fc29f47bd107940bd88dfd7d588980bbb72df54d5b8e9a3222006" + ] + }, + { + "name": "rate_downsampling", + "about": "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", + "clip_sha256": [ + "c4edba1107169547cc6876a1b46a8a7526a29cc1ddac6698a92b5db51d0a902a" + ] + }, + { + "name": "deadline_held_layer", + "about": "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", + "clip_sha256": [ + "ee605579ccb95d90c626343ece2fd5e347647f588b73d48ccf01c7432b724bd2" + ] + }, + { + "name": "live_multi_base", + "about": "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", + "clip_sha256": [ + "aebc5f02725a6b51a50ed4f8eb29793bc14548f0e5bb79069aa1338c6eada40a", + "892935ee985334efbada8760d05d321b5f64e651ddd872af614885a21f487cab" + ] + }, + { + "name": "remove_readd_replay", + "about": "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", + "clip_sha256": [ + "fd2937f18c3732ca0fa24424bfaf5feedb1fbd7e95732fe2bbc32c41b71e62d0" + ] + }, + { + "name": "remove_readd_frozen", + "about": "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", + "clip_sha256": [ + "30836e3ff57924a86260a38c7031f85283cb6a93d581b535c790ad3b6797e75f" + ] + }, + { + "name": "remove_readd_batch_frozen", + "about": "layers 1a and 1b, held for the base, conflict in one batch and freeze the units with nothing applied; the chain is dropped and the base decoded again: the frozen flags are part of the record, so 1a is refused", + "clip_sha256": [ + "059c584015e4d63f5adbe512f820431e611ccb8ca31e661c66498bf5ee9047ce" + ] + }, + { + "name": "record_budget", + "about": "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + "clip_sha256": [ + "e46d8b6cce68ded46f9d5a32739d0b2e1edb39c9b744e6761e33e509afb9308f", + "7afc585abb5a585bc805778f132bd3267b28a40b8f2adceacaa82e9e37df0861", + null + ] + }, + { + "name": "remove_readd_diverge", + "about": "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", + "clip_sha256": [ + "0ffaf3f861041a87d543be9f70f90d805aacfe0a072f74deb7c16ae8415f96e0" + ] + }, + { + "name": "base_readded", + "about": "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", + "clip_sha256": [ + "11513b7b7c495cc504f4d6deb308d15c95c08aaca41e85479f2359764f49046d" + ] + }, + { + "name": "refused_on_release", + "about": "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", + "clip_sha256": [ + "f7efabb2f5641fb662d3e5fcbcd77c1a1b0106dd91d0f42c2d7a722f455dd1c1" + ] + }, + { + "name": "attested_after_decode", + "about": "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + "clip_sha256": [ + "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177", + "4008894e56d04cd74635889e6f4ac81b787bd0b35bf6d7681648457caa0ff177" + ] + }, + { + "name": "attested_across_drop", + "about": "an attested base's layer 1 applies with its receipt; the chain is dropped and the base decoded again by a decode that carries no receipt: it is still attested, so layer 1 with no receipt is refinement-unattested, and with its receipt it replays", + "clip_sha256": [ + "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e", + "7990406f958ddaefc5e91f8e0cd9ca1d265cd3c81a681eec4bb7dbb9ba04c92e" + ] + }, + { + "name": "attested_before_decode", + "about": "the sink verifies the base's receipt before it decodes the base: the decode, which carries no receipt, gives an attested base, so layer 1 with no receipt is refinement-unattested and applies with its receipt", + "clip_sha256": [ + "893d5537bffddf6524f550c8e283f52ae9db467d9da05c3f7cb2527ab2e49e22" + ] + }, + { + "name": "evidence_upgrade", + "about": "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", + "clip_sha256": [ + "97edd7b43a75c09b47bed54626f511065c1abba7e3dbfffe7aa3398eca890a7b" + ] + }, + { + "name": "evidence_upgrade_deadline", + "about": "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", + "clip_sha256": [ + "f9a7d9010bd6d8b13be120607274ca4ea0f5c57dee5a2fd7c177d293d92918f6" + ] + } + ] +} diff --git a/wai/staged-conformance/schedule.json b/wai/staged-conformance/schedule.json new file mode 100644 index 000000000..a0bbbcbc2 --- /dev/null +++ b/wai/staged-conformance/schedule.json @@ -0,0 +1,191 @@ +{ + "clock": { + "t0_ms": 0, + "unit_ms": 40, + "target_latency_ms": 120 + }, + "traces": [ + { + "base_ms": [ + 0, + 0, + 0, + 0 + ], + "layers_ms": [ + [ + 10, + 10, + 10, + 10 + ], + [ + 20, + 20, + 20, + 20 + ] + ], + "lead_ms": [ + 20, + 20 + ], + "presented": [ + { + "at_ms": 120, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 160, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 200, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 240, + "stage": 2, + "expected": 2 + } + ], + "deadline_misses": 0 + }, + { + "base_ms": [ + 0, + 0, + 0, + 0 + ], + "layers_ms": [ + [ + 110, + 110, + 110, + 110 + ], + [ + 130, + 130, + 170, + 240 + ] + ], + "lead_ms": [ + 20, + 20 + ], + "presented": [ + { + "at_ms": 120, + "stage": 0, + "expected": 2 + }, + { + "at_ms": 160, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 200, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 240, + "stage": 1, + "expected": 2 + } + ], + "deadline_misses": 2 + }, + { + "base_ms": [ + 0, + 50, + 200, + 210 + ], + "layers_ms": [ + [ + 0, + null, + 0, + 0 + ], + [ + 0, + 0, + null, + 300 + ] + ], + "lead_ms": [ + 10, + 10 + ], + "presented": [ + { + "at_ms": 120, + "stage": 2, + "expected": 2 + }, + { + "at_ms": 160, + "stage": 0, + "expected": 0 + }, + { + "at_ms": 200, + "stage": 1, + "expected": 1 + }, + { + "at_ms": 240, + "stage": 1, + "expected": 2 + } + ], + "deadline_misses": 1 + }, + { + "base_ms": [ + 130, + 130, + 130 + ], + "layers_ms": [ + [ + 100, + 140, + 190 + ] + ], + "lead_ms": [ + 0 + ], + "presented": [ + { + "at_ms": 130, + "stage": 1, + "expected": 1 + }, + { + "at_ms": 160, + "stage": 1, + "expected": 1 + }, + { + "at_ms": 200, + "stage": 1, + "expected": 1 + } + ], + "deadline_misses": 0 + } + ] +} diff --git a/wai/staged-conformance/staged_verify.py b/wai/staged-conformance/staged_verify.py new file mode 100644 index 000000000..350903489 --- /dev/null +++ b/wai/staged-conformance/staged_verify.py @@ -0,0 +1,1645 @@ +#!/usr/bin/env python3 +"""Independent verifier of the staged-delivery corpus (extensions/staged-delivery.md §12). + +Written from the extension text and SPEC §2, §3 and §7.1. It shares no code with +wai-rs, which generates the corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py +and the rANS decoder and the WIV1 keyframe-0 decode from +../int-payload-conformance/int_payload_verify.py, both written from their +specifications, and implements the rest: + +- a strict reader of envelope JSON (SPEC §2): UTF-8 with no byte-order mark, no member + named twice in an object, no NaN or Infinity, no number past the 64-bit float range, + no lone surrogate, at most 127 nested containers; `-0`, a fraction or an exponent is + not an integer; and the members SPEC §3 (WAI1) and §7.1 (WAI2) require, with their + types; +- `refines` and its rules (§3); +- WIR1: the parse in its stated order, the declared cost, the stream checks, the three + predictors and rdiv (Appendix A); +- the sink, as §4 states it: each envelope through §4.1's numbered steps in turn + (step 1, then the base or a hold for it, then steps 2–6, then steps 7 and 8 for each + unit), batches as §4.4 forms them, holds and their bound (§4.3), the chain budget + (§4.5), live deadlines on one clock across bases (§8) with a saturating clock, and + receipt evidence (§10); tiers and stage hashes (§5); +- base decodes: WIV1 keyframe 0, PNG (zlib and the five filters), FLAC (VERBATIM + subframes, with both CRCs); +- WAI2 refinement components (SPEC §7.1): the role checks, envelope manifests, each + entry's exploded envelope, and the envelopes a packer must refuse; +- the live schedule's presentation rule (schedule.json). + +For every case it starts from the case's stage0.bin, checks its SHA-256, decodes the base +independently where it can (WIV1 keyframe 0, PNG, FLAC) and compares, derives the base's +tier, replays every step, and requires every step's reports (one per envelope; the +order of the reports within a step is not part of the outcome) and every unit's final +stage, producer, frozen flag, tier, shape and stage hash to be the case's. + + python3 staged_verify.py + +Exit status 0 only if every case, every WAI2 file and the schedule verify. +""" +import hashlib +import json +import math +import os +import struct +import sys +import zlib + +HERE = os.path.dirname(os.path.abspath(__file__)) +WAI = os.path.normpath(os.path.join(HERE, "..")) +sys.path.insert(0, os.path.join(WAI, "claim-conformance")) +sys.path.insert(0, os.path.join(WAI, "int-payload-conformance")) +from _blake3 import blake3 # noqa: E402 +import int_payload_verify as IPV # noqa: E402 + +DE, EC, NONE = "decode-equivalence", "entropy-consistency", "none" +U64 = (1 << 64) - 1 + +# ---------------------------------------------------------------- strict JSON (SPEC §2) + +# The most containers a reader nests: the reference reader's limit, which an envelope +# that two readers must agree on stays within. +DEPTH_LIMIT = 127 + + +class NotJSON(ValueError): + pass + + +def strict_json(b): + """The JSON value of the bytes `b`, read strictly, or NotJSON.""" + if b[:3] == b"\xef\xbb\xbf": + raise NotJSON("a byte-order mark") + try: + text = b.decode("utf-8") + except UnicodeDecodeError as e: + raise NotJSON(f"not UTF-8: {e}") + + def members(pairs): + out = {} + for k, v in pairs: + if k in out: + raise NotJSON(f"the member {k!r} is named twice") + out[k] = v + return out + + def constant(name): + raise NotJSON(f"{name} is not a JSON number") + + def real(s): + f = float(s) + if math.isinf(f): + raise NotJSON(f"{s} is past the 64-bit float range") + return f + + def integer(s): + # `-0` is a number and not an integer; an integer past 64 bits is read as the + # float it rounds to, and is then no integer either. + if s == "-0": + return -0.0 + n = int(s) + if -(1 << 63) <= n <= U64: + return n + try: + return float(n) + except OverflowError: + raise NotJSON(f"{s[:20]}… is past the 64-bit float range") + + try: + v = json.loads(text, object_pairs_hook=members, parse_constant=constant, parse_float=real, parse_int=integer) + except NotJSON: + raise + except (ValueError, RecursionError) as e: + raise NotJSON(str(e)) + stack = [(v, 1)] + while stack: + x, depth = stack.pop() + if isinstance(x, (dict, list)): + if depth > DEPTH_LIMIT: + raise NotJSON("nested past the depth limit") + kids = list(x.items()) if isinstance(x, dict) else [(None, y) for y in x] + for k, y in kids: + if k is not None and any(0xD800 <= ord(c) <= 0xDFFF for c in k): + raise NotJSON("a lone surrogate") + stack.append((y, depth + 1)) + elif isinstance(x, str) and any(0xD800 <= ord(c) <= 0xDFFF for c in x): + raise NotJSON("a lone surrogate") + return v + + +def is_int(v): + return isinstance(v, int) and not isinstance(v, bool) + + +def is_num(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) + + +class Schema(ValueError): + pass + + +def need(o, k, ok, what, optional=False): + """Member `k` of the object `o`: present and `ok` (or, if `optional`, absent or null).""" + if k not in o or o[k] is None: + if optional: + return None + raise Schema(f"{k} is required ({what})") + if not ok(o[k]): + raise Schema(f"{k} is not {what}") + return o[k] + + +def a_str(v): + return isinstance(v, str) + + +def an_obj(v): + return isinstance(v, dict) + + +def a_u64(v): + return is_int(v) and 0 <= v <= U64 + + +def an_f32(v): + if not is_num(v): + return False + try: + struct.pack(" len(b): + raise NotWai1("truncated manifest") + text = b[8:8 + ml] + try: + m = strict_json(text) + check_manifest_v1(m) + except (NotJSON, Schema) as e: + raise NotWai1(str(e)) + pl = struct.unpack_from(" len(b): + raise NotWai1("truncated payload") + return m, text, b[12 + ml:12 + ml + pl] + + +def plaintext(m): + s = m.get("security") + return s is None or s.get("encryption") in (None, "none") + + +# ---------------------------------------------------------------- the registry (SPEC §5, §7) + + +# SPEC §5, Refinement capabilities and Base admission (the refinement rows). +UNIT_KIND = { + "wai.image.int_refine": "rgb8", + "wai.video.int_refine": "rgb8", + "wai.audio.int_refine": "pcm", + "wai.splat.int_refine": "attr", +} +ADMIT = { + "wai.image.int_refine": {"wai.neural.int_hyper", "wai.neural.int_mlicpp", "wai.neural.int_synth", "wai.image.png"}, + "wai.video.int_refine": {"wai.video.int_motion", "wai.video.int_hyper"}, + "wai.audio.int_refine": {"wai.audio.int_codec", "wai.audio.flac"}, + "wai.splat.int_refine": {"wai.splat.int_codec"}, +} +# The registry classes of the bases the corpus uses (SPEC §7). +CLASS = { + "wai.image.png": "StandardDefined", + "wai.audio.flac": "StandardDefined", + "wai.video.int_motion": "StandardDefined", + "wai.neural.int_hyper": "NeuralIntegerExact", + "wai.video.int_hyper": "NeuralIntegerExact", + "wai.audio.int_codec": "NeuralIntegerExact", + "wai.splat.int_codec": "NeuralIntegerExact", +} + + +def base_tier(cap, manifest, pin_resolved): + cls = CLASS.get(cap) + if cls == "StandardDefined": + return DE + if cls == "NeuralIntegerExact": + pin = (manifest.get("conditioning") or {}).get("prior") + return DE if pin_resolved and isinstance(pin, dict) and pin.get("tier") == DE else NONE + return NONE + + + + +# ---------------------------------------------------------------- refines (§3) + + +class Malformed(Exception): + pass + + +def is_hash(v): + return isinstance(v, str) and len(v) == 64 and all(c in "0123456789abcdef" for c in v) + + +def u32_int(v, least): + return is_int(v) and least <= v <= 0xFFFFFFFF + + +def parse_refines(r): + """§3, the rules a sink checks without the base. `None` (absent or null) is + malformed: a refinement-only capability carries `refines`.""" + if not isinstance(r, dict): + raise Malformed("not an object") + base = r.get("base") + if not is_hash(base): + raise Malformed("base") + layer = r.get("layer") + if not (is_int(layer) and 1 <= layer <= 255): + raise Malformed("layer") + span = None + if r.get("span") is not None: # null is absent (SPEC §3.1) + s = r["span"] + if not isinstance(s, dict): + raise Malformed("span") + if not u32_int(s.get("from"), 0) or not u32_int(s.get("count"), 1): + raise Malformed("span members") + if s["from"] + s["count"] > 0xFFFFFFFF: + raise Malformed("span past u32") + span = (s["from"], s["count"]) + p = r.get("prev") + if is_hash(p): + runs = [(p, None)] + elif isinstance(p, list) and p: + runs = [] + for x in p: + if not isinstance(x, dict) or not is_hash(x.get("producer")) or not u32_int(x.get("count"), 1): + raise Malformed("a run") + if runs and runs[-1][0] == x["producer"]: + raise Malformed("adjacent runs with one producer") + runs.append((x["producer"], x["count"])) + if len(runs) == 1: + raise Malformed("an array of one run") + else: + raise Malformed("prev") + if layer == 1 and any(h != base for h, _ in runs): + raise Malformed("layer 1 names a producer other than base") + if span is not None and runs[0][1] is not None and sum(c for _, c in runs) != span[1]: + raise Malformed("runs do not sum to the span") + return {"base": base, "layer": layer, "span": span, "runs": runs} + + +def units_of(ref, n): + return ref["span"] if ref["span"] is not None else (0, n) + + +def check_units(ref, n): + """§3's checks that need the base's unit count (§4.1 step 2).""" + frm, count = units_of(ref, n) + if frm + count > n or count == 0: + raise Malformed("the span lies outside the base's units") + if ref["runs"][0][1] is not None and sum(c for _, c in ref["runs"]) != count: + raise Malformed("runs do not sum to the units covered") + + +def producer_for(ref, i): + """P(u) for the i-th unit the layer covers.""" + if ref["runs"][0][1] is None: + return ref["runs"][0][0] + at = 0 + for h, c in ref["runs"]: + at += c + if i < at: + return h + return None + + +# ---------------------------------------------------------------- WIR1 (Appendix A) + + +class Refused(Exception): + def __init__(self, code): + super().__init__(code) + self.code = code + + +def parse_wir1(p, kind): + if p[:4] != b"WIR1": + raise Refused("bad_magic") + if len(p) < 5: + raise Refused("stream_length") + if p[4] != 1: + raise Refused("version") + if len(p) < 44: + raise Refused("stream_length") + if p[42] or p[43]: + raise Refused("reserved_nonzero") + elem, pred, mode = p[5], p[6], p[7] + n_units, d0, d1, d2, num, den, q = struct.unpack_from("<7I", p, 8) + offset = struct.unpack_from(" 1 << 26 or n_units > 1 << 20 or unit * n_units > 1 << 30: + raise Refused("too_large") + at = 44 + tables = [] + for _ in range(n_units if mode == 1 else 1): + if at + 4 * cdf_len > len(p): + raise Refused("stream_length") + t = list(struct.unpack_from(f"<{cdf_len}I", p, at)) + at += 4 * cdf_len + try: + IPV.check_cdf(t) + except IPV.Refused: + raise Refused("cdf_malformed") + tables.append(t) + lens = [] + for _ in range(n_units): + if at + 4 > len(p): + raise Refused("stream_length") + n = struct.unpack_from(" len(p): + raise Refused("stream_length") + streams.append(p[at:at + n]) + at += n + if at != len(p): + raise Refused("trailing") + return dict(kind=kind, pred=pred, per_unit=mode == 1, n_units=n_units, d=(d0, d1, d2), num=num, den=den, + q=q, offset=offset, tables=tables, streams=streams) + + +def table(w, u): + return w["tables"][u] if w["per_unit"] else w["tables"][0] + + +def check_streams(w): + n = w["d"][0] * w["d"][1] * w["d"][2] + for u, s in enumerate(w["streams"]): + d = IPV.Rans(s) + for _ in range(n): + d.decode(table(w, u), w["offset"]) + if d.exhausted: + raise Refused("stream_exhausted") + if d.exhausted or d.i != len(d.w) or d.x != 1 << 31: + raise Refused("stream_noncanonical") + + +def cost(w): + unit = w["d"][0] * w["d"][1] * w["d"][2] + eb = 2 if w["kind"] == "pcm" else 1 + # Working bytes: the parsed tables, the index of the streams (8 bytes a unit) and one + # unit's output (A.5). + return (unit * eb * w["n_units"], unit * w["n_units"], sum(4 * len(t) for t in w["tables"]) + 8 * w["n_units"] + unit * eb) + + +def rdiv(n, d): + q = (2 * abs(n) + d) // (2 * d) + return -q if n < 0 else q + + +def accepts_below(w, shape): + d0, d1, d2 = w["d"] + if w["pred"] == 0: + return shape == (d0, d1, d2) + if w["pred"] == 1: + return shape == (-(-d0 // 2), -(-d1 // 2), d2) + n = shape[0] + return shape[1] == 1 and shape[2] == d2 and n >= 1 and n * w["num"] // w["den"] == d0 + + +def predictor(w, prev, shape): + d0, d1, d2 = w["d"] + if w["pred"] == 0: + return lambda i: prev[i] + if w["pred"] == 1: + h2, w2, c = shape + + def up2(i): + y, x, ch = i // (d1 * d2), (i // d2) % d1, i % d2 + + def p(yy, xx): + return prev[(min(yy, h2 - 1) * w2 + min(xx, w2 - 1)) * c + ch] + iy, fy, ix, fx = y >> 1, y & 1, x >> 1, x & 1 + if not fy and not fx: + return p(iy, ix) + if not fy: + return (p(iy, ix) + p(iy, ix + 1) + 1) >> 1 + if not fx: + return (p(iy, ix) + p(iy + 1, ix) + 1) >> 1 + return (p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2 + return up2 + num, den, n_src = w["num"], w["den"], shape[0] + + def rate(i): + t, ch = i // d2, i % d2 + s = t * den + lo, rho = s // num, s % num + hi = min(lo + 1, n_src - 1) + a, b = prev[lo * d2 + ch], prev[hi * d2 + ch] + return rdiv(a * num + (b - a) * rho, num) + return rate + + +def apply_unit(w, u, prev, shape): + """The unit's next stage (the shape was checked).""" + n = w["d"][0] * w["d"][1] * w["d"][2] + lo, hi = (-32768, 32767) if w["kind"] == "pcm" else (0, 255) + pred = predictor(w, prev, shape) + d = IPV.Rans(w["streams"][u]) + out = [] + for i in range(n): + r = d.decode(table(w, u), w["offset"]) + out.append(min(max(pred(i) + r * w["q"], lo), hi)) + return out + + + +# ---------------------------------------------------------------- the sink (§4, §5, §8, §10) + +# §4.2, in its order. +REASONS = ["refines-malformed", "capability-not-refinement", "base-absent", "base-via-fallback", "base-not-admitted", + "refinement-unattested", "payload-malformed", "cost-over-limit", "chain-over-limit", "layer-conflict", + "prev-mismatch", "layer-gap", "geometry-mismatch", "deadline-missed"] +EVIDENCE = {"none": 0, "inclusion-verified": 1, "delivered-verified": 2} # §10, weakest first + + +class Outcome: + """What one offer, base decode or expiry decided, for each envelope it touched.""" + + def __init__(self): + self.by = {} + + def of(self, h): + if h not in self.by: + self.by[h] = {"envelope": None, "detail": None, "held_for_base": False, "applied": [], "held": [], "refused": []} + return self.by[h] + + def refuse(self, h, why, detail=None): + r = self.of(h) + r["envelope"], r["detail"] = why, detail + + def reports(self): + """One report per envelope: its units in order; a unit held and then decided in + the same step is reported by its decision.""" + out = [] + for h, r in self.by.items(): + decided = set(r["applied"]) | {u for u, _ in r["refused"]} + out.append({"object": h, "envelope": r["envelope"], "detail": r["detail"], "held_for_base": r["held_for_base"], + "applied": sorted(r["applied"]), "held": sorted({u for u in r["held"] if u not in decided}), + "refused": [[u, w] for u, w in sorted(r["refused"], key=lambda x: (x[0], REASONS.index(x[1])))]}) + return out + + +class Envelope: + """A received envelope that passed §4.1 step 1.""" + + def __init__(self, h, cap, ref, payload, at, evidence): + self.h, self.cap, self.ref, self.payload = h, cap, ref, payload + self.arrival, self.evidence, self.verified = at, evidence, at + self.w = None # its payload, parsed at step 6 + self.applied = 0 # the units it has been applied to (§4.5) + + def p(self, u, n): + """P(u): the producer `prev` gives for unit u of a base of n units.""" + return producer_for(self.ref, u - units_of(self.ref, n)[0]) + + +def unit_bytes(s): + return len(s["data"]) * (2 if s["elem"] == "i16" else 1) + + +class Base: + """A decoded base and its units (§1), with what §4 keeps for them.""" + + def __init__(self, info, units, cfg, record=None): + self.info, self.cfg = dict(info), cfg + self.units = [dict(stage=0, producer=info["hash"], frozen=False, tier=info["tier"], shape=s, elem=e, data=list(d), reached=0) + for (s, e, d) in units] + self.passed = {} # step 7's record: (u, layer, P(u)) -> the envelope that passed step 8 for u + if record is not None: # §4.4: a base decoded again starts at stage 0 with its record + self.passed = dict(record["passed"]) + for u, (frozen, reached) in record["units"].items(): + if u < len(self.units): + self.units[u]["frozen"], self.units[u]["reached"] = frozen, reached + self.held = set() # (envelope hash, u): u held for the envelope (step 8) + self.retained = {} # envelope hash -> Envelope, from step 6 to when no unit is held for it (§4.5) + + def n(self): + return len(self.units) + + def held_envelopes(self): + return {h for h, _ in self.held} + + def state(self): + """§4.5: units' output bytes and 32 bytes per stage their record holds (the + highest stage each reached); retained layers' payload bytes and declared working + bytes, and, for each unit a layer covers and has not been applied to, one unit's + output and 32 bytes.""" + s = sum(unit_bytes(x) + 32 * x["reached"] for x in self.units) + for e in self.retained.values(): + n = e.w["n_units"] + s += len(e.payload) + e.cost[2] + (n - e.applied) * (e.cost[0] // n + 32) + return s + + # -- §4.1 steps 2 to 6, for an envelope whose base this is + + def steps_2_to_6(self, e): + """The envelope's reason and detail, or None and the envelope retained.""" + try: + check_units(e.ref, self.n()) # step 2 + except Malformed: + return ("refines-malformed", None) + if self.info["via_fallback"]: # step 3 + return ("base-via-fallback", None) + if self.info["capability"] not in ADMIT[e.cap]: # step 4 + return ("base-not-admitted", None) + if self.info["attested"] and e.evidence != "delivered-verified": # step 5 + return ("refinement-unattested", None) + try: # step 6 + w = parse_wir1(e.payload, UNIT_KIND[e.cap]) + except Refused as x: + return ("payload-malformed", x.code) + if w["n_units"] != units_of(e.ref, self.n())[1]: + return ("payload-malformed", "field_range") + c = cost(w) + lim = self.cfg["cost_limit"] + if lim is not None: + for got, key, what in zip(c, ("output_bytes", "symbols", "working_bytes"), ("output bytes", "symbols", "working bytes")): + if got > lim[key]: + return ("cost-over-limit", what) + if self.state() + len(e.payload) + c[0] + c[2] + 32 * w["n_units"] > self.cfg["budget"]: + return ("chain-over-limit", None) + try: + check_streams(w) + except Refused as x: + return ("payload-malformed", x.code) + e.w, e.cost = w, c + # §8, §10: over an attested base a layer counts as arrived when its receipt was verified. + e.when = e.verified if self.info["attested"] else e.arrival + self.retained[e.h] = e + return None + + # -- steps 7 and 8, for unit u + + def conflicted(self, e, u): + """Step 7 against what passed step 8 before.""" + s = self.units[u] + if s["frozen"]: + # §4.4: a frozen unit replays its own record, in order, never past it. + k, p = e.ref["layer"], e.p(u, self.n()) + replay = k == s["stage"] + 1 and k <= s["reached"] and p == s["producer"] and self.passed.get((u, k, p)) == e.h + return not replay + other = self.passed.get((u, e.ref["layer"], e.p(u, self.n()))) + return other is not None and other != e.h + + def readiness(self, e, u): + """Step 8.""" + s, want = self.units[u], e.ref["layer"] - 1 + if s["stage"] == want and s["producer"] == e.p(u, self.n()): + return "ready" + return "held" if s["stage"] < want else "not" + + def freeze(self, u, out): + """Step 7: no further layer for u; every envelope held for it is refused for it.""" + self.units[u]["frozen"] = True + for h, x in sorted(self.held): + if x == u: + self.held.discard((h, x)) + out.of(h)["refused"].append((u, "layer-conflict")) + + # -- steps 9 and 10, and the next stage + + def steps_9_10_apply(self, e, u): + s, w = self.units[u], e.w + if (s["elem"] == "i16") != (w["kind"] == "pcm") or len(s["data"]) != s["shape"][0] * s["shape"][1] * s["shape"][2] \ + or not accepts_below(w, s["shape"]): + return "geometry-mismatch" + live = self.cfg["live"] + if live is not None: + leads = live["lead_ms"] + lead = leads[e.ref["layer"] - 1] if e.ref["layer"] - 1 < len(leads) else 0 + position = self.info["first_unit"] + u + due = min(live["t0_ms"] + position * live["unit_ms"] + live["target_latency_ms"], U64) + if e.when + lead > min(due, U64): + return "deadline-missed" + s["data"] = apply_unit(w, u - units_of(e.ref, self.n())[0], s["data"], s["shape"]) + s["shape"] = w["d"] + s["stage"] = e.ref["layer"] + s["reached"] = max(s["reached"], s["stage"]) + s["producer"] = e.h + s["tier"] = DE if s["tier"] == DE else NONE # a layer acts on reconstructed output (§5) + e.applied += 1 + return None + + # -- §4.4 + + def receive(self, envelopes, out, held_elsewhere, on_arrival): + """Envelopes offered on arrival (one) or released when this base was decoded + (in arrival order): each passes steps 2 to 6, and for each unit step 7 against + what passed step 8 earlier and step 8, alone; the ready pairs then form a batch.""" + ready = [] + for e in envelopes: + if e.h in self.retained: + e = self.retained[e.h] # §4.5: steps 2 to 6 passed, and it adds nothing + else: + why = self.steps_2_to_6(e) + if why is not None: + out.refuse(e.h, *why) + continue + r = out.of(e.h) + already_held = e.h in self.held_envelopes() + waiting = [] + frm, count = units_of(e.ref, self.n()) + for u in range(frm, frm + count): + if self.conflicted(e, u): + r["refused"].append((u, "layer-conflict")) + self.freeze(u, out) + continue + k = self.readiness(e, u) + if k == "ready": + ready.append((e, u)) + elif k == "held": + waiting.append(u) + else: + r["refused"].append((u, "prev-mismatch")) + if not waiting: + continue + # §4.3: an envelope that would pass the bound's count is refused on arrival. + if on_arrival and not already_held and len(self.held_envelopes()) + held_elsewhere >= self.cfg["bound"]["max_envelopes"]: + r["refused"] += [(u, "layer-gap") for u in waiting] + else: + for u in waiting: + self.held.add((e.h, u)) + r["held"].append(u) + self.batches(ready, out) + self.drop_unheld() + + def batches(self, batch, out): + while batch: + n = self.n() + keys = {} + for e, u in batch: + keys.setdefault((u, e.ref["layer"], e.p(u, n)), set()).add(e.h) + advanced = [] + go = [] + for e, u in batch: + # Step 7 over the batch: another pair of it with the same key and other content. + if self.conflicted(e, u) or len(keys[(u, e.ref["layer"], e.p(u, n))]) > 1: + out.of(e.h)["refused"].append((u, "layer-conflict")) + self.freeze(u, out) + else: + go.append((e, u)) + for e, u in go: + self.passed[(u, e.ref["layer"], e.p(u, n))] = e.h + why = self.steps_9_10_apply(e, u) + if why is None: + out.of(e.h)["applied"].append(u) + advanced.append(u) + else: + out.of(e.h)["refused"].append((u, why)) + # The envelopes held for a unit whose stage advanced are released together. + batch = [] + for u in advanced: + for h, x in sorted(self.held): + if x != u or (h, x) not in self.held: + continue + self.held.discard((h, x)) + e = self.retained[h] + if self.conflicted(e, u): + out.of(h)["refused"].append((u, "layer-conflict")) + self.freeze(u, out) + continue + k = self.readiness(e, u) + if k == "ready": + batch.append((e, u)) + elif k == "held": + self.held.add((h, u)) + else: + out.of(h)["refused"].append((u, "prev-mismatch")) + + def drop_unheld(self): + """§4.5: the end of an offer, a release or an expiry drops what no unit holds.""" + keep = self.held_envelopes() + self.retained = {h: e for h, e in self.retained.items() if h in keep} + + def expire(self, now, out): + for h, u in sorted(self.held): + if max(now - self.retained[h].arrival, 0) > self.cfg["bound"]["max_age_ms"]: + self.held.discard((h, u)) + out.of(h)["refused"].append((u, "layer-gap")) + self.drop_unheld() + + def unit_sha(self, u): + s = self.units[u] + if s["tier"] != DE: + return None + return hashlib.sha256(canonical(s["elem"], s["data"])).hexdigest() + + def clip_sha(self): + hs = [self.unit_sha(u) for u in range(self.n())] + if any(x is None for x in hs): + return None + return hashlib.sha256(b"".join(bytes.fromhex(x) for x in hs)).hexdigest() + + +def canonical(elem, data): + return struct.pack(f"<{len(data)}h", *data) if elem == "i16" else bytes(data) + + +class Sink: + """The bases a sink decoded, and the envelopes it holds for bases it has not.""" + + def __init__(self, cfg): + self.cfg, self.bases, self.waiting = cfg, {}, [] + # §4.4: what the sink keeps of each base it does not hold (its units' records and + # its attestation), with when it was last written, within the record budget. + self.kept = {} + self.clock = 0 + + def held_elsewhere(self, base): + return len(self.waiting) + sum(len(b.held_envelopes()) for k, b in self.bases.items() if k != base) + + def offer(self, env, at, evidence): + """§4.1 for one received envelope. NotWai1 or Encrypted when the bytes are not a + WAI1 this sink reads (an encrypted layer is decrypted before step 6, §14).""" + h = content_hash(env) + m, _, payload = read_wai1(env) + if not plaintext(m): + raise Encrypted("encrypted") + out = Outcome() + try: # step 1 + ref = parse_refines(m.get("refines")) + except Malformed: + out.refuse(h, "refines-malformed") + return out.reports() + mr = m["model_requirement"] + if mr["capability"] not in UNIT_KIND or mr.get("fallback") is not None: + out.refuse(h, "capability-not-refinement") + return out.reports() + e = Envelope(h, mr["capability"], ref, payload, at, evidence) + base = self.bases.get(ref["base"]) + if base is not None: # step 2 + base.receive([e], out, self.held_elsewhere(ref["base"]), True) + return out.reports() + # Held for its base (§4.3), once; stronger receipt evidence is kept. + same = next((x for x in self.waiting if x.h == h), None) + if same is not None: + if EVIDENCE[evidence] > EVIDENCE[same.evidence]: + same.evidence, same.verified = evidence, at + out.of(h)["held_for_base"] = True + return out.reports() + if self.held_elsewhere(None) >= self.cfg["bound"]["max_envelopes"]: + out.refuse(h, "base-absent") + return out.reports() + self.waiting.append(e) + out.of(h)["held_for_base"] = True + return out.reports() + + def add_base(self, info, units): + """A decoded base: the envelopes held for it are released together (§4.4). A + base this sink holds already is kept as it is (§4.4), except that a receipt + verified for it since makes it attested from then on (§10), never the reverse.""" + if info["hash"] in self.bases: + if info["attested"]: + self.bases[info["hash"]].info["attested"] = True + return [] + # Attestation belongs to the base hash: a base attested before stays attested. + k = self.kept.pop(info["hash"], None) + info = dict(info, attested=info["attested"] or (k is not None and k["attested"])) + b = self.bases[info["hash"]] = Base(info, units, self.cfg, k) + mine = [e for e in self.waiting if e.ref["base"] == info["hash"]] + self.waiting = [e for e in self.waiting if e.ref["base"] != info["hash"]] + out = Outcome() + if mine: + b.receive(mine, out, 0, False) + return out.reports() + + def remove_base(self, h): + """§4.4: the sink drops a base's chain. Every envelope held for its units is + discarded (base-absent); each unit's record is kept for the base decoded again.""" + b = self.bases.pop(h, None) + if b is None: + return [] + out = Outcome() + for e in sorted(b.held_envelopes()): + out.refuse(e, "base-absent") + # A unit's record is empty when it reached no stage, nothing passed step 8 for + # it and it is not frozen; only the others are kept. + units = {} + for u, x in enumerate(b.units): + passed_here = any(k[0] == u for k in b.passed) + if x["reached"] or x["frozen"] or passed_here: + units[u] = (x["frozen"], x["reached"]) + passed = {k: v for k, v in b.passed.items() if k[0] in units} + self.keep(h, {"units": units, "passed": passed, "attested": b.info["attested"]}) + return out.reports() + + def mark_attested(self, h): + """§10: the sink verified the base's delivery receipt, whether it holds the + base, dropped it, or has not decoded it yet.""" + if h in self.bases: + self.bases[h].info["attested"] = True + else: + k = self.kept.pop(h, None) or {"units": {}, "passed": {}, "attested": False} + k["attested"] = True + self.keep(h, k) + return [] + + @staticmethod + def kept_size(k): + """§4.4: 32 bytes for the base, and for each kept unit 8, 32 a recorded stage and + 32 for a producer that passed step 8 and was not applied.""" + n = 32 + for u, (_, reached) in k["units"].items(): + n += 8 + 32 * reached + (32 if any(key[0] == u and key[1] == reached + 1 for key in k["passed"]) else 0) + return n + + def keep(self, h, k): + """Keep what is not empty, written now; then, past the record budget, forget the + bases written longest ago, whole (§4.4, §14).""" + if not k["units"] and not k["attested"]: + return + self.clock += 1 + k["written"] = self.clock + self.kept[h] = k + while sum(self.kept_size(x) for x in self.kept.values()) > self.cfg["record_budget"]: + oldest = min(self.kept, key=lambda x: self.kept[x]["written"]) + del self.kept[oldest] + + def expire(self, now): + """§4.3: what was held past the bound's duration.""" + out = Outcome() + age = self.cfg["bound"]["max_age_ms"] + for e in [x for x in self.waiting if max(now - x.arrival, 0) > age]: + out.refuse(e.h, "base-absent") + self.waiting = [x for x in self.waiting if not max(now - x.arrival, 0) > age] + for k in sorted(self.bases): + self.bases[k].expire(now, out) + return out.reports() + + +# ---------------------------------------------------------------- base decodes + + +def paeth(a, b, c): + p = a + b - c + pa, pb, pc = abs(p - a), abs(p - b), abs(p - c) + return a if pa <= pb and pa <= pc else (b if pb <= pc else c) + + +def decode_png(b): + if b[:8] != b"\x89PNG\r\n\x1a\n": + raise ValueError("not a PNG") + at, idat, ihdr = 8, b"", None + while at < len(b): + n = struct.unpack_from(">I", b, at)[0] + kind, data = b[at + 4:at + 8], b[at + 8:at + 8 + n] + if zlib.crc32(kind + data) != struct.unpack_from(">I", b, at + 8 + n)[0]: + raise ValueError("a chunk CRC") + if kind == b"IHDR": + ihdr = struct.unpack(">IIBBBBB", data) + elif kind == b"IDAT": + idat += data + at += 12 + n + w, h, depth, ctype = ihdr[0], ihdr[1], ihdr[2], ihdr[3] + if (depth, ctype) != (8, 2): + raise ValueError("only 8-bit RGB is read here") + raw, stride = zlib.decompress(idat), w * 3 + out = bytearray() + prev = bytearray(stride) + for y in range(h): + ft, row = raw[y * (stride + 1)], bytearray(raw[y * (stride + 1) + 1:(y + 1) * (stride + 1)]) + for i in range(stride): + a = row[i - 3] if i >= 3 else 0 + c = prev[i - 3] if i >= 3 else 0 + pr = [0, a, prev[i], (a + prev[i]) // 2, paeth(a, prev[i], c)][ft] + row[i] = (row[i] + pr) & 255 + out += row + prev = row + return (h, w, 3), list(out) + + +class BitReader: + def __init__(self, b, at): + self.b, self.bit = b, at * 8 + + def get(self, n): + v = 0 + for _ in range(n): + v = (v << 1) | ((self.b[self.bit >> 3] >> (7 - (self.bit & 7))) & 1) + self.bit += 1 + return v + + +def crc8(data): + c = 0 + for x in data: + c ^= x + for _ in range(8): + c = ((c << 1) ^ 0x07) & 0xFF if c & 0x80 else (c << 1) & 0xFF + return c + + +def crc16(data): + c = 0 + for x in data: + c ^= x << 8 + for _ in range(8): + c = ((c << 1) ^ 0x8005) & 0xFFFF if c & 0x8000 else (c << 1) & 0xFFFF + return c + + +def decode_flac(b): + """FLAC with 16-bit independent channels and VERBATIM subframes, both CRCs checked.""" + if b[:4] != b"fLaC": + raise ValueError("not FLAC") + at, last, info = 4, False, None + while not last: + last, kind = bool(b[at] & 0x80), b[at] & 0x7F + n = int.from_bytes(b[at + 1:at + 4], "big") + if kind == 0: + r = BitReader(b, at + 4) + r.get(16), r.get(16), r.get(24), r.get(24) + info = dict(rate=r.get(20), ch=r.get(3) + 1, bps=r.get(5) + 1, total=r.get(36)) + at += 4 + n + if info["bps"] != 16: + raise ValueError("only 16 bits per sample are read here") + ch, out, total = info["ch"], [], 0 + while total < info["total"]: + r = BitReader(b, at) + if r.get(14) != 0b11111111111110 or r.get(1) or r.get(1): + raise ValueError("frame sync") + bs_code, sr_code, chan, ss, _ = r.get(4), r.get(4), r.get(4), r.get(3), r.get(1) + if chan != ch - 1 or ss != 0b100 or sr_code != 0 or bs_code != 0b0111: + raise ValueError("a frame header this reader does not read") + r.get(8) # a one-byte frame number + length = r.get(16) + 1 + hdr_end = r.bit >> 3 + if crc8(b[at:hdr_end]) != r.get(8): + raise ValueError("header CRC-8") + chans = [] + for _ in range(ch): + if r.get(1) or r.get(6) != 1 or r.get(1): + raise ValueError("not a VERBATIM subframe") + chans.append([(lambda v: v - 65536 if v >= 32768 else v)(r.get(16)) for _ in range(length)]) + r.bit = (r.bit + 7) & ~7 + end = r.bit >> 3 + if crc16(b[at:end]) != int.from_bytes(b[end:end + 2], "big"): + raise ValueError("frame CRC-16") + for i in range(length): + out += [chans[c][i] for c in range(ch)] + total += length + at = end + 2 + return (info["total"], 1, ch), out + + + +# ---------------------------------------------------------------- WAI2 identity (SPEC §7.1) + +WS = " \t\n\r" + + +def skip(t, i): + while i < len(t) and t[i] in WS: + i += 1 + return i + + +def node(t, i): + """The JSON value at `i`: (start, end, members) with members a dict for an object, + a list for an array, None otherwise. Spans index `t`.""" + i = skip(t, i) + s, c = i, t[i] + if c == "{": + kids, i = {}, skip(t, i + 1) + if t[i] == "}": + return (s, i + 1, kids) + while True: + k = node(t, i) + i = skip(t, k[1]) + assert t[i] == ":" + v = node(t, i + 1) + kids[json.loads(t[k[0]:k[1]])] = v + i = skip(t, v[1]) + if t[i] == ",": + i += 1 + continue + assert t[i] == "}" + return (s, i + 1, kids) + if c == "[": + kids, i = [], skip(t, i + 1) + if t[i] == "]": + return (s, i + 1, kids) + while True: + v = node(t, i) + kids.append(v) + i = skip(t, v[1]) + if t[i] == ",": + i += 1 + continue + assert t[i] == "]" + return (s, i + 1, kids) + if c == '"': + j = i + 1 + while t[j] != '"': + j += 2 if t[j] == "\\" else 1 + return (s, j + 1, None) + j = i + while j < len(t) and t[j] not in ",}] \t\n\r": + j += 1 + return (s, j, None) + + +class NotWai2(ValueError): + pass + + +def read_wai2(b): + """A WAI2 envelope (SPEC §7.1): its manifest text, its entries and their payloads. + The manifest is read strictly (SPEC §2) and its members checked; the table has one + entry per rendition, contiguous from offset 0, within the bytes.""" + if len(b) < 8 or b[:4] != b"WAI2": + raise NotWai2("not WAI2") + ml = struct.unpack_from(" len(b): + raise NotWai2("truncated manifest") + try: + m = strict_json(b[8:8 + ml]) + check_manifest_v2(m) + except (NotJSON, Schema) as e: + raise NotWai2(str(e)) + text = b[8:8 + ml].decode("utf-8") + at = 8 + ml + if at + 2 > len(b): + raise NotWai2("truncated table") + n = struct.unpack_from(" len(b): + raise NotWai2("truncated table") + start, expect, payloads = at + 8 * n, 0, [] + for i in range(n): + off, ln = struct.unpack_from(" len(b): + raise NotWai2("a rendition outside the payload block") + payloads.append(b[start + off:start + off + ln]) + expect += ln + return text, m["renditions"], payloads + + +def parse_wai2(b): + text, _, payloads = read_wai2(b) + return text, payloads + + +def is_component_id(s): + return isinstance(s, str) and 1 <= len(s) <= 64 and all(c in "abcdefghijklmnopqrstuvwxyz0123456789_-" for c in s) + + +def parse_refines_ids(r): + """`refines` with component ids in place of hashes.""" + if not isinstance(r, dict): + raise Malformed("not an object") + swap = {} + + def h(x): + if not is_component_id(x): + raise Malformed("not a component id") + return swap.setdefault(x, hashlib.sha256(x.encode()).hexdigest()) + rr = dict(r) + rr["base"] = h(r.get("base")) if isinstance(r.get("base"), str) else None + p = r.get("prev") + if isinstance(p, str): + rr["prev"] = h(p) + elif isinstance(p, list): + rr["prev"] = [dict(x, producer=h(x.get("producer"))) if isinstance(x, dict) and isinstance(x.get("producer"), str) else x for x in p] + out = parse_refines(rr) + back = {v: k for k, v in swap.items()} + out["base"] = back[out["base"]] + out["runs"] = [(back[x], c) for x, c in out["runs"]] + return out + + +def refines_json(ref, tr): + o = {"base": tr(ref["base"])} + if ref["runs"][0][1] is None: + o["prev"] = tr(ref["runs"][0][0]) + else: + o["prev"] = [{"producer": tr(x), "count": c} for x, c in ref["runs"]] + o["layer"] = ref["layer"] + if ref["span"] is not None: + o["span"] = {"from": ref["span"][0], "count": ref["span"][1]} + return o + + +def wai1(manifest, payload): + return b"WAI1" + struct.pack(" DEPTH_LIMIT: + return False + if m["model_requirement"]["capability"] in UNIT_KIND: + try: + ref = parse_refines(m.get("refines")) + except Malformed: + return False + if ref["base"] not in hashes or any(p not in hashes for p, _ in ref["runs"]): + return False + if m["model_requirement"].get("fallback") is not None: + return False + hashes.append(content_hash(env)) + return True + + +def wai2_expect(b): + """The four checks wai2/index.json states for a WAI2.""" + _, entries, _ = read_wai2(b) + ex = explode_each(b) + return {"roles": role_check(entries), "manifests": manifests_check(b), + "exploded": [None if e is None else content_hash(e) for e in ex], "refinements_of_0": refinements_of_0(b)} + + +def verify_wai2(d, fails): + idx = json.load(open(os.path.join(d, "wai2", "index.json"))) + for f in idx["files"]: + b = open(os.path.join(d, "wai2", f["file"]), "rb").read() + got = wai2_expect(b) + if got != f["expect"]: + fails.append(f"wai2/{f['file']}") + print(f"FAIL wai2/{f['file']}: {got} != {f['expect']}") + srcs = [open(os.path.join(d, "wai2", s), "rb").read() for s in f["sources"]] + if srcs and explode_each(b) != srcs: + fails.append(f"wai2/{f['file']} sources") + print(f"FAIL wai2/{f['file']}: does not explode to its sources") + for p in idx["packs"]: + srcs = [open(os.path.join(d, "wai2", s), "rb").read() for s in p["sources"]] + if packs(srcs) != p["packs"]: + fails.append(f"wai2 pack {p['name']}") + print(f"FAIL wai2 pack {p['name']}: packs is {packs(srcs)}, the index states {p['packs']}") + return len(idx["files"]) + len(idx["packs"]) + + +# ---------------------------------------------------------------- the schedule (§8) + + +def present(clock, base_ms, layers, lead): + """What a sink presents for a trace: each unit at its presentation time (or when its + base arrives, if later), at the stage of the run of layers that arrived in time. The + clock saturates at 2^64 − 1.""" + out = [] + for u, b in enumerate(base_ms): + due = min(clock["t0_ms"] + u * clock["unit_ms"] + clock["target_latency_ms"], U64) + stage = expected = 0 + in_time = True + for li, arr in enumerate(layers): + t = arr[u] if u < len(arr) else None + if t is None: + break + expected += 1 + t_lead = t + (lead[li] if li < len(lead) else 0) + in_time = in_time and t_lead <= U64 and t_lead <= due + if in_time: + stage += 1 + out.append({"at_ms": max(due, b), "stage": stage, "expected": expected}) + return out + + +# ---------------------------------------------------------------- cases + + +def load_case(cdir): + """A case's bases (info and stage-0 units), its envelopes, config and steps.""" + case = json.load(open(os.path.join(cdir, "case.json"))) + bases = [] + for b in case["bases"]: + env = open(os.path.join(cdir, b["file"]), "rb").read() + s0 = open(os.path.join(cdir, b["stage0"]), "rb").read() + units, at = [], 0 + for u in b["units"]: + shape, elem = tuple(u["shape"]), u["elem"] + n = shape[0] * shape[1] * shape[2] + if elem == "u8": + data = list(s0[at:at + n]) + at += n + else: + data = list(struct.unpack_from(f"<{n}h", s0, at)) + at += 2 * n + units.append((shape, elem, data)) + bases.append({"hash": content_hash(env), "capability": b["decoded_via"], "via_fallback": b["via_fallback"], + "attested": b["attested"], "tier": b["tier"], "first_unit": b["first_unit"], "units": units, + "env": env, "s0": s0, "spec": b}) + files = {f["file"]: open(os.path.join(cdir, f["file"]), "rb").read() for f in case["files"]} + return case, bases, files + + +def replay(case, bases, files): + """Every step's reports and every base's final units, as a sink of this verifier + reaches them. An envelope that is not a WAI1 this sink reads is the step's error.""" + sink = Sink(case["config"]) + got = [] + for st in case["steps"]: + if st["op"] == "offer": + try: + got.append(sink.offer(files[st["file"]], st["at_ms"], st["evidence"])) + except Encrypted: + got.append({"error": "encrypted"}) + except NotWai1: + got.append({"error": "not-wai1"}) + elif st["op"] == "base": + b = bases[st["index"]] + got.append(sink.add_base(b, b["units"])) + elif st["op"] == "remove": + got.append(sink.remove_base(bases[st["index"]]["hash"])) + elif st["op"] == "attest": + got.append(sink.mark_attested(bases[st["index"]]["hash"])) + else: + got.append(sink.expire(st["at_ms"])) + finals = [] + for b in bases: + c = sink.bases.get(b["hash"]) + if c is None: + finals.append(None) + continue + finals.append({"units": [{"stage": s["stage"], "producer": s["producer"], "frozen": s["frozen"], "tier": s["tier"], + "shape": list(s["shape"]), "stage_sha256": c.unit_sha(u)} for u, s in enumerate(c.units)], + "clip_sha256": c.clip_sha()}) + return got, finals + + +def replay_dir(cdir): + """A case directory replayed: {"reports": [...], "finals": [...]}, each step's + reports in object order.""" + case, bases, files = load_case(cdir) + got, finals = replay(case, bases, files) + return {"reports": [sorted(g, key=lambda r: r["object"]) if isinstance(g, list) else g for g in got], "finals": finals} + + +VECTORS = os.path.join(WAI, "wai-rs", "tests", "vectors") +VECTOR_DIGESTS = {} + + +def vector_digests(): + """The committed integer-codec vectors' bitstreams, by SHA-256, with the decoded + output's digests their sidecar (sha256.json) states: `X_bitstream.ext` or `X.ext` + under entry `X`.""" + out = {} + for d in sorted(os.listdir(VECTORS)): + side = os.path.join(VECTORS, d, "sha256.json") + if not os.path.exists(side): + continue + entries = json.load(open(side)) + for f in os.listdir(os.path.join(VECTORS, d)): + stem, ext = os.path.splitext(f) + key = stem[:-len("_bitstream")] if stem.endswith("_bitstream") else stem + if ext in (".wih1", ".wia1", ".wis1", ".wiv1") and key in entries: + b = open(os.path.join(VECTORS, d, f), "rb").read() + out[hashlib.sha256(b).hexdigest()] = (f"{d}/{f}", entries[key]) + return out + + +def verify_case(cdir, fails): + case, bases, files = load_case(cdir) + name = case["name"] + + def fail(m): + fails.append(f"{name}: {m}") + print(f"FAIL {name}: {m}") + for b in bases: + spec = b["spec"] + if hashlib.sha256(b["s0"]).hexdigest() != spec["stage0_sha256"]: + fail(f"{spec['stage0']} does not have its SHA-256") + if b["hash"] != spec["object"]: + fail(f"{spec['file']} does not have its content hash") + m, _, payload = read_wai1(b["env"]) + via, kind = spec["decoded_via"], spec["decode"]["kind"] + independent = None + if kind == "png": + independent = [decode_png(payload)] + elif kind == "flac": + independent = [decode_flac(payload)] + elif via == "wai.video.int_motion": + v = IPV.parse_wiv1(payload, 0) + independent = [((v["h"], v["w"], 3), list(f)) for f in IPV.decode_wiv1_k0(v)] + if independent is not None and [(s, d) for s, _, d in b["units"]] != independent: + fail(f"the independent decode of {spec['file']} is not its stage0") + # A base whose payload is a committed vector's bitstream: its stage 0 is the + # decoded output that vector's sidecar states. + known = VECTOR_DIGESTS.get(hashlib.sha256(payload).hexdigest()) + if kind == "int" and known is not None: + name_v, digests = known + if "sha256" in digests: + ok = hashlib.sha256(b["s0"]).hexdigest() == digests["sha256"] + else: + ok = [hashlib.sha256(canonical(e, d)).hexdigest() for _, e, d in b["units"]] == digests["frames_sha256"] + if not ok: + fail(f"the stage0 of {spec['file']} is not the decoded output {name_v}'s sidecar states") + if base_tier(via, m, spec["pin_resolved"]) != spec["tier"]: + fail(f"the tier of {spec['file']} is not {spec['tier']}") + for f in case["files"]: + if content_hash(files[f["file"]]) != f["object"] or len(files[f["file"]]) != f["bytes"]: + fail(f"{f['file']} is not the object the case names") + got, finals = replay(case, bases, files) + expect = case["expect"]["reports"] + if len(got) != len(expect): + fail("a different number of steps") + for i, (g, e) in enumerate(zip(got, expect)): + same = (g == e) if isinstance(g, dict) or isinstance(e, dict) else \ + sorted(g, key=lambda r: r["object"]) == sorted(e, key=lambda r: r["object"]) + if not same: + fail(f"step {i}: reports {g} != {e}") + for bi, (got_b, exp) in enumerate(zip(finals, case["expect"]["bases"])): + if got_b is None or exp is None: + if got_b != exp: + fail(f"base {bi}: decoded {got_b is not None}, the case states {exp is not None}") + continue + if got_b["units"] != exp["units"]: + fail(f"base {bi}: final units differ") + if got_b["clip_sha256"] != exp["clip_sha256"]: + fail(f"base {bi}: the clip hash differs") + + +def main(d): + global VECTOR_DIGESTS + VECTOR_DIGESTS = vector_digests() + fails = [] + cases = sorted(os.listdir(os.path.join(d, "cases"))) + for name in cases: + verify_case(os.path.join(d, "cases", name), fails) + n_wai2 = verify_wai2(d, fails) + sched = json.load(open(os.path.join(d, "schedule.json"))) + for i, t in enumerate(sched["traces"]): + p = present(sched["clock"], t["base_ms"], t["layers_ms"], t["lead_ms"]) + misses = sum(1 for x in p if x["stage"] < x["expected"]) + if p != t["presented"] or misses != t["deadline_misses"]: + fails.append(f"schedule trace {i}") + print(f"FAIL schedule trace {i}") + print(f"{len(cases)} cases, {n_wai2} WAI2 checks, schedule: {len(sched['traces'])} traces, {len(fails)} failure(s)") + return 1 if fails else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else HERE)) diff --git a/wai/staged-conformance/wai2/component_manifest_mismatch.wai b/wai/staged-conformance/wai2/component_manifest_mismatch.wai new file mode 100644 index 000000000..ccf66b00c Binary files /dev/null and b/wai/staged-conformance/wai2/component_manifest_mismatch.wai differ diff --git a/wai/staged-conformance/wai2/digest_mismatch.wai b/wai/staged-conformance/wai2/digest_mismatch.wai new file mode 100644 index 000000000..be5a5c450 Binary files /dev/null and b/wai/staged-conformance/wai2/digest_mismatch.wai differ diff --git a/wai/staged-conformance/wai2/index.json b/wai/staged-conformance/wai2/index.json new file mode 100644 index 000000000..9a5c9f400 --- /dev/null +++ b/wai/staged-conformance/wai2/index.json @@ -0,0 +1,276 @@ +{ + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte. `packs` lists WAI1 envelopes and whether a packer packs them: it refuses bytes around a manifest's JSON value or after a payload, and a manifest that inside a WAI2 entry passes the JSON depth limit.", + "files": [ + { + "file": "pack_live_chain.wai", + "sources": [ + "pack_live_chain.src0.wai", + "pack_live_chain.src1.wai", + "pack_live_chain.src2.wai", + "pack_live_chain.src3.wai" + ], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "7c2879ab5b602075503c62526abfcf8fc1dc7eb6626b88c9895ffeabcf493687", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 1, + 2, + 3 + ] + } + }, + { + "file": "ok_explode.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "2ae6c786594f2752428614de4ac6adba78a6e1614896ab3b185d014e0599dc54" + ], + "refinements_of_0": [ + 1, + 2 + ] + } + }, + { + "file": "digest_mismatch.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "6d45d931e5e0b7d7bbf5c858819b7507f5204adf08a056dd31644e6b0c8d9799" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "component_manifest_mismatch.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": [ + 1, + "component-manifest-mismatch" + ], + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "7c2879ab5b602075503c62526abfcf8fc1dc7eb6626b88c9895ffeabcf493687", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 2, + 3 + ] + } + }, + { + "file": "prev_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-prev-missing", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + null + ], + "refinements_of_0": [ + 1, + 2 + ] + } + }, + { + "file": "refinement_before_selectable.wai", + "sources": [], + "expect": { + "roles": "refinement-before-selectable", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1", + "2ae6c786594f2752428614de4ac6adba78a6e1614896ab3b185d014e0599dc54", + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4" + ], + "refinements_of_0": [ + 1, + 2 + ] + } + }, + { + "file": "refinement_manifest_fallback.wai", + "sources": [], + "expect": { + "roles": null, + "manifests": [ + 1, + "component-manifest-mismatch" + ], + "exploded": [ + "20488045874a49bf9680c7c635b5db88dcdf1aefad0f48acaacd6f7c286e07df", + "9f29f42293547ca8a1c463acf427dd251e3b6324418f059113a2fb2cb5e504c8", + "7e347c1d544e77ee0b3d47f4798f29044be7f075fc0df364bad7bb94dac98731", + "b8b998dac5a0e74511eaeb9692414ede5447cb4b280ee6311c084a31084988ee" + ], + "refinements_of_0": [ + 1, + 2, + 3 + ] + } + }, + { + "file": "refinement_not_refinement_only.wai", + "sources": [], + "expect": { + "roles": "refinement-not-refinement-only", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "e49911c2135e39829cc527ced20d9de8c78fe7629eb11b6c802cbe1f5afe64ec" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_identity_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-identity-missing", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "53d7a0e6db5d6b6a0f2788f60d86717d3619b3dd5c834fd65ee18456eab7c5f1" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_base_missing.wai", + "sources": [], + "expect": { + "roles": "refinement-base-missing", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + null + ], + "refinements_of_0": [] + } + }, + { + "file": "refinement_base_not_admitted.wai", + "sources": [], + "expect": { + "roles": "refinement-base-not-admitted", + "manifests": null, + "exploded": [ + "b8f86cdfdad10c074504661ae17e37567d57897b58d4731d2128025d33290308", + "78c02620a638b17fd4d04a8a83c4934a89966bbc0bdac4de1906158c63fefdb5" + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refines_on_non_refinement.wai", + "sources": [], + "expect": { + "roles": "refines-on-non-refinement", + "manifests": null, + "exploded": [ + null, + null + ], + "refinements_of_0": [ + 1 + ] + } + }, + { + "file": "refinement_only_as_component.wai", + "sources": [], + "expect": { + "roles": "refinement-only-as-component", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + "68b51fa0c19eea9aaac266bc686c06d2d2204220d5bd3c9d5c1a30a7fe4f7565" + ], + "refinements_of_0": [] + } + }, + { + "file": "refinement_refines_malformed.wai", + "sources": [], + "expect": { + "roles": "refines-malformed", + "manifests": null, + "exploded": [ + "548f6a1b589a1b74fac21eaa7226b8bdc6be0360b8d47886cac964b1f081a5d4", + null + ], + "refinements_of_0": [] + } + } + ], + "packs": [ + { + "name": "pack_trailing_newline", + "sources": [ + "pack_trailing_newline.src0.wai" + ], + "packs": false + }, + { + "name": "pack_leading_space", + "sources": [ + "pack_leading_space.src0.wai" + ], + "packs": false + }, + { + "name": "pack_after_payload", + "sources": [ + "pack_after_payload.src0.wai" + ], + "packs": false + }, + { + "name": "pack_deep_120", + "sources": [ + "pack_deep_120.src0.wai" + ], + "packs": true + }, + { + "name": "pack_deep_124", + "sources": [ + "pack_deep_124.src0.wai" + ], + "packs": false + } + ] +} diff --git a/wai/staged-conformance/wai2/ok_explode.wai b/wai/staged-conformance/wai2/ok_explode.wai new file mode 100644 index 000000000..42da3e6c3 Binary files /dev/null and b/wai/staged-conformance/wai2/ok_explode.wai differ diff --git a/wai/staged-conformance/wai2/pack_after_payload.src0.wai b/wai/staged-conformance/wai2/pack_after_payload.src0.wai new file mode 100644 index 000000000..548725bc9 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_after_payload.src0.wai differ diff --git a/wai/staged-conformance/wai2/pack_deep_120.src0.wai b/wai/staged-conformance/wai2/pack_deep_120.src0.wai new file mode 100644 index 000000000..dfa4068f3 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_deep_120.src0.wai differ diff --git a/wai/staged-conformance/wai2/pack_deep_124.src0.wai b/wai/staged-conformance/wai2/pack_deep_124.src0.wai new file mode 100644 index 000000000..b600eebb4 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_deep_124.src0.wai differ diff --git a/wai/staged-conformance/wai2/pack_leading_space.src0.wai b/wai/staged-conformance/wai2/pack_leading_space.src0.wai new file mode 100644 index 000000000..26c849980 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_leading_space.src0.wai differ diff --git a/wai/staged-conformance/wai2/pack_live_chain.src0.wai b/wai/staged-conformance/wai2/pack_live_chain.src0.wai new file mode 100644 index 000000000..d7413225c Binary files /dev/null and b/wai/staged-conformance/wai2/pack_live_chain.src0.wai differ diff --git a/wai/staged-conformance/wai2/pack_live_chain.src1.wai b/wai/staged-conformance/wai2/pack_live_chain.src1.wai new file mode 100644 index 000000000..fd9e5d435 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_live_chain.src1.wai differ diff --git a/wai/staged-conformance/wai2/pack_live_chain.src2.wai b/wai/staged-conformance/wai2/pack_live_chain.src2.wai new file mode 100644 index 000000000..320ae80e4 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_live_chain.src2.wai differ diff --git a/wai/staged-conformance/wai2/pack_live_chain.src3.wai b/wai/staged-conformance/wai2/pack_live_chain.src3.wai new file mode 100644 index 000000000..7d8bb84ba Binary files /dev/null and b/wai/staged-conformance/wai2/pack_live_chain.src3.wai differ diff --git a/wai/staged-conformance/wai2/pack_live_chain.wai b/wai/staged-conformance/wai2/pack_live_chain.wai new file mode 100644 index 000000000..1a541dec7 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_live_chain.wai differ diff --git a/wai/staged-conformance/wai2/pack_trailing_newline.src0.wai b/wai/staged-conformance/wai2/pack_trailing_newline.src0.wai new file mode 100644 index 000000000..1bc948cb2 Binary files /dev/null and b/wai/staged-conformance/wai2/pack_trailing_newline.src0.wai differ diff --git a/wai/staged-conformance/wai2/prev_missing.wai b/wai/staged-conformance/wai2/prev_missing.wai new file mode 100644 index 000000000..d1176dfc5 Binary files /dev/null and b/wai/staged-conformance/wai2/prev_missing.wai differ diff --git a/wai/staged-conformance/wai2/refinement_base_missing.wai b/wai/staged-conformance/wai2/refinement_base_missing.wai new file mode 100644 index 000000000..215ac78cc Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_base_missing.wai differ diff --git a/wai/staged-conformance/wai2/refinement_base_not_admitted.wai b/wai/staged-conformance/wai2/refinement_base_not_admitted.wai new file mode 100644 index 000000000..976228b14 Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_base_not_admitted.wai differ diff --git a/wai/staged-conformance/wai2/refinement_before_selectable.wai b/wai/staged-conformance/wai2/refinement_before_selectable.wai new file mode 100644 index 000000000..ba4848199 Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_before_selectable.wai differ diff --git a/wai/staged-conformance/wai2/refinement_identity_missing.wai b/wai/staged-conformance/wai2/refinement_identity_missing.wai new file mode 100644 index 000000000..261c2e0b4 Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_identity_missing.wai differ diff --git a/wai/staged-conformance/wai2/refinement_manifest_fallback.wai b/wai/staged-conformance/wai2/refinement_manifest_fallback.wai new file mode 100644 index 000000000..5fdeac10d Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_manifest_fallback.wai differ diff --git a/wai/staged-conformance/wai2/refinement_not_refinement_only.wai b/wai/staged-conformance/wai2/refinement_not_refinement_only.wai new file mode 100644 index 000000000..f3ab95c09 Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_not_refinement_only.wai differ diff --git a/wai/staged-conformance/wai2/refinement_only_as_component.wai b/wai/staged-conformance/wai2/refinement_only_as_component.wai new file mode 100644 index 000000000..7e903702a Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_only_as_component.wai differ diff --git a/wai/staged-conformance/wai2/refinement_refines_malformed.wai b/wai/staged-conformance/wai2/refinement_refines_malformed.wai new file mode 100644 index 000000000..1231c0805 Binary files /dev/null and b/wai/staged-conformance/wai2/refinement_refines_malformed.wai differ diff --git a/wai/staged-conformance/wai2/refines_on_non_refinement.wai b/wai/staged-conformance/wai2/refines_on_non_refinement.wai new file mode 100644 index 000000000..1c7501bc4 Binary files /dev/null and b/wai/staged-conformance/wai2/refines_on_non_refinement.wai differ diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index 24016e876..dd4bfb0e9 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -5,10 +5,11 @@ # A WAI manifest, typed or not: every manifest has a wai (version) field. A WAI1 manifest names a # capability, a WAI2 manifest a renditions list, and never both. The capability is a -# registered payload or companion capability that is not a candidate; the fallback a -# registered payload capability that is not a candidate. (A WAI1 naming a companion-only -# capability parses but does not conform; the reference implementation's role check -# reports it.) media is informational, and a reader accepts a value it does not know +# registered payload, companion or refinement capability that is not a candidate; the +# fallback a registered payload capability that is not a candidate. (A WAI1 naming a +# companion-only capability, or a refinement-only one without refines, parses but does +# not conform; the reference implementation's role check reports it.) media is +# informational, and a reader accepts a value it does not know # (SPEC sections 3 and 8), so only its form is checked. wai:ManifestShape a sh:NodeShape ; sh:targetClass wai:Manifest ; @@ -22,6 +23,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.audio.flac wai:wai.audio.iamf wai:wai.audio.int_codec + wai:wai.audio.int_refine wai:wai.audio.ivas wai:wai.audio.mpegh wai:wai.audio.opus @@ -35,6 +37,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.haptic.mihs wai:wai.haptic.signal wai:wai.image.avif + wai:wai.image.int_refine wai:wai.image.jp2 wai:wai.image.jpeg wai:wai.image.jpegai @@ -71,6 +74,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.semantic.perco2 wai:wai.splat.4d wai:wai.splat.int_codec + wai:wai.splat.int_refine wai:wai.splat.spz wai:wai.ternary.tt wai:wai.text.xz @@ -85,6 +89,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.video.hevc wai:wai.video.int_hyper wai:wai.video.int_motion + wai:wai.video.int_refine wai:wai.video.j2k wai:wai.video.lcevc wai:wai.video.vp9 @@ -178,9 +183,9 @@ wai:ManifestShape a sh:NodeShape ; [ sh:property [ sh:path wai:renditions ; sh:minCount 1 ] ] ) . -# A WAI2 component, in either role: its capability is a registered payload or companion -# capability that is not a candidate, and the base a companion names is a registered -# payload capability that is not a candidate. +# A WAI2 component, in any role: its capability is a registered payload, companion or +# refinement capability that is not a candidate, and the base a companion names is a +# registered payload capability that is not a candidate. wai:ComponentShape a sh:NodeShape ; sh:targetClass wai:Component ; sh:targetObjectsOf wai:renditions ; @@ -192,6 +197,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.audio.flac wai:wai.audio.iamf wai:wai.audio.int_codec + wai:wai.audio.int_refine wai:wai.audio.ivas wai:wai.audio.mpegh wai:wai.audio.opus @@ -205,6 +211,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.haptic.mihs wai:wai.haptic.signal wai:wai.image.avif + wai:wai.image.int_refine wai:wai.image.jp2 wai:wai.image.jpeg wai:wai.image.jpegai @@ -241,6 +248,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.semantic.perco2 wai:wai.splat.4d wai:wai.splat.int_codec + wai:wai.splat.int_refine wai:wai.splat.spz wai:wai.ternary.tt wai:wai.text.xz @@ -255,6 +263,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.video.hevc wai:wai.video.int_hyper wai:wai.video.int_motion + wai:wai.video.int_refine wai:wai.video.j2k wai:wai.video.lcevc wai:wai.video.vp9 diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 2f52a640f..9747b7ac6 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -31,6 +31,7 @@ wai:ParameterSetPin a owl:Class ; rdfs:label "Parameter-set pin (SPEC section 3. wai:kind-payload a wai:Kind ; rdfs:label "payload" . wai:kind-companion a wai:Kind ; rdfs:label "companion" . +wai:kind-refinement a wai:Kind ; rdfs:label "refinement" . wai:kind-derivation a wai:Kind ; rdfs:label "derivation" . wai:kind-record a wai:Kind ; rdfs:label "record" . wai:status-registered a wai:Status ; rdfs:label "registered" . @@ -47,6 +48,7 @@ wai:media-binary a wai:Media ; rdfs:label "binary" . wai:role-standalone a wai:Role ; rdfs:label "standalone" . wai:role-companion a wai:Role ; rdfs:label "companion" . wai:role-enhancement a wai:Role ; rdfs:label "enhancement" . +wai:role-refinement a wai:Role ; rdfs:label "refinement" . wai:tier-lossless a wai:Tier ; rdfs:label "lossless" . wai:tier-lossy a wai:Tier ; rdfs:label "lossy" . wai:tier-perceptual a wai:Tier ; rdfs:label "perceptual" . @@ -181,6 +183,18 @@ wai:wai.audio.int_codec a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-06-27"^^xsd:date . +wai:wai.audio.int_refine a wai:Capability ; + rdfs:label "wai.audio.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-audio ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.audio.ivas a wai:Capability ; rdfs:label "wai.audio.ivas" ; wai:kind wai:kind-payload ; @@ -428,6 +442,18 @@ wai:wai.image.avif a wai:Capability ; wai:specSection "§5 Recommended modern set" ; wai:registeredOn "2026-06-12"^^xsd:date . +wai:wai.image.int_refine a wai:Capability ; + rdfs:label "wai.image.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-image ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.image.jp2 a wai:Capability ; rdfs:label "wai.image.jp2" ; wai:kind wai:kind-payload ; @@ -876,6 +902,18 @@ wai:wai.splat.int_codec a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-06-28"^^xsd:date . +wai:wai.splat.int_refine a wai:Capability ; + rdfs:label "wai.splat.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-volumetric ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.splat.sog a wai:Capability ; rdfs:label "wai.splat.sog" ; wai:kind wai:kind-payload ; @@ -1077,6 +1115,18 @@ wai:wai.video.int_motion a wai:Capability ; wai:specSection "§5 Neural capabilities" ; wai:registeredOn "2026-06-27"^^xsd:date . +wai:wai.video.int_refine a wai:Capability ; + rdfs:label "wai.video.int_refine" ; + wai:kind wai:kind-refinement ; + wai:status wai:status-registered ; + wai:media wai:media-video ; + wai:role wai:role-refinement ; + wai:tier wai:tier-lossy ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Refinement capabilities (staged delivery)" ; + wai:registeredOn "2026-10-01"^^xsd:date . + wai:wai.video.j2k a wai:Capability ; rdfs:label "wai.video.j2k" ; wai:kind wai:kind-payload ; diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 6fe930a16..3139cbc0c 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -30,7 +30,7 @@ unexpected_cfgs = { level = "warn", check-cfg = ["cfg(kani)"] } [dependencies] # Container manifest. preserve_order so JSON round-trips byte-identical. serde = { version = "1", features = ["derive"] } -serde_json = { version = "1", features = ["preserve_order"] } +serde_json = { version = "1", features = ["preserve_order", "raw_value"] } # ---- zeroth codec wrappers: lossy + lossless across all media ---- # All optional behind the (default-on) `codecs` feature: native sinks get @@ -179,6 +179,11 @@ oer = ["dep:blake3", "dep:ed25519-dalek", "dep:num-bigint", "serde_json/float_ro # jwp-receipts §2.1's object content hash (src/jwp_hash.rs), on its own so any # receipt family that names an object can compute it with BLAKE3 alone. jwp_hash = ["dep:blake3"] +# Staged delivery (extensions/staged-delivery.md): a base first, then refinements as +# their own objects, each bound to what it refines by content hash. The residual form +# (WIR1) is integer rANS (neural_int); content hashes are jwp_hash's BLAKE3; stage +# hashes are SHA-256, always on. +staged = ["neural_int", "jwp_hash"] # Typed receipt classes on the JWP chain (extensions/jwp-receipts.md §2.4.3–§2.4.7, # §2.6, §3.1–§3.2): `claim` receipts of class energy / lawful-basis / log-inclusion, # the soft-binding class, and recovery under the ancestor rule. Needs `provenance` @@ -491,6 +496,11 @@ name = "wai_int_payload_vectors" path = "src/bin/wai_int_payload_vectors.rs" required-features = ["video_int", "audio_int", "splat_int"] +[[bin]] +name = "wai_staged_vectors" +path = "src/bin/wai_staged_vectors.rs" +required-features = ["staged", "video_int", "audio_int", "splat_int"] + [[bin]] name = "wai_film_vectors" path = "src/bin/wai_film_vectors.rs" @@ -651,6 +661,16 @@ name = "int_mlicpp_preflight" path = "tests/int_mlicpp_preflight.rs" required-features = ["neural_int"] +[[test]] +name = "cli_stage" +path = "tests/cli_stage.rs" +required-features = ["staged", "video_int", "audio_int", "splat_int"] + +[[test]] +name = "staged_resources" +path = "tests/staged_resources.rs" +required-features = ["staged", "video_int"] + [[test]] name = "int_payload" path = "tests/int_payload.rs" diff --git a/wai/wai-rs/src/bin/wai.rs b/wai/wai-rs/src/bin/wai.rs index ec7ce19e5..3d1b6e722 100644 --- a/wai/wai-rs/src/bin/wai.rs +++ b/wai/wai-rs/src/bin/wai.rs @@ -18,6 +18,10 @@ use wai::{Conditioning, Manifest, ModelRequirement, Wai}; #[cfg(feature = "codecs")] use wai::*; +#[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))] +#[path = "wai_cli/stage.rs"] +mod stage; + fn die(msg: impl std::fmt::Display) -> ExitCode { eprintln!("error: {msg}"); ExitCode::from(2) @@ -35,6 +39,8 @@ fn usage() { eprintln!(" write (or, with --check, compare) registry/capabilities.json, its schema,"); eprintln!(" vocab/v2.* and the generated table of REGISTRY.md"); eprintln!("wai prior-set print the listing of a parameter set (wai.prior.set) and its pin"); + eprintln!("wai stage refine|apply|inspect|pack|explode … staged-delivery layers (a build with `staged`,"); + eprintln!(" `video_int`, `audio_int` and `splat_int`); `wai stage` alone prints its usage"); eprintln!(); eprintln!("encode and decode need a build with the `codecs` feature."); eprintln!("codecs: png jpeg avif jxl jxl-lossless opus flac av1 av1-lossless zstd xz"); @@ -327,6 +333,12 @@ fn inspect(path: &str) -> Result<(), String> { Ok(()) => println!(" pin check: \u{2713} no static finding (SPEC \u{a7}3.1, \u{a7}7)"), Err(f) => println!(" pin check: \u{2717} {f}"), } + match w.refines() { + None => {} + Some(Ok(r)) => println!(" refines: base {}… layer {}{}", &r.base[..16], r.layer, + r.span.map(|s| format!(" units {}..{}", s.from, s.from + s.count)).unwrap_or_default()), + Some(Err(e)) => println!(" refines: \u{2717} {e}"), + } println!(); println!(" capability: {cap}"); println!(" resolution: {}", @@ -506,6 +518,12 @@ fn wrap(cap: &str, input: &str, out: &str, fallback: Option<&str>, if !wai::codecs::is_known_capability(cap) { return Err(format!("'{cap}' is not a registered capability (see `wai capabilities`)")); } + // A layer names, in `refines`, what it refines, and `wai wrap` writes no + // `refines` (SPEC §5, Refinement capabilities). + if wai::codecs::is_refinement_only(cap) { + return Err(format!("{cap} is refinement-only: a layer names what it refines, which `wai wrap` \ + does not write; `wai stage refine` writes one (extensions/staged-delivery.md)")); + } // A candidate is refused before any other rule is applied, in every place the // envelope would name it (SPEC §5, Candidate capabilities). if companions.is_empty() { @@ -548,6 +566,8 @@ fn wrap(cap: &str, input: &str, out: &str, fallback: Option<&str>, envelope.check_component_roles().map_err(|e| match e { wai::container::RoleError::CompanionOnlyAsCapability { .. } => format!( "{e} (SPEC \u{a7}5): `wai wrap --companion {cap} `"), + wai::container::RoleError::RefinementWithoutRefines { .. } => format!( + "{e}: a layer names what it refines; `wai stage refine` writes one (extensions/staged-delivery.md)"), _ => format!("{e} (SPEC \u{a7}5)"), })?; let n = envelope.write(out).map_err(|e| e.to_string())?; @@ -792,6 +812,10 @@ fn main() -> ExitCode { Err(e) => Err(e), }, "prior-set" if args.len() >= 2 => prior_set(&args[1]), + #[cfg(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int"))] + "stage" => stage::main(&args[1..]), + #[cfg(not(all(feature = "staged", feature = "video_int", feature = "audio_int", feature = "splat_int")))] + "stage" => Err("`wai stage` needs a build with the features staged, video_int, audio_int and splat_int".into()), _ => { usage(); return ExitCode::from(2); } }; match r { diff --git a/wai/wai-rs/src/bin/wai_cli/stage.rs b/wai/wai-rs/src/bin/wai_cli/stage.rs new file mode 100644 index 000000000..c8cdd2da2 --- /dev/null +++ b/wai/wai-rs/src/bin/wai_cli/stage.rs @@ -0,0 +1,377 @@ +//! `wai stage`: write, apply, inspect, pack and explode staged-delivery layers +//! (`extensions/staged-delivery.md`; SPEC §7.1). Built with the `staged` feature and +//! the integer codecs' features, which decode the bases. +//! +//! A base of an integer capability is decoded through `int_payload::decode_int`: with +//! no parameter set for `wai.video.int_motion`, otherwise against the set in the +//! directory `--set` names (its files by the names SPEC §5 gives them), with the base's +//! pin verified when it carries one. Any other base (PNG, FLAC: this tool has no +//! `codecs` decoder for them) is read from `--stage0`, the units' canonical buffers +//! concatenated, with `--units N --dims D0,D1,D2 --elem u8|i16`. + +use std::collections::BTreeMap; + +use wai::container::{Prev, PrevRun, Refines, Span, Wai, WaiMulti}; +use wai::int_payload::{decode_int, resolve}; +use wai::pin::HeldBytes; +use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams}; +use wai::staged::{tier, BaseInfo, ChainConfig, HoldBound, ReceiptEvidence, Samples, StageChain, UnitBuf}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn opt<'a>(args: &'a [String], flag: &str) -> Option<&'a str> { + args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).map(String::as_str) +} + +fn opts<'a>(args: &'a [String], flag: &str) -> Vec<&'a str> { + args.iter().enumerate().filter(|(_, a)| *a == flag).filter_map(|(i, _)| args.get(i + 1)).map(String::as_str).collect() +} + +/// The positional arguments: those that are neither a flag nor a flag's value. +fn positional(args: &[String]) -> Vec<&str> { + let mut out = Vec::new(); + let mut skip = false; + for a in args { + if skip { + skip = false; + } else if a.starts_with("--") || a == "-o" { + skip = true; + } else { + out.push(a.as_str()); + } + } + out +} + +fn read(path: &str) -> Result, String> { + std::fs::read(path).map_err(|e| format!("read {path}: {e}")) +} + +fn dims(s: &str) -> Result<(u32, u32, u32), String> { + let v: Vec = s.split(',').map(|x| x.trim().parse::().map_err(|e| format!("--dims {s}: {e}"))).collect::>()?; + match v[..] { + [a, b, c] => Ok((a, b, c)), + _ => Err(format!("--dims {s}: three dimensions, D0,D1,D2")), + } +} + +/// The base's info and units, decoded as the module doc states. +fn base(path: &str, args: &[String]) -> Result<(Vec, BaseInfo, Vec), String> { + let env = read(path)?; + let w = Wai::from_bytes(&env).map_err(|e| format!("{path}: {e}"))?; + let cap = w.capability().to_string(); + let hash = wai::jwp_hash::content_hash(&env); + let (units, pin_resolved) = if let Some(s0) = opt(args, "--stage0") { + let bytes = read(s0)?; + let n: usize = opt(args, "--units").unwrap_or("1").parse().map_err(|e| format!("--units: {e}"))?; + let d = dims(opt(args, "--dims").ok_or("--stage0 needs --dims")?)?; + let per = d.0 as usize * d.1 as usize * d.2 as usize; + let i16 = opt(args, "--elem") == Some("i16"); + let need = n * per * if i16 { 2 } else { 1 }; + if bytes.len() != need { + return Err(format!("{s0}: {} bytes, the units need {need}", bytes.len())); + } + let units = (0..n) + .map(|u| { + let samples = if i16 { + Samples::I16(bytes[2 * u * per..2 * (u + 1) * per].chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect()) + } else { + Samples::U8(bytes[u * per..(u + 1) * per].to_vec()) + }; + UnitBuf { shape: d, samples } + }) + .collect(); + (units, false) + } else { + let set: BTreeMap> = match opt(args, "--set") { + Some(dir) => wai::pin::read_dir_set(std::path::Path::new(dir)).map_err(|e| format!("--set {dir}: {e}"))?, + None => BTreeMap::new(), + }; + let files: BTreeMap = set.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let listing = if files.is_empty() { vec![] } else { wai::pin::listing_from(&files).map_err(|e| e.to_string())? }; + let one_file = wai::container::pin_shape(&cap) == wai::codecs::ParameterSetShape::OneFile; + let held: Vec = if files.is_empty() { + vec![] + } else if one_file && w.prior().is_some_and(|p| p.form().is_none()) { + vec![HeldBytes::File(files.values().next().copied().unwrap_or_default())] + } else { + vec![HeldBytes::Set { listing: &listing, files: &files }] + }; + let params = resolve(&cap, w.prior(), &held, None).map_err(|e| format!("{path}: {e}"))?; + let pinned = params.is_pinned(); + let d = decode_int(&cap, &w.payload, ¶ms).map_err(|e| format!("{path}: {e}"))?; + (wai::staged::units_from_decoded(d).ok_or("an output kind with no units")?, pinned) + }; + let info = BaseInfo { + content_hash: hash, + tier: tier::base_tier(&cap, if pin_resolved { w.prior() } else { None }), + capability: cap, + via_fallback: false, + attested: false, + first_unit: 0, + }; + Ok((env, info, units)) +} + +fn below(s: &Samples) -> Below<'_> { + match s { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 1024, max_age_ms: u64::MAX }, cost_limit: None, budget: u64::MAX, record_budget: u64::MAX, live: None } +} + +fn print_units(c: &StageChain) { + for (u, s) in c.units().iter().enumerate() { + let sha = c.unit_sha256(u as u32).map(|h| hex(&h)).unwrap_or_else(|| "(none: not decode-equivalence)".into()); + println!(" unit {u}: stage {} {}x{}x{} producer {} tier {}{} sha256 {sha}", + s.stage, s.buf.shape.0, s.buf.shape.1, s.buf.shape.2, &hex(&s.producer)[..16], tier::token(s.tier), + if s.frozen { " FROZEN" } else { "" }); + } + match c.clip_sha256() { + Some(h) => println!(" clip sha256 {}", hex(&h)), + None => println!(" clip sha256 (none: not every unit holds decode-equivalence)"), + } +} + +/// `wai stage apply …`: offer each layer in order; print every +/// report and the units at the end. +fn apply(args: &[String]) -> Result<(), String> { + let pos = positional(args); + let (_, info, units) = base(pos.first().ok_or("wai stage apply …")?, args)?; + let mut c = StageChain::new(info, units, cfg()); + for (i, l) in pos[1..].iter().enumerate() { + let bytes = read(l)?; + for r in c.offer(&bytes, i as u64, ReceiptEvidence::None).map_err(|e| format!("{l}: {e}"))? { + print!("{l}: {}", &hex(&r.content_hash)[..16]); + if let Some(e) = r.envelope { + print!(" refused {}", e.code()); + if let Some(d) = r.detail { + print!(" ({d})"); + } + } + if !r.applied.is_empty() { + print!(" applied {:?}", r.applied); + } + if !r.held.is_empty() { + print!(" held {:?}", r.held); + } + for (u, w) in &r.refused { + print!(" unit {u}: {}", w.code()); + } + println!(); + } + } + print_units(&c); + Ok(()) +} + +/// `wai stage refine --base [--on ]… --target --q N +/// [--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--out-dims D0,D1,D2] +/// [--max-symbols N] [--table-per-unit] -o L.wai`: a residual layer toward `--target` +/// (the span's units' canonical buffers concatenated, at `--out-dims`, by default the +/// predictor's output shape), over the stage the `--on` layers give; `prev` names the +/// producers the span holds. `--dims` is the stage-0 shape a `--stage0` base is read +/// at, never the layer's. +fn refine(args: &[String]) -> Result<(), String> { + let base_path = opt(args, "--base").ok_or("--base ")?; + let out = opt(args, "-o").ok_or("-o ")?; + let (_, info, units) = base(base_path, args)?; + let cap = match units.first().map(|u| &u.samples) { + Some(Samples::I16(_)) => wai::codecs::CAP_AUDIO_INT_REFINE, + _ if wai::codecs::capability_media(&info.capability) == Some(wai::codecs::Media::Video) => wai::codecs::CAP_VIDEO_INT_REFINE, + _ if wai::codecs::capability_media(&info.capability) == Some(wai::codecs::Media::Volumetric) => wai::codecs::CAP_SPLAT_INT_REFINE, + _ => wai::codecs::CAP_IMAGE_INT_REFINE, + }; + let base_hash = info.content_hash; + let n_units = units.len() as u32; + let mut c = StageChain::new(info, units, cfg()); + for l in opts(args, "--on") { + let bytes = read(l)?; + c.offer(&bytes, 0, ReceiptEvidence::None).map_err(|e| format!("{l}: {e}"))?; + } + let (from, count) = match opt(args, "--span") { + Some(s) => { + let (a, b) = s.split_once(':').ok_or("--span F:C")?; + (a.parse::().map_err(|e| e.to_string())?, b.parse::().map_err(|e| e.to_string())?) + } + None => (0, n_units), + }; + if count == 0 || from as u64 + count as u64 > n_units as u64 { + return Err(format!("--span {from}:{count} is outside the base's {n_units} units")); + } + let span_units: Vec<&wai::staged::UnitState> = (from..from + count).map(|u| c.unit(u).expect("in range")).collect(); + let layer = span_units[0].stage.checked_add(1).ok_or("a unit is at stage 255")?; + if span_units.iter().any(|s| s.stage + 1 != layer) { + return Err("the span's units are at different stages; refine them in spans of one stage".into()); + } + let below_shape = span_units[0].buf.shape; + if span_units.iter().any(|s| s.buf.shape != below_shape) { + return Err("the span's units have different shapes".into()); + } + let q: u32 = opt(args, "--q").ok_or("--q N")?.parse().map_err(|e| format!("--q: {e}"))?; + let (predictor, rate) = match opt(args, "--predictor").unwrap_or("identity") { + "identity" => (Predictor::Identity, (0, 0)), + "up2" => (Predictor::Up2, (0, 0)), + r if r.starts_with("rate:") => { + let (a, b) = r[5..].split_once('/').ok_or("--predictor rate:NUM/DEN")?; + (Predictor::Rate, (a.parse::().map_err(|e| e.to_string())?, b.parse::().map_err(|e| e.to_string())?)) + } + p => return Err(format!("--predictor {p}: identity, up2 or rate:NUM/DEN")), + }; + let out_dims = match opt(args, "--out-dims") { + Some(d) => dims(d)?, + None => match predictor { + Predictor::Identity => below_shape, + Predictor::Up2 => (below_shape.0 * 2, below_shape.1 * 2, below_shape.2), + Predictor::Rate => (wai::staged::predict::rate_len(below_shape.0 as u64, rate.0, rate.1).ok_or("rate")? as u32, 1, below_shape.2), + }, + }; + let target = read(opt(args, "--target").ok_or("--target ")?)?; + let per = out_dims.0 as usize * out_dims.1 as usize * out_dims.2 as usize; + let pcm = matches!(span_units[0].buf.samples, Samples::I16(_)); + if target.len() != count as usize * per * if pcm { 2 } else { 1 } { + return Err(format!("--target holds {} bytes; {count} units of {}x{}x{} need {}", target.len(), out_dims.0, out_dims.1, out_dims.2, count as usize * per * if pcm { 2 } else { 1 })); + } + let targets: Vec = (0..count as usize) + .map(|u| if pcm { + Samples::I16(target[2 * u * per..2 * (u + 1) * per].chunks_exact(2).map(|x| i16::from_le_bytes([x[0], x[1]])).collect()) + } else { + Samples::U8(target[u * per..(u + 1) * per].to_vec()) + }) + .collect(); + let belows: Vec = span_units.iter().map(|s| below(&s.buf.samples)).collect(); + let tb: Vec = targets.iter().map(below).collect(); + let p = RefineParams { + q, + predictor, + rate, + table_per_unit: args.iter().any(|a| a == "--table-per-unit"), + max_symbols: opt(args, "--max-symbols").unwrap_or("4095").parse().map_err(|e| format!("--max-symbols: {e}"))?, + }; + let kind = wai::staged::unit_kind(cap).expect("a refinement"); + let (payload, recon) = wir1::encode(kind, &belows, below_shape, &tb, out_dims, &p).map_err(|e| e.to_string())?; + let mut runs: Vec = Vec::new(); + for s in &span_units { + let h = hex(&s.producer); + match runs.last_mut() { + Some(r) if r.producer == h => r.count += 1, + _ => runs.push(PrevRun { producer: h, count: 1 }), + } + } + let prev = if runs.len() == 1 { Prev::One(runs.remove(0).producer) } else { Prev::Runs(runs) }; + let span = (from != 0 || count != n_units).then_some(Span { from, count }); + let refines = Refines { base: hex(&base_hash), prev, layer, span }; + let media = wai::codecs::capability_media(cap).map(|m| m.label()).unwrap_or("binary"); + let m = wai::Manifest::new(media, "replicate", wai::ModelRequirement::new(cap), wai::Conditioning::new("wir1")).with_refines(&refines); + let env = Wai::new(m, payload); + let bytes = env.to_emit_bytes().map_err(|e| e.to_string())?; + env.check_component_roles().map_err(|e| e.to_string())?; + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + let err: u64 = recon.iter().zip(&targets).map(|(r, t)| match (r, t) { + (Next::U8(a), Samples::U8(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0), + (Next::I16(a), Samples::I16(b)) => a.iter().zip(b).map(|(x, y)| (*x as i64 - *y as i64).unsigned_abs()).max().unwrap_or(0), + _ => 0, + }).max().unwrap_or(0); + println!("refined → {out} ({} B) {cap} layer {layer} units {from}..{} q {q} {} largest error {err}", + bytes.len(), from + count, predictor.token(), ); + println!(" content hash {}", hex(&wai::jwp_hash::content_hash(&bytes))); + Ok(()) +} + +/// `wai stage inspect `: the layer's `refines`, its payload's header and its +/// declared cost. +fn inspect(args: &[String]) -> Result<(), String> { + let path = *positional(args).first().ok_or("wai stage inspect ")?; + let bytes = read(path)?; + let w = Wai::from_bytes(&bytes).map_err(|e| e.to_string())?; + println!("{path}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes))); + println!(" capability {} role {}", w.capability(), wai::codecs::capability_role(w.capability()).unwrap_or("unregistered")); + match w.refines() { + None => println!(" refines: (none)"), + Some(Err(e)) => println!(" refines: {e}"), + Some(Ok(r)) => { + println!(" refines: base {} layer {}", r.base, r.layer); + match &r.prev { + Prev::One(p) => println!(" prev {p}"), + Prev::Runs(runs) => runs.iter().for_each(|x| println!(" prev {} × {}", x.producer, x.count)), + } + if let Some(s) = r.span { + println!(" span {}..{}", s.from, s.from + s.count); + } + } + } + match w.check_component_roles() { + Ok(()) => println!(" roles: ✓"), + Err(e) => println!(" roles: ✗ {} ({e})", e.code()), + } + if let Some(kind) = wai::staged::unit_kind(w.capability()) { + match wir1::parse(&w.payload, kind) { + Ok(p) => { + let c = p.declared_cost(); + println!(" WIR1: {} units of {}x{}x{} predictor {} q {} tables {} ", + p.n_units, p.d0, p.d1, p.d2, p.predictor.token(), p.q, p.n_tables()); + println!(" declared cost: {} output bytes, {} symbols, {} working bytes", c.output_bytes, c.symbols, c.working_bytes); + match p.check_streams(&w.payload) { + Ok(()) => println!(" streams: ✓ every unit's stream decodes to its end"), + Err((u, e)) => println!(" streams: ✗ unit {u}: {e}"), + } + } + Err(e) => println!(" WIR1: ✗ {e}"), + } + } + Ok(()) +} + +/// `wai stage pack … -o x.wai` and `wai stage explode +/// [-o out.wai]`. +fn pack(args: &[String]) -> Result<(), String> { + let out = opt(args, "-o").ok_or("-o ")?; + let files: Vec> = positional(args).iter().map(|p| read(p)).collect::>()?; + let refs: Vec<&[u8]> = files.iter().map(Vec::as_slice).collect(); + let bytes = WaiMulti::pack(&refs).map_err(|e| e.to_string())?; + let w = WaiMulti::from_bytes(&bytes).map_err(|e| e.to_string())?; + w.check_component_roles().map_err(|e| e.to_string())?; + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + println!("packed {} envelopes → {out} ({} B)", files.len(), bytes.len()); + Ok(()) +} + +fn explode(args: &[String]) -> Result<(), String> { + let pos = positional(args); + let (path, i) = match pos[..] { + [p, i] => (p, i.parse::().map_err(|e| e.to_string())?), + _ => return Err("wai stage explode [-o out.wai]".into()), + }; + let bytes = WaiMulti::explode(&read(path)?, i).map_err(|e| e.to_string())?; + println!("component {i}: {} B content hash {}", bytes.len(), hex(&wai::jwp_hash::content_hash(&bytes))); + if let Some(out) = opt(args, "-o") { + std::fs::write(out, &bytes).map_err(|e| format!("write {out}: {e}"))?; + } + Ok(()) +} + +pub fn main(args: &[String]) -> Result<(), String> { + match args.first().map(String::as_str) { + Some("apply") => apply(&args[1..]), + Some("refine") => refine(&args[1..]), + Some("inspect") => inspect(&args[1..]), + Some("pack") => pack(&args[1..]), + Some("explode") => explode(&args[1..]), + _ => Err(USAGE.into()), + } +} + +pub const USAGE: &str = "wai stage refine --base [--on ]… --target --q N \ +[--predictor identity|up2|rate:NUM/DEN] [--span F:C] [--out-dims D0,D1,D2] [--max-symbols N] [--table-per-unit] -o L.wai +wai stage apply … offer the layers in order; print each report and the units +wai stage inspect a layer's refines, WIR1 header and declared cost +wai stage pack … -o x.wai pack WAI1 envelopes into a WAI2 (manifests verbatim) +wai stage explode [-o out.wai] a WAI2 component as its WAI1 envelope + a base of an integer codec decodes with --set (its parameter files by name); any + other base is read from --stage0 --units N --dims D0,D1,D2 [--elem u8|i16]"; diff --git a/wai/wai-rs/src/bin/wai_staged_vectors.rs b/wai/wai-rs/src/bin/wai_staged_vectors.rs new file mode 100644 index 000000000..1edae28d1 --- /dev/null +++ b/wai/wai-rs/src/bin/wai_staged_vectors.rs @@ -0,0 +1,2007 @@ +//! `wai_staged_vectors` — writes and verifies the staged-delivery corpus +//! (`extensions/staged-delivery.md` §12, `wai/staged-conformance/`). +//! +//! wai_staged_vectors gen +//! wai_staged_vectors verify +//! +//! `gen` builds every case from inputs it computes or reads from the committed +//! integer vectors (`wai-rs/tests/vectors/`): each case's bases and their stage-0 +//! output (`stage0.bin`), its layers (`*.wai`), and `case.json`, which states the +//! steps a sink takes (offer a layer, decode a base, expire holds) and what a +//! conforming sink reports at each step and holds at the end. It also writes +//! `schedule.json` (arrival traces and what a sink presents) and `index.json`. Every +//! file is a function of those inputs, so a second run writes the same bytes. +//! +//! `verify` regenerates every file in memory and requires it to equal the committed +//! one byte for byte; replays every case from its committed files through +//! `staged::StageSink` and requires every report and the final stage, producer, +//! frozen flag, tier and stage hash of every unit; checks each `stage0.bin` against its +//! SHA-256 and, for a base this build decodes (the integer codecs), against the base's +//! decode through `int_payload::decode_int` with its pinned set; and replays +//! `schedule.json`. A PNG or FLAC base is a fixture here (this build has no `codecs`), +//! decoded by the independent verifier `staged_verify.py`. Exit status 1 on any +//! failure. +//! +//! The content is synthetic: pictures, frames and PCM computed by integer functions in +//! this file. The base payloads of the integer codecs are the committed vectors; the +//! `wai.video.int_motion` bases are coded by a keyframe-0 encoder confined to this file +//! (zero motion, one block per frame), which the decode checks. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use std::process::ExitCode; + +use serde_json::{json, Value}; + +use wai::codecs::{ + DeclaredTier, CAP_AUDIO_FLAC, CAP_AUDIO_INT_CODEC, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_IMAGE_PNG, + CAP_NEURAL_INT_HYPER, CAP_PRIOR_SET, CAP_SPLAT_INT_CODEC, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_HYPER, + CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE, +}; +use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Prior, Refines, Rendition, RenditionMeta, Span, Wai, WaiMulti}; +use wai::int_payload::{decode_int, resolve, IntDecoded}; +use wai::pin::HeldBytes; +use wai::staged::schedule::{deadline_misses, present, UnitClock}; +use wai::staged::wir1::{self, Below, Next, Predictor, RefineParams, UnitKind, Wir1Cost}; +use wai::staged::{tier, BaseInfo, ChainConfig, HoldBound, Live, OfferError, OfferReport, ReceiptEvidence, Samples, StageSink, UnitBuf}; + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn sha(b: &[u8]) -> String { + hex(&wai::pin::sha256(b)) +} + +fn chash(b: &[u8]) -> [u8; 32] { + wai::jwp_hash::content_hash(b) +} + +const VEC: &str = "wai-rs/tests/vectors"; + +// ───────────────────────── synthetic content ───────────────────────── + +/// A picture of `h × w` RGB8: gradients, a disc and a fine texture, moved by `t`. +fn picture(h: u32, w: u32, seed: u32, t: u32) -> Vec { + let mut out = Vec::with_capacity((h * w * 3) as usize); + let (cy, cx) = ((h / 2 + t) as i64, (w / 3 + 2 * t) as i64); + let r2 = ((h.min(w) / 3).max(1) as i64).pow(2); + for y in 0..h { + for x in 0..w { + let d = (y as i64 - cy).pow(2) + (x as i64 - cx).pow(2); + for c in 0..3u32 { + let grad = (y * 160 / h.max(1) + x * 90 / w.max(1)) as i64; + let disc = if d <= r2 { 70 + 25 * c as i64 } else { 0 }; + let tex = (((x * 7 + y * 13 + c * 5 + seed * 11) ^ (x * y + seed)) % 17) as i64 - 8; + out.push((grad + disc + tex + (seed as i64 % 40) + c as i64 * 15).clamp(0, 255) as u8); + } + } + } + out +} + +/// `n` samples of `ch` interleaved channels at `rate` hertz: two triangle tones per +/// channel and a click every 997 samples. +fn pcm(n: u32, ch: u32, rate: u32, seed: u32) -> Vec { + let tri = |t: u64, period: u64, amp: i64| -> i64 { + let p = (t % period) as i64; + let half = period as i64 / 2; + amp * (if p < half { 2 * p - half } else { 3 * half - 2 * p }) / half.max(1) + }; + let mut out = Vec::with_capacity((n * ch) as usize); + for i in 0..n as u64 { + for c in 0..ch as u64 { + let t = i * 48_000 / rate as u64; + let v = tri(t, 109 + 23 * c + seed as u64, 9000) + tri(t, 31 + 7 * c, 2500) + if i % 997 == 3 { 12_000 } else { 0 }; + out.push(v.clamp(-32768, 32767) as i16); + } + } + out +} + +// ───────────────────────── envelopes ───────────────────────── + +fn envelope(media: &str, cap: &str, fallback: Option<&str>, kind: &str, prior: Option, refines: Option<&Refines>, payload: Vec) -> Vec { + let mut mr = ModelRequirement::new(cap); + if let Some(f) = fallback { + mr = mr.with_fallback(f); + } + let mut cond = Conditioning::new(kind); + if let Some(p) = prior { + cond = cond.with_prior(p); + } + let mut m = Manifest::new(media, "replicate", mr, cond); + if let Some(r) = refines { + m = m.with_refines(r); + } + Wai::new(m, payload).to_bytes().expect("serialise") +} + +// ───────────────────────── a keyframe-0 WIV1 encoder (this generator only) ───────────────────────── + +/// Code `frames` (each `h × w` RGB8) as `wai.video.int_motion` (`WIV1`, keyframe 0) +/// with zero motion and one block per frame: frame 0 predicted from grey, each later +/// frame from the previous decoded frame. Returns the payload and its decode. +fn encode_wiv1(frames: &[Vec], h: u32, w: u32, q: u32) -> (Vec, Vec>) { + let mut recon: Vec> = Vec::new(); + let mut rs: Vec = Vec::new(); + for (f, src) in frames.iter().enumerate() { + let mut cur = Vec::with_capacity(src.len()); + for (i, &t) in src.iter().enumerate() { + let pred = if f == 0 { 128 } else { recon[f - 1][i] as i64 }; + let r = wai::staged::predict::rdiv(t as i64 - pred, q as i64); + rs.push(r); + cur.push((pred + r * q as i64).clamp(0, 255) as u8); + } + recon.push(cur); + } + let lo = *rs.iter().min().unwrap(); + let hi = *rs.iter().max().unwrap(); + let width = ((hi - lo + 1) as u64).min(4095); + let mut hist = vec![0u64; width as usize]; + for &r in &rs { + if r >= lo && r < lo + width as i64 { + hist[(r - lo) as usize] += 1; + } + } + let cdf = wai::int_entropy::hist_to_cdf(&hist, 16).expect("table"); + let mut e = wai::rans::RansEncoder::new(); + for &r in &rs { + assert!(e.encode(r as i32, &cdf, cdf.len(), lo as i32)); + } + let stream = e.flush(); + let n = frames.len() as u32; + let block = h.max(w); + let v = wai::int_wire::Wiv1 { + mv_precision: 1, + keyframe: 0, + n_frames: n, + h, + w, + block: if n == 1 { 1 } else { block }, + q, + offset: lo as i32, + cdf, + mvs: vec![(0, 0); (n as usize - 1) * if n == 1 { 0 } else { 1 }], + rans: &stream, + wih1: None, + }; + let payload = wai::int_wire::write_wiv1(&v); + (payload, recon) +} + +// ───────────────────────── PNG and FLAC writers (fixtures) ───────────────────────── + +fn crc32(data: &[u8]) -> u32 { + let mut c = 0xFFFF_FFFFu32; + for &b in data { + c ^= b as u32; + for _ in 0..8 { + c = if c & 1 != 0 { 0xEDB8_8320 ^ (c >> 1) } else { c >> 1 }; + } + } + !c +} + +fn adler32(data: &[u8]) -> u32 { + let (mut a, mut b) = (1u32, 0u32); + for &x in data { + a = (a + x as u32) % 65521; + b = (b + a) % 65521; + } + (b << 16) | a +} + +/// An RGB8 PNG, filter type `row % 5` on each row, stored (uncompressed) deflate. +fn png(rgb: &[u8], h: u32, w: u32) -> Vec { + let stride = (w * 3) as usize; + let mut raw = Vec::new(); + for y in 0..h as usize { + let ft = (y % 5) as u8; + raw.push(ft); + let row = &rgb[y * stride..(y + 1) * stride]; + let up: Vec = if y == 0 { vec![0; stride] } else { rgb[(y - 1) * stride..y * stride].to_vec() }; + for i in 0..stride { + let a = if i >= 3 { row[i - 3] as i16 } else { 0 }; + let b = up[i] as i16; + let c = if i >= 3 { up[i - 3] as i16 } else { 0 }; + let pred = match ft { + 0 => 0, + 1 => a, + 2 => b, + 3 => (a + b) / 2, + _ => { + let p = a + b - c; + let (pa, pb, pc) = ((p - a).abs(), (p - b).abs(), (p - c).abs()); + if pa <= pb && pa <= pc { a } else if pb <= pc { b } else { c } + } + }; + raw.push((row[i] as i16 - pred) as u8); + } + } + let mut z = vec![0x78, 0x01]; + for (k, block) in raw.chunks(65535).enumerate() { + let last = (k + 1) * 65535 >= raw.len(); + z.push(last as u8); + z.extend_from_slice(&(block.len() as u16).to_le_bytes()); + z.extend_from_slice(&(!(block.len() as u16)).to_le_bytes()); + z.extend_from_slice(block); + } + z.extend_from_slice(&adler32(&raw).to_be_bytes()); + let mut out = vec![0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A]; + let mut chunk = |kind: &[u8], data: &[u8]| { + out.extend_from_slice(&(data.len() as u32).to_be_bytes()); + let mut body = kind.to_vec(); + body.extend_from_slice(data); + out.extend_from_slice(&body); + out.extend_from_slice(&crc32(&body).to_be_bytes()); + }; + let mut ihdr = Vec::new(); + ihdr.extend_from_slice(&w.to_be_bytes()); + ihdr.extend_from_slice(&h.to_be_bytes()); + ihdr.extend_from_slice(&[8, 2, 0, 0, 0]); + chunk(b"IHDR", &ihdr); + chunk(b"IDAT", &z); + chunk(b"IEND", &[]); + out +} + +struct Bits { + out: Vec, + acc: u64, + n: u32, +} + +impl Bits { + fn new() -> Self { + Bits { out: vec![], acc: 0, n: 0 } + } + fn put(&mut self, v: u64, bits: u32) { + for i in (0..bits).rev() { + self.acc = (self.acc << 1) | ((v >> i) & 1); + self.n += 1; + if self.n == 8 { + self.out.push(self.acc as u8); + self.acc = 0; + self.n = 0; + } + } + } + fn align(&mut self) { + while self.n != 0 { + self.put(0, 1); + } + } +} + +fn crc8(data: &[u8]) -> u8 { + let mut c = 0u8; + for &b in data { + c ^= b; + for _ in 0..8 { + c = if c & 0x80 != 0 { (c << 1) ^ 0x07 } else { c << 1 }; + } + } + c +} + +fn crc16(data: &[u8]) -> u16 { + let mut c = 0u16; + for &b in data { + c ^= (b as u16) << 8; + for _ in 0..8 { + c = if c & 0x8000 != 0 { (c << 1) ^ 0x8005 } else { c << 1 }; + } + } + c +} + +/// A FLAC stream of 16-bit PCM, `ch` independent channels, VERBATIM subframes, fixed +/// blocks of 256 samples (the last shorter), an unset MD5. +fn flac(samples: &[i16], ch: u32, rate: u32) -> Vec { + let n = samples.len() as u32 / ch; + let bs = 256u32; + let mut out = b"fLaC".to_vec(); + let mut si = Bits::new(); + si.put(bs as u64, 16); + si.put(bs as u64, 16); + si.put(0, 24); + si.put(0, 24); + si.put(rate as u64, 20); + si.put((ch - 1) as u64, 3); + si.put(15, 5); + si.put(n as u64, 36); + for _ in 0..16 { + si.put(0, 8); + } + out.push(0x80); // last block, STREAMINFO + out.extend_from_slice(&(si.out.len() as u32).to_be_bytes()[1..]); + out.extend_from_slice(&si.out); + let mut frame_no = 0u32; + let mut at = 0u32; + while at < n { + let len = bs.min(n - at); + let mut b = Bits::new(); + b.put(0b1111_1111_1111_10, 14); + b.put(0, 1); + b.put(0, 1); // fixed block size + b.put(0b0111, 4); // 16-bit (block size − 1) at the end of the header + b.put(0, 4); // sample rate from STREAMINFO + b.put((ch - 1) as u64, 4); // independent channels + b.put(0b100, 3); // 16 bits per sample + b.put(0, 1); + assert!(frame_no < 128, "the UTF-8 frame number is one byte here"); + b.put(frame_no as u64, 8); + b.put((len - 1) as u64, 16); + let crc = crc8(&b.out); + b.put(crc as u64, 8); + for c in 0..ch { + b.put(0, 1); + b.put(0b000001, 6); // VERBATIM + b.put(0, 1); + for i in 0..len { + b.put(samples[((at + i) * ch + c) as usize] as u16 as u64, 16); + } + } + b.align(); + let crc = crc16(&b.out); + b.put(crc as u64, 16); + out.extend_from_slice(&b.out); + at += len; + frame_no += 1; + } + out +} + +// ───────────────────────── bases ───────────────────────── + +/// How a base's stage 0 is checked. +#[derive(Clone)] +enum Decode { + /// `int_payload::decode_int` against the set (member → path below `wai/`). + Int { set: Vec<(&'static str, String)> }, + Png, + Flac, +} + +#[derive(Clone)] +struct Base { + envelope: Vec, + decoded_via: String, + via_fallback: bool, + attested: bool, + pin_resolved: bool, + tier: Option, + units: Vec, + decode: Decode, + /// The position of its unit 0 on a live clock (§8). + first_unit: u64, +} + +impl Base { + fn hash(&self) -> [u8; 32] { + chash(&self.envelope) + } + fn info(&self) -> BaseInfo { + BaseInfo { content_hash: self.hash(), capability: self.decoded_via.clone(), via_fallback: self.via_fallback, tier: self.tier, attested: self.attested, first_unit: self.first_unit } + } + fn unit(&self, u: usize) -> Vec { + match &self.units[u].samples { + Samples::U8(v) => v.clone(), + Samples::I16(_) => panic!("u8 unit expected"), + } + } + fn stage0(&self) -> Vec { + self.units.iter().flat_map(|u| u.samples.canonical_bytes()).collect() + } +} + +struct Ctx { + wai: PathBuf, +} + +impl Ctx { + fn read(&self, rel: &str) -> Vec { + std::fs::read(self.wai.join(rel)).unwrap_or_else(|e| panic!("read {rel}: {e}")) + } +} + +fn units_of(d: IntDecoded) -> Vec { + wai::staged::units_from_decoded(d).expect("an output kind the corpus uses") +} + +/// Decode `payload` through `cap` with `set` (member → path), pinned by `prior` when +/// given: the units, and whether a pin resolved. +fn decode_with>(ctx: &Ctx, cap: &str, payload: &[u8], set: &[(S, String)], prior: Option<&Prior>) -> Vec { + let bytes: Vec<(String, Vec)> = set.iter().map(|(m, p)| (m.as_ref().to_string(), ctx.read(p))).collect(); + let files: BTreeMap = bytes.iter().map(|(m, b)| (m.clone(), b.as_slice())).collect(); + let listing = if files.is_empty() { vec![] } else { wai::pin::listing_from(&files).expect("listing") }; + let held: Vec = if set.len() == 1 && prior.is_some_and(|p| p.form().is_none()) { + vec![HeldBytes::File(files.values().next().copied().unwrap())] + } else if set.is_empty() { + vec![] + } else { + vec![HeldBytes::Set { listing: &listing, files: &files }] + }; + let params = resolve(cap, prior, &held, None).unwrap_or_else(|e| panic!("resolve {cap}: {e}")); + assert_eq!(params.is_pinned(), prior.is_some(), "{cap}: the pin resolved"); + units_of(decode_int(cap, payload, ¶ms).unwrap_or_else(|e| panic!("decode {cap}: {e}"))) +} + +fn tiny_set() -> Vec<(&'static str, String)> { + vec![ + ("entropy.json", format!("{VEC}/int_hyper/tiny_entropy.json")), + ("g_s.whm", format!("{VEC}/int_hyper/tiny_g_s.bin")), + ("h_s.whm", format!("{VEC}/int_hyper/tiny_h_s.bin")), + ] +} + +fn set_pin(ctx: &Ctx, id: &str, set: &[(&'static str, String)], det: &str) -> Prior { + let bytes: Vec<(String, Vec)> = set.iter().map(|(m, p)| (m.to_string(), ctx.read(p))).collect(); + let files: BTreeMap = bytes.iter().map(|(m, b)| (m.clone(), b.as_slice())).collect(); + let listing = wai::pin::listing_from(&files).expect("listing"); + Prior::new(id, sha(&listing), det).with_form(CAP_PRIOR_SET).with_tier(DeclaredTier::DecodeEquivalence) +} + +fn file_pin(ctx: &Ctx, id: &str, path: &str, det: &str) -> Prior { + Prior::new(id, sha(&ctx.read(path)), det).with_tier(DeclaredTier::DecodeEquivalence) +} + +fn tiny_base(ctx: &Ctx, pinned: bool) -> Base { + let payload = ctx.read(&format!("{VEC}/int_hyper/tiny_bitstream.wih1")); + let set = tiny_set(); + let prior = pinned.then(|| set_pin(ctx, "int_hyper/tiny", &set, "int-rans/p16+int-hyper/i64")); + let units = decode_with(ctx, CAP_NEURAL_INT_HYPER, &payload, &set, prior.as_ref()); + let tier = tier::base_tier(CAP_NEURAL_INT_HYPER, prior.as_ref()); + Base { + envelope: envelope("image", CAP_NEURAL_INT_HYPER, None, "int_hyper", prior, None, payload), + decoded_via: CAP_NEURAL_INT_HYPER.into(), + via_fallback: false, + attested: false, + pin_resolved: pinned, + tier, + units, + decode: Decode::Int { set }, + first_unit: 0, + } +} + +fn nk_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_video/video_nk_bitstream.wiv1")); + let set = tiny_set(); + let prior = Some(set_pin(ctx, "int_hyper/tiny", &set, "int-rans/p16+int-hyper/i64")); + let units = decode_with(ctx, CAP_VIDEO_INT_HYPER, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_VIDEO_INT_HYPER, prior.as_ref()), + envelope: envelope("video", CAP_VIDEO_INT_HYPER, None, "int_hyper", prior, None, payload), + decoded_via: CAP_VIDEO_INT_HYPER.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + first_unit: 0, + } +} + +fn audio_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_audio/audio_bitstream.wia1")); + let model = format!("{VEC}/int_audio/audio_model.bin"); + let set = vec![("model.wam", model.clone())]; + let prior = Some(file_pin(ctx, "int_audio/vector", &model, "int-rans/p16+int-audio/i64")); + let units = decode_with(ctx, CAP_AUDIO_INT_CODEC, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_AUDIO_INT_CODEC, prior.as_ref()), + envelope: envelope("audio", CAP_AUDIO_INT_CODEC, None, "int_codec", prior, None, payload), + decoded_via: CAP_AUDIO_INT_CODEC.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + first_unit: 0, + } +} + +fn splat_base(ctx: &Ctx) -> Base { + let payload = ctx.read(&format!("{VEC}/int_splat/splat.wis1")); + let model = format!("{VEC}/int_splat/splat_gs.bin"); + let set = vec![("g_s.whm", model.clone())]; + let prior = Some(file_pin(ctx, "int_splat/vector", &model, "int-rans/p16+int-splat/i64")); + let units = decode_with(ctx, CAP_SPLAT_INT_CODEC, &payload, &set, prior.as_ref()); + Base { + tier: tier::base_tier(CAP_SPLAT_INT_CODEC, prior.as_ref()), + envelope: envelope("volumetric", CAP_SPLAT_INT_CODEC, None, "int_codec", prior, None, payload), + decoded_via: CAP_SPLAT_INT_CODEC.into(), + via_fallback: false, + attested: false, + pin_resolved: true, + units, + decode: Decode::Int { set }, + first_unit: 0, + } +} + +/// A `wai.video.int_motion` GOP of `n` frames of `h × w`, from the moving picture +/// `seed`, starting at time `t0`. +fn gop_base(ctx: &Ctx, h: u32, w: u32, n: u32, seed: u32, t0: u32, q: u32) -> Base { + let frames: Vec> = (0..n).map(|f| picture(h, w, seed, t0 + f)).collect(); + let (payload, recon) = encode_wiv1(&frames, h, w, q); + let units = decode_with::<&str>(ctx, CAP_VIDEO_INT_MOTION, &payload, &[], None); + let mine: Vec = recon.into_iter().map(|f| UnitBuf { shape: (h, w, 3), samples: Samples::U8(f) }).collect(); + assert_eq!(units, mine, "the WIV1 decode is the encoder's reconstruction"); + Base { + tier: tier::base_tier(CAP_VIDEO_INT_MOTION, None), + envelope: envelope("video", CAP_VIDEO_INT_MOTION, None, "int_motion", None, None, payload), + decoded_via: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units, + decode: Decode::Int { set: vec![] }, + first_unit: 0, + } +} + +fn png_base(h: u32, w: u32, seed: u32) -> Base { + let rgb = picture(h, w, seed, 0); + Base { + tier: tier::base_tier(CAP_IMAGE_PNG, None), + envelope: envelope("image", CAP_IMAGE_PNG, None, "png", None, None, png(&rgb, h, w)), + decoded_via: CAP_IMAGE_PNG.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units: vec![UnitBuf { shape: (h, w, 3), samples: Samples::U8(rgb) }], + decode: Decode::Png, + first_unit: 0, + } +} + +fn flac_base(n: u32, ch: u32, rate: u32) -> Base { + let s = pcm(n, ch, rate, 1); + Base { + tier: tier::base_tier(CAP_AUDIO_FLAC, None), + envelope: envelope("audio", CAP_AUDIO_FLAC, None, "flac", None, None, flac(&s, ch, rate)), + decoded_via: CAP_AUDIO_FLAC.into(), + via_fallback: false, + attested: false, + pin_resolved: false, + units: vec![UnitBuf { shape: (n, 1, ch), samples: Samples::I16(s) }], + decode: Decode::Flac, + first_unit: 0, + } +} + +// ───────────────────────── layers ───────────────────────── + +fn below(s: &Samples) -> Below<'_> { + match s { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } +} + +/// A residual layer of `cap` over `base`: `prev`, `layer`, `span`; the units below +/// (`belows`, of shape `below_shape`) refined toward `targets` (of shape `dims`). +#[allow(clippy::too_many_arguments)] +fn layer(cap: &str, base: &[u8; 32], prev: Prev, layer: u8, span: Option, belows: &[Samples], below_shape: (u32, u32, u32), targets: &[Samples], dims: (u32, u32, u32), p: RefineParams) -> (Vec, Vec) { + let kind = wai::staged::unit_kind(cap).expect("a refinement"); + let b: Vec = belows.iter().map(below).collect(); + let t: Vec = targets.iter().map(below).collect(); + let (payload, recon) = wir1::encode(kind, &b, below_shape, &t, dims, &p).unwrap_or_else(|e| panic!("encode: {e}")); + let r = Refines { base: hex(base), prev, layer, span }; + let media = match kind { + UnitKind::Rgb8 => if cap == CAP_VIDEO_INT_REFINE { "video" } else { "image" }, + UnitKind::Pcm => "audio", + UnitKind::Attr => "volumetric", + }; + let env = envelope(media, cap, None, "wir1", None, Some(&r), payload); + (env, recon.into_iter().map(|n| match n { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) }).collect()) +} + +fn params(q: u32, predictor: Predictor, max_symbols: u32) -> RefineParams { + RefineParams { q, predictor, rate: (0, 0), table_per_unit: false, max_symbols } +} + +fn one(h: &[u8; 32]) -> Prev { + Prev::One(hex(h)) +} + +fn runs(hs: &[[u8; 32]]) -> Prev { + let mut r: Vec = Vec::new(); + for h in hs { + match r.last_mut() { + Some(l) if l.producer == hex(h) => l.count += 1, + _ => r.push(PrevRun { producer: hex(h), count: 1 }), + } + } + if r.len() == 1 { Prev::One(r.remove(0).producer) } else { Prev::Runs(r) } +} + +fn u8s(v: Vec) -> Samples { + Samples::U8(v) +} + +/// Rewrite a layer envelope's manifest or payload (for refusal cases). +fn edit(env: &[u8], f: impl FnOnce(&mut Wai)) -> Vec { + let mut w = Wai::from_bytes(env).expect("an envelope"); + f(&mut w); + w.to_bytes().expect("serialise") +} + +// ───────────────────────── cases ───────────────────────── + +#[derive(Clone)] +enum Step { + Offer { file: String, at_ms: u64, evidence: ReceiptEvidence }, + Base { index: usize, at_ms: u64 }, + Expire { at_ms: u64 }, + /// The sink drops the base's chain (§4.4). + Remove { index: usize, at_ms: u64 }, + /// The sink verifies the base's delivery receipt (§10), held or not. + Attest { index: usize, at_ms: u64 }, +} + +struct Case { + name: &'static str, + about: &'static str, + bases: Vec, + files: Vec<(String, Vec)>, + cfg: ChainConfig, + steps: Vec, +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 16, max_age_ms: 1000 }, cost_limit: None, budget: 1 << 30, record_budget: 1 << 20, live: None } +} + +fn offer(file: &str, at_ms: u64) -> Step { + Step::Offer { file: file.into(), at_ms, evidence: ReceiptEvidence::None } +} + +fn base_at(index: usize, at_ms: u64) -> Step { + Step::Base { index, at_ms } +} + +fn rgb_unit(b: &Base, u: usize) -> Samples { + u8s(b.unit(u)) +} + +fn cases(ctx: &Ctx) -> Vec { + let mut out = Vec::new(); + let pid = Predictor::Identity; + + // ---- image ---- + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let t = u8s(picture(16, 16, 1, 0)); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t.clone()], (16, 16, 3), params(12, pid, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (16, 16, 3), &[t], (16, 16, 3), params(3, pid, 4095)); + out.push(Case { name: "image_int_hyper_2l", about: "a pinned wai.neural.int_hyper base, then two identity layers", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = tiny_base(ctx, false); + let bh = b.hash(); + let t = u8s(picture(16, 16, 1, 0)); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t], (16, 16, 3), params(12, pid, 4095)); + out.push(Case { name: "image_int_hyper_unpinned", about: "an unpinned wai.neural.int_hyper base: the layer applies, and no unit holds a tier, so no stage hash is stated", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let t = u8s(picture(32, 32, 2, 0)); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[t.clone()], (32, 32, 3), params(6, Predictor::Up2, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (32, 32, 3), &[t], (32, 32, 3), params(2, pid, 4095)); + out.push(Case { name: "image_up2_from_int_hyper", about: "a 16×16 base, an up2 layer to 32×32, then an identity layer", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = tiny_base(ctx, true); + let bh = b.hash(); + let (l1, r1) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[u8s(picture(31, 31, 3, 0))], (31, 31, 3), params(4, Predictor::Up2, 4095)); + let (l2, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (31, 31, 3), &[u8s(picture(61, 61, 3, 0))], (61, 61, 3), params(4, Predictor::Up2, 4095)); + out.push(Case { name: "image_up2_odd", about: "up2 to an odd size, then up2 from it: 16×16 → 31×31 → 61×61", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + { + let b = png_base(16, 16, 7); + let bh = b.hash(); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], (16, 16, 3), &[u8s(picture(32, 32, 7, 1))], (32, 32, 3), params(1, Predictor::Up2, 4095)); + out.push(Case { name: "image_png_up2", about: "a PNG base (a stage-0 fixture here), an up2 layer at q = 1", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- video ---- + { + let gops: Vec = (0..3).map(|g| gop_base(ctx, 16, 16, 4, 9, 4 * g, 10)).collect(); + let mut files = Vec::new(); + let mut steps = Vec::new(); + for (g, b) in gops.iter().enumerate() { + steps.push(base_at(g, 0)); + let bh = b.hash(); + let mut hs = Vec::new(); + let mut mid = Vec::new(); + for f in 0..4u32 { + let t = u8s(picture(16, 16, 9, 4 * g as u32 + f)); + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(b, f as usize)], (16, 16, 3), &[t], (16, 16, 3), params(4, pid, 4095)); + hs.push(chash(&l)); + mid.push(r.into_iter().next().unwrap()); + let name = format!("g{g}_L1_f{f}.wai"); + steps.push(offer(&name, 1)); + files.push((name, l)); + } + let targets: Vec = (0..4).map(|f| u8s(picture(16, 16, 9, 4 * g as u32 + f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (16, 16, 3), &targets, (16, 16, 3), params(1, pid, 4095)); + let name = format!("g{g}_L2.wai"); + steps.push(offer(&name, 2)); + files.push((name, l2)); + } + out.push(Case { name: "video_int_motion_gop_perframe", about: "three closed-GOP wai.video.int_motion bases of four frames; a layer 1 per frame; a layer 2 per GOP whose prev is the run of its frames' producers", bases: gops, files, cfg: cfg(), steps }); + } + { + let b = gop_base(ctx, 16, 16, 4, 11, 0, 10); + let bh = b.hash(); + let mut files = Vec::new(); + let mut hs = Vec::new(); + let mut mid = Vec::new(); + let mut steps = vec![base_at(0, 0)]; + for f in 0..4u32 { + let t = u8s(picture(16, 16, 11, f)); + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&b, f as usize)], (16, 16, 3), &[t], (16, 16, 3), params(4, pid, 4095)); + hs.push(chash(&l)); + mid.push(r.into_iter().next().unwrap()); + let name = format!("L1_f{f}.wai"); + if f != 2 { + steps.push(offer(&name, 1)); + } + files.push((name, l)); + } + let targets: Vec = (0..4).map(|f| u8s(picture(16, 16, 11, f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (16, 16, 3), &targets, (16, 16, 3), params(1, pid, 4095)); + files.push(("L2.wai".into(), l2)); + steps.push(offer("L2.wai", 2)); + steps.push(Step::Expire { at_ms: 1003 }); + out.push(Case { name: "video_mixed_stage_l2", about: "frame 2's layer 1 is withheld: the run-form layer 2 applies to frames 0, 1 and 3, and frame 2 holds it to the bound (layer-gap)", bases: vec![b], files, cfg: cfg(), steps }); + } + { + let b = nk_base(ctx); + let bh = b.hash(); + let belows: Vec = (0..5).map(|f| rgb_unit(&b, f)).collect(); + let targets: Vec = (0..5).map(|f| u8s(picture(16, 16, 13, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (16, 16, 3), &targets, (16, 16, 3), RefineParams { table_per_unit: true, ..params(4, pid, 4095) }); + out.push(Case { name: "video_int_hyper_keyframe", about: "a pinned wai.video.int_hyper clip (keyframe 1), then one layer over its five frames with a table per unit", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- audio ---- + { + let b = audio_base(ctx); + let bh = b.hash(); + let n = b.units[0].shape.0; + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (n, 1, 1), &[Samples::I16(pcm(n, 1, 16_000, 2))], (n, 1, 1), params(16, pid, 4095)); + out.push(Case { name: "audio_int_codec_1l", about: "a pinned wai.audio.int_codec base, then an identity layer", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = flac_base(400, 2, 8000); + let bh = b.hash(); + let (l1, r1) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (400, 1, 2), &[Samples::I16(pcm(800, 2, 16_000, 1))], (800, 1, 2), + RefineParams { rate: (2, 1), ..params(8, Predictor::Rate, 4095) }); + let (l2, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&chash(&l1)), 2, None, &r1, (800, 1, 2), &[Samples::I16(pcm(2400, 2, 48_000, 1))], (2400, 1, 2), + RefineParams { rate: (3, 1), ..params(2, Predictor::Rate, 4095) }); + out.push(Case { name: "audio_flac_rate_ladder", about: "a stereo FLAC base at 8 kHz (a stage-0 fixture here), a rate layer to 16 kHz, then one to 48 kHz", bases: vec![b], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2)] }); + } + + // ---- splat ---- + { + let b = splat_base(ctx); + let bh = b.hash(); + let sh = b.units[0].shape; + let target: Vec = b.unit(0).iter().enumerate().map(|(i, &v)| (v as i32 + ((i as i32 * 37) % 23) - 11).clamp(0, 255) as u8).collect(); + let (l1, _) = layer(CAP_SPLAT_INT_REFINE, &bh, one(&bh), 1, None, &[rgb_unit(&b, 0)], sh, &[u8s(target)], sh, params(3, pid, 4095)); + out.push(Case { name: "splat_attr_1l", about: "a pinned wai.splat.int_codec base, then an identity layer over its attribute tensor", bases: vec![b], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + + // ---- refusals, on a two-frame 8×8 GOP ---- + let small = |seed: u32| gop_base(ctx, 8, 8, 2, seed, 0, 8); + let l1_of = |b: &Base, seed: u32, q: u32| -> (Vec, Vec) { + let bh = b.hash(); + let belows: Vec = (0..2).map(|f| rgb_unit(b, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed, f))).collect(); + layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(q, pid, 4095)) + }; + let l2_of = |b: &Base, l1: &[u8], mid: &[Samples], seed: u32| -> Vec { + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed, f + 7))).collect(); + layer(CAP_VIDEO_INT_REFINE, &b.hash(), one(&chash(l1)), 2, None, mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)).0 + }; + let simple = |name: &'static str, about: &'static str, b: Base, files: Vec<(String, Vec)>, steps: Vec| Case { name, about, bases: vec![b], files, cfg: cfg(), steps }; + { + let (a, other) = (small(20), small(21)); + let (lo, _) = l1_of(&other, 22, 4); + out.push(simple("wrong_base", "a layer of another stage set, whose base this sink never decodes: held for its base, then base-absent at the bound", a, vec![("L1_other.wai".into(), lo)], + vec![base_at(0, 0), offer("L1_other.wai", 1), Step::Expire { at_ms: 1002 }])); + } + { + let a = small(23); + let (l1, _) = l1_of(&a, 24, 4); + out.push(Case { name: "refinement_without_base", about: "a layer whose base never arrives: held, then base-absent at the bound", bases: vec![a], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![offer("L1.wai", 0), Step::Expire { at_ms: 1001 }] }); + } + { + let a = small(25); + let (l1, mid) = l1_of(&a, 26, 4); + let l2 = l2_of(&a, &l1, &mid, 26); + let wrong = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&a.hash()), layer: 2, span: None }.to_value())); + out.push(simple("wrong_prev", "a layer 2 whose prev names the base, not the layer 1 the units were refined by: prev-mismatch", a, + vec![("L1.wai".into(), l1), ("L2_wrong_prev.wai".into(), wrong)], vec![base_at(0, 0), offer("L1.wai", 1), offer("L2_wrong_prev.wai", 2)])); + } + { + let a = small(27); + let (l1, mid) = l1_of(&a, 28, 4); + let l2 = l2_of(&a, &l1, &mid, 28); + out.push(simple("missing_layer", "a layer 2 whose layer 1 never arrives: held for each unit, then layer-gap at the bound", a, vec![("L2.wai".into(), l2)], + vec![base_at(0, 0), offer("L2.wai", 1), Step::Expire { at_ms: 1002 }])); + } + { + let a = small(29); + let (la, _) = l1_of(&a, 30, 4); + let (lb, _) = l1_of(&a, 31, 4); + out.push(Case { name: "conflict_held", about: "two different layers 1 held for the base and released together: neither applies, and both units are frozen", bases: vec![a], + files: vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], cfg: cfg(), steps: vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2)] }); + } + { + let a = small(32); + let (la, mid) = l1_of(&a, 33, 4); + let (lb, _) = l1_of(&a, 34, 4); + let l2 = l2_of(&a, &la, &mid, 33); + out.push(simple("conflict_after_apply", "layer 1a applies; a different layer 1b conflicts and freezes the units at stage 1; a layer 2 over 1a is then refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb), ("L2.wai".into(), l2)], vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), offer("L2.wai", 3)])); + } + { + let a = small(73); + let (l1, mid) = l1_of(&a, 74, 4); + let l2 = l2_of(&a, &l1, &mid, 74); + out.push(Case { name: "pending_l1_l2", about: "layers 2 and 1 both arrive before their base: released together, layer 1 applies and releases layer 2, which applies", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], cfg: cfg(), steps: vec![offer("L2.wai", 0), offer("L1.wai", 1), base_at(0, 2)] }); + } + { + let a = small(35); + let (l1, mid) = l1_of(&a, 36, 4); + let l2 = l2_of(&a, &l1, &mid, 36); + out.push(simple("out_of_order", "layer 2 arrives before layer 1: held, and applied when layer 1 releases it; the stages equal an in-order arrival", a, + vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], vec![base_at(0, 0), offer("L2.wai", 1), offer("L1.wai", 2)])); + } + { + let mut a = small(37); + a.envelope = edit(&a.envelope, |w| { + w.manifest.model_requirement.capability = wai::codecs::CAP_VIDEO_VVC.into(); + w.manifest.model_requirement.fallback = Some(CAP_VIDEO_INT_MOTION.into()); + }); + a.via_fallback = true; + let (l1, _) = l1_of(&a, 38, 4); + out.push(simple("base_via_fallback", "the base was decoded through its manifest's fallback: base-via-fallback", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(39); + let small_units: Vec = (0..2).map(|f| u8s(picture(4, 4, 40, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &small_units, (4, 4, 3), &small_units, (4, 4, 3), params(4, pid, 4095)); + out.push(simple("geometry_mismatch", "a layer coded for 4×4 frames over 8×8 frames: geometry-mismatch for each unit", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(41); + let (l1, _) = l1_of(&a, 42, 4); + let cut = edit(&l1, |w| { + let n = w.payload.len(); + w.payload.truncate(n - 3); + }); + out.push(simple("payload_truncated", "a WIR1 payload three bytes short: payload-malformed (stream_length)", a, vec![("L1.wai".into(), cut)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(43); + let (l1, _) = l1_of(&a, 44, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let at = 44 + 4 * (len / 2); + w.payload[at..at + 4].copy_from_slice(&0u32.to_le_bytes()); + }); + out.push(simple("cdf_malformed", "a table entry set to 0 in mid-table: payload-malformed (cdf_malformed)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(45); + let (l1, _) = l1_of(&a, 46, 4); + let bad = edit(&l1, |w| w.payload[32..36].copy_from_slice(&0u32.to_le_bytes())); + out.push(simple("q_zero", "a step q of 0: payload-malformed (field_range)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(47); + let (l1, _) = l1_of(&a, 48, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let lens_at = 44 + 4 * len; + let first = u32::from_le_bytes(w.payload[lens_at..lens_at + 4].try_into().unwrap()); + w.payload[lens_at..lens_at + 4].copy_from_slice(&(first + 4).to_le_bytes()); + let streams_at = lens_at + 8; + let end = streams_at + first as usize; + w.payload.splice(end..end, [0u8; 4]); + }); + out.push(simple("stream_noncanonical", "a word after unit 0's last symbol: payload-malformed (stream_noncanonical)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(49); + let (l1, _) = l1_of(&a, 50, 4); + let bad = edit(&l1, |w| { + let len = u16::from_le_bytes([w.payload[40], w.payload[41]]) as usize; + let lens_at = 44 + 4 * len; + let first = u32::from_le_bytes(w.payload[lens_at..lens_at + 4].try_into().unwrap()) as usize; + let streams_at = lens_at + 8; + // Unit 0's stream becomes the state 2^31 alone: it codes no symbol. + w.payload.splice(streams_at..streams_at + first, [0, 0, 0, 0x80, 0, 0, 0, 0]); + w.payload[lens_at..lens_at + 4].copy_from_slice(&8u32.to_le_bytes()); + }); + out.push(simple("stream_exhausted", "unit 0's stream codes no symbol: it runs out at the first, payload-malformed (stream_exhausted)", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(51); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 52, f).iter().enumerate().map(|(i, _)| if i % 2 == 0 { 0 } else { 255 }).collect())).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(200, pid, 4095)); + out.push(simple("clamp_extremes", "targets at 0 and 255 with q = 200: residuals that overshoot are clamped to the u8 range", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(53); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 54, f).iter().map(|&v| 255 - v).collect())).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(1, pid, 1)); + out.push(simple("bypass_extremes", "a table of one symbol at q = 1: nearly every residual takes the bypass escape, and the layer is exact", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(55); + let (l1, _) = l1_of(&a, 56, 4); + let bad = edit(&l1, |w| w.manifest.refines = Some(json!({"base": hex(&a.hash()), "prev": hex(&a.hash()), "layer": 0}))); + out.push(simple("layer_zero", "refines.layer 0: refines-malformed", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(57); + let (l1, _) = l1_of(&a, 58, 4); + let bad = edit(&l1, |w| w.manifest.refines = Some(json!({"base": hex(&a.hash()).to_uppercase(), "prev": hex(&a.hash()).to_uppercase(), "layer": 1}))); + out.push(simple("refines_malformed", "a base hash in upper case: refines-malformed", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(59); + let (l1, _) = l1_of(&a, 60, 4); + let bad = edit(&l1, |w| w.manifest.model_requirement.capability = CAP_VIDEO_INT_MOTION.into()); + out.push(simple("capability_not_refinement", "refines on a wai.video.int_motion envelope: capability-not-refinement", a, vec![("L1.wai".into(), bad)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let a = small(61); + let belows: Vec = (0..2).map(|f| rgb_unit(&a, f)).collect(); + let (l1, _) = layer(CAP_IMAGE_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &belows, (8, 8, 3), &belows, (8, 8, 3), params(4, pid, 4095)); + out.push(simple("base_not_admitted", "a wai.image.int_refine layer over a wai.video.int_motion base: base-not-admitted", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let mut a = small(63); + a.attested = true; + let (l1, _) = l1_of(&a, 64, 4); + out.push(simple("inclusion_only_evidence", "an attested base: the layer verified by inclusion only is refinement-unattested; the same layer delivered-verified applies", a, + vec![("L1.wai".into(), l1)], + vec![base_at(0, 0), Step::Offer { file: "L1.wai".into(), at_ms: 1, evidence: ReceiptEvidence::InclusionVerified }, + Step::Offer { file: "L1.wai".into(), at_ms: 2, evidence: ReceiptEvidence::DeliveredVerified }])); + } + { + let a = small(65); + let (l1, _) = l1_of(&a, 66, 4); + out.push(Case { name: "cost_over_limit", about: "a sink whose cost limit is 100 output bytes: the 384-byte layer is cost-over-limit", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: 100, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(67); + let bh = a.hash(); + let mut files = Vec::new(); + for f in 0..2u32 { + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 68, f))], (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1_f{f}.wai"), l)); + } + out.push(Case { name: "deadline_missed", about: "units presented at 140 and 180 with a 30 ms lead: frame 0's layer arrives at 120 (late), frame 1's at 120 (in time)", bases: vec![a], files, + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 40 }, lead_ms: vec![30] }), ..cfg() }, + steps: vec![base_at(0, 0), offer("L1_f0.wai", 120), offer("L1_f1.wai", 120)] }); + } + { + let a = small(69); + let (la, _) = l1_of(&a, 70, 4); + let (lb, _) = l1_of(&a, 71, 4); + out.push(Case { name: "hold_bound_count", about: "a bound of one held envelope: the first layer is held for its base, the second is base-absent on arrival", bases: vec![a], + files: vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], cfg: ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }, + steps: vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2)] }); + } + + // ---- §3 and step 1 ---- + let refusals = |name: &'static str, about: &'static str, a: Base, files: Vec<(String, Vec)>| { + let mut steps = vec![base_at(0, 0)]; + steps.extend(files.iter().enumerate().map(|(i, (f, _))| offer(f, i as u64 + 1))); + Case { name, about, bases: vec![a], files, cfg: cfg(), steps } + }; + { + let a = small(101); + let (l1, _) = l1_of(&a, 102, 4); + let none = edit(&l1, |w| w.manifest.refines = None); + let fb = edit(&l1, |w| w.manifest.model_requirement.fallback = Some(CAP_VIDEO_INT_MOTION.into())); + out.push(refusals("step1_refusals", "a refinement-only envelope with no refines (refines-malformed), and one whose manifest names a fallback (capability-not-refinement)", a, + vec![("L1_no_refines.wai".into(), none), ("L1_fallback.wai".into(), fb)])); + } + { + let a = small(103); + let bh = hex(&a.hash()); + let (l1, _) = l1_of(&a, 104, 4); + let other = "e".repeat(64); + let with = |r: Value| edit(&l1, |w| w.manifest.refines = Some(r)); + out.push(refusals("refines_rules", "§3's rules, each broken once: a layer 1 naming a producer other than the base; prev an array of one run; two adjacent runs naming one producer; runs not summing to the span's count. Each is refines-malformed", a, vec![ + ("L1_other_producer.wai".into(), with(json!({"base": bh, "prev": other, "layer": 1}))), + ("L1_one_run.wai".into(), with(json!({"base": bh, "prev": [{"producer": bh, "count": 2}], "layer": 1}))), + ("L2_adjacent_runs.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 1}, {"producer": other, "count": 1}], "layer": 2}))), + ("L2_runs_not_span.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 1}, {"producer": bh, "count": 2}], "layer": 2, "span": {"from": 0, "count": 2}}))), + ])); + } + { + let a = small(105); + let (l1, _) = l1_of(&a, 106, 4); + let text = |e: &[u8]| String::from_utf8(wai::container::manifest_bytes(e).expect("WAI1").to_vec()).expect("UTF-8"); + let t = text(&l1); + let spelled = |from: &str, to: &str| { + let u = t.replacen(from, to, 1); + assert_ne!(u, t, "{from}"); + raw_manifest(&l1, &u) + }; + let with_span = raw_manifest(&l1, &t.replacen("\"layer\":1", "\"layer\":1,\"span\":{\"from\":0,\"count\":2}", 1)); + let ts = text(&with_span); + let span_null = raw_manifest(&l1, &t.replacen("\"layer\":1", "\"layer\":1,\"span\":null", 1)); + out.push(refusals("integer_spellings", "an integer member of refines written as -0, 1.0, 1e0 or -0.0 is not an integer (SPEC §2): refines-malformed; span null is span absent, and that layer applies", a, vec![ + ("L1_layer_minus0.wai".into(), spelled("\"layer\":1", "\"layer\":-0")), + ("L1_layer_1.0.wai".into(), spelled("\"layer\":1", "\"layer\":1.0")), + ("L1_layer_1e0.wai".into(), spelled("\"layer\":1", "\"layer\":1e0")), + ("L1_from_minus0.wai".into(), raw_manifest(&l1, &ts.replacen("\"from\":0", "\"from\":-0", 1))), + ("L1_count_2.0.wai".into(), raw_manifest(&l1, &ts.replacen("\"count\":2", "\"count\":2.0", 1))), + ("L1_span_null.wai".into(), span_null), + ])); + } + { + let a = small(148); + let (l1, _) = l1_of(&a, 149, 4); + let t = String::from_utf8(wai::container::manifest_bytes(&l1).expect("WAI1").to_vec()).expect("UTF-8"); + let with = |u: String| { + assert_ne!(u, t); + raw_manifest(&l1, &u) + }; + let deep = format!("{},\"zz\":{}{}}}", &t[..t.len() - 1], "[".repeat(127), "]".repeat(127)); + let ok_deep = format!("{},\"zz\":{}{}}}", &t[..t.len() - 1], "[".repeat(126), "]".repeat(126)); + out.push(refusals("unreadable_envelopes", "envelopes a reader of SPEC §2 and §3 refuses, so a sink never reaches §4.1: a member named twice (in refines, and at the top), NaN, a number past the float range, a lone surrogate, a byte-order mark, a manifest nested 128 deep, a manifest with no media; an encrypted layer; then the layer with a manifest nested 127 deep, which is read and applies", a, vec![ + ("dup_in_refines.wai".into(), with(t.replacen("\"layer\":1", "\"layer\":2,\"layer\":1", 1))), + ("dup_top.wai".into(), with(t.replacen("\"media\":", "\"zz\":1,\"zz\":1,\"media\":", 1))), + ("nan.wai".into(), with(format!("{},\"zz\":NaN}}", &t[..t.len() - 1]))), + ("1e400.wai".into(), with(format!("{},\"zz\":1e400}}", &t[..t.len() - 1]))), + ("lone_surrogate.wai".into(), with(format!("{},\"zz\":\"\\ud800\"}}", &t[..t.len() - 1]))), + ("bom.wai".into(), with(format!("\u{feff}{t}"))), + ("deep_128.wai".into(), with(deep)), + ("no_media.wai".into(), with(t.replacen("\"media\":\"video\",", "", 1))), + ("encrypted.wai".into(), edit(&l1, |w| w.manifest.security = Some(serde_json::from_value(json!({"encryption": "aes-128-gcm", "kid": 1})).expect("security")))), + ("deep_127.wai".into(), with(ok_deep)), + ])); + } + // ---- step 2 ---- + { + let a = small(107); + let bh = hex(&a.hash()); + let (l1, _) = l1_of(&a, 108, 4); + let other = "e".repeat(64); + let with = |r: Value| edit(&l1, |w| w.manifest.refines = Some(r)); + out.push(refusals("refines_against_base", "refines well formed alone and malformed once the base is known (step 2): a span past the base's two units; runs summing to three units with no span", a, vec![ + ("L1_span_past.wai".into(), with(json!({"base": bh, "prev": bh, "layer": 1, "span": {"from": 1, "count": 2}}))), + ("L2_runs_past.wai".into(), with(json!({"base": bh, "prev": [{"producer": other, "count": 2}, {"producer": bh, "count": 1}], "layer": 2}))), + ])); + } + // ---- step 6 ---- + { + let a = small(109); + let (l1, _) = l1_of(&a, 110, 4); + let bh = hex(&a.hash()); + let one_unit = edit(&l1, |w| w.manifest.refines = Some(json!({"base": bh, "prev": bh, "layer": 1, "span": {"from": 0, "count": 1}}))); + let pay = |f: &dyn Fn(&mut Vec)| edit(&l1, |w| f(&mut w.payload)); + out.push(refusals("wir1_faults", "step 6, each payload fault once: a two-unit payload over a one-unit span (field_range); bad_magic; version; reserved_nonzero; an element of 2 bytes for RGB8 (field_range); a frame past 2^26 elements (too_large); a byte after the last stream (trailing). Each is payload-malformed with its code", a, vec![ + ("L1_unit_count.wai".into(), one_unit), + ("L1_bad_magic.wai".into(), pay(&|p| p[0] = b'X')), + ("L1_version.wai".into(), pay(&|p| p[4] = 2)), + ("L1_reserved.wai".into(), pay(&|p| p[42] = 1)), + ("L1_elem.wai".into(), pay(&|p| p[5] = 2)), + ("L1_too_large.wai".into(), pay(&|p| { + p[12..16].copy_from_slice(&8192u32.to_le_bytes()); + p[16..20].copy_from_slice(&8192u32.to_le_bytes()); + })), + ("L1_trailing.wai".into(), pay(&|p| p.push(0))), + ])); + } + { + let a = small(111); + let (l1, _) = l1_of(&a, 112, 4); + out.push(Case { name: "cost_over_limit_symbols", about: "a sink whose cost limit is 64 symbols: the 384-symbol layer is cost-over-limit (symbols)", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: u64::MAX, symbols: 64, working_bytes: u64::MAX }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(113); + let (l1, _) = l1_of(&a, 114, 4); + out.push(Case { name: "cost_over_limit_working", about: "a sink whose cost limit is 100 working bytes: a layer that holds its tables, its stream index and one 192-byte frame is cost-over-limit (working bytes)", bases: vec![a], files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { cost_limit: Some(Wir1Cost { output_bytes: u64::MAX, symbols: u64::MAX, working_bytes: 100 }), ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(115); + let (l1, _) = l1_of(&a, 116, 4); + let state0: u64 = a.units.iter().map(|u| u.samples.byte_len()).sum(); + out.push(Case { name: "chain_over_limit", about: "a chain budget one byte short of the base's units and the layer's payload, declared output and working bytes and 32 bytes a unit: chain-over-limit, before any stream is decoded", bases: vec![a], + files: vec![("L1.wai".into(), l1.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1) - 1, ..cfg() }, steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(117); + let (l1, mid) = l1_of(&a, 118, 4); + let l2 = l2_of(&a, &l1, &mid, 118); + let state0: u64 = a.units.iter().map(|u| u.samples.byte_len()).sum(); + out.push(Case { name: "chain_budget_retained", about: "a budget that holds the base and either layer, not both: layer 2, held, is retained, so layer 1 is chain-over-limit; once layer 2 expires and is dropped, layer 1 applies", bases: vec![a], + files: vec![("L1.wai".into(), l1.clone()), ("L2.wai".into(), l2.clone())], cfg: ChainConfig { budget: state0 + reserve(&l1).max(reserve(&l2)) + 2 * 64, ..cfg() }, + steps: vec![base_at(0, 0), offer("L2.wai", 1), offer("L1.wai", 2), Step::Expire { at_ms: 1002 }, offer("L1.wai", 1003)] }); + } + { + let a = small(150); + let bh = a.hash(); + let (l1, r1) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 1, count: 1 }), &[rgb_unit(&a, 1)], (8, 8, 3), &[u8s(picture(8, 8, 151, 1))], (8, 8, 3), params(4, pid, 4095)); + let (l1x, r1x) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&a, 0)], (8, 8, 3), &[u8s(picture(8, 8, 151, 0))], (8, 8, 3), params(4, pid, 4095)); + let mid = vec![r1x[0].clone(), r1[0].clone()]; + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 152, f))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&[chash(&l1x), chash(&l1)]), 2, None, &mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)); + // The state once layer 2 is applied to unit 1 and held for unit 0, then room for + // layer 1x and no more. + let mut probe = StageSink::new(cfg()); + probe.add_base(a.info(), a.units.clone()); + for e in [&l1, &l2] { + probe.offer(e, 0, ReceiptEvidence::None).expect("WAI1"); + } + let budget = probe.chain(&bh).expect("decoded").state_bytes() + reserve(&l1x); + out.push(Case { name: "budget_partly_applied", about: "layer 2 is applied to unit 1 and held for unit 0, so it stays retained; unit 1's share has left its reserve, which leaves room for layer 1x over unit 0 at a budget with no byte to spare; 1x applies and releases layer 2 for unit 0", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2), ("L1x.wai".into(), l1x)], cfg: ChainConfig { budget, ..cfg() }, + steps: vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2), offer("L1x.wai", 3)] }); + } + // ---- steps 7 and 8 ---- + { + let a = small(119); + let (la, mid) = l1_of(&a, 120, 4); + let (lb, _) = l1_of(&a, 121, 4); + let l2 = l2_of(&a, &la, &mid, 120); + let l3 = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&chash(&l2)), layer: 3, span: None }.to_value())); + out.push(simple("frozen_refuses_held", "a layer 3 held for both units; layer 1a applies, then layer 1b conflicts: the units freeze, and the held layer 3 is refused for them (layer-conflict)", a, + vec![("L3.wai".into(), l3), ("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L3.wai", 1), offer("L1a.wai", 2), offer("L1b.wai", 3)])); + } + { + let a = small(122); + let (l1, mid) = l1_of(&a, 123, 4); + let l2a = l2_of(&a, &l1, &mid, 124); + let l2b = l2_of(&a, &l1, &mid, 125); + out.push(simple("batch_conflict_release", "layers 2a and 2b, both over layer 1, held; layer 1 applies and releases them together: they conflict in the batch, and the units freeze at stage 1", a, + vec![("L2a.wai".into(), l2a), ("L2b.wai".into(), l2b), ("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L2a.wai", 1), offer("L2b.wai", 2), offer("L1.wai", 3)])); + } + { + let a = small(126); + let (l1, mid) = l1_of(&a, 127, 4); + let l2a = l2_of(&a, &l1, &mid, 128); + let l2b = l2_of(&a, &l1, &mid, 129); + out.push(Case { name: "layer_gap_on_arrival", about: "a bound of one held envelope: layer 2a is held for both units, and layer 2b, which would be held too, is refused on arrival (layer-gap)", bases: vec![a], + files: vec![("L2a.wai".into(), l2a), ("L2b.wai".into(), l2b)], cfg: ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }, + steps: vec![base_at(0, 0), offer("L2a.wai", 1), offer("L2b.wai", 2)] }); + } + { + let a = small(130); + let (la, mid) = l1_of(&a, 131, 4); + let (lb, _) = l1_of(&a, 132, 4); + let l2 = l2_of(&a, &la, &mid, 131); + out.push(simple("held_then_prev_mismatch", "a layer 2 over layer 1a is held; layer 1b applies instead and releases it: the units' producer is 1b, so layer 2 is prev-mismatch", a, + vec![("L2.wai".into(), l2), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L2.wai", 1), offer("L1b.wai", 2)])); + } + // ---- steps 9 and 10 ---- + { + let a = small(133); + let quarter: Vec = (0..2).map(|f| u8s(picture(4, 4, 134, f))).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 134, f))).collect(); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &a.hash(), one(&a.hash()), 1, None, &quarter, (4, 4, 3), &targets, (8, 8, 3), params(4, Predictor::Up2, 4095)); + out.push(simple("up2_geometry_mismatch", "an up2 layer to 8×8 reads 4×4 frames; the units are 8×8: geometry-mismatch", a, vec![("L1.wai".into(), l1)], vec![base_at(0, 0), offer("L1.wai", 1)])); + } + { + let b = flac_base(400, 2, 8000); + let bh = b.hash(); + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[Samples::I16(pcm(300, 2, 8000, 2))], (300, 1, 2), &[Samples::I16(pcm(600, 2, 16_000, 2))], (600, 1, 2), + RefineParams { rate: (2, 1), ..params(8, Predictor::Rate, 4095) }); + out.push(Case { name: "rate_geometry_mismatch", about: "a rate layer to 600 samples reads 300; the base holds 400: geometry-mismatch", bases: vec![b], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let b = flac_base(480, 1, 48_000); + let bh = b.hash(); + let (l1, _) = layer(CAP_AUDIO_INT_REFINE, &bh, one(&bh), 1, None, &[b.units[0].samples.clone()], (480, 1, 1), &[Samples::I16(pcm(160, 1, 16_000, 3))], (160, 1, 1), + RefineParams { rate: (1, 3), ..params(4, Predictor::Rate, 4095) }); + out.push(Case { name: "rate_downsampling", about: "a mono FLAC base at 48 kHz and a rate layer to 16 kHz (rate 1/3), the predictor reading every third sample", bases: vec![b], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1.wai", 1)] }); + } + { + let a = small(135); + let (l1, mid) = l1_of(&a, 136, 4); + let l2 = l2_of(&a, &l1, &mid, 136); + out.push(Case { name: "deadline_held_layer", about: "units presented at 200 and 240, leads 10 and 30: layer 2 arrives at 175 and is held; layer 1 at 180 applies to both and releases it; layer 2 is late for unit 0 (deadline-missed) and in time for unit 1", bases: vec![a], + files: vec![("L1.wai".into(), l1), ("L2.wai".into(), l2)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 100 }, lead_ms: vec![10, 30] }), ..cfg() }, + steps: vec![base_at(0, 0), offer("L2.wai", 175), offer("L1.wai", 180)] }); + } + { + let mut a = gop_base(ctx, 8, 8, 2, 137, 0, 8); + let mut b = gop_base(ctx, 8, 8, 2, 137, 2, 8); + a.first_unit = 0; + b.first_unit = 2; + let bh = b.hash(); + let (lb, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&b, 0)], (8, 8, 3), &[u8s(picture(8, 8, 138, 2))], (8, 8, 3), params(4, pid, 4095)); + out.push(Case { name: "live_multi_base", about: "a clip split into two bases of two frames on one clock (the second's first unit is the clip's unit 2, presented at 200): its layer for that unit arriving at 150 with a 30 ms lead is in time", bases: vec![a, b], + files: vec![("L1_second.wai".into(), lb)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 0 }, lead_ms: vec![30] }), ..cfg() }, + steps: vec![base_at(0, 0), base_at(1, 0), offer("L1_second.wai", 150)] }); + } + // ---- bases and releases ---- + { + let a = small(155); + let (l1, mid) = l1_of(&a, 156, 4); + let l2 = l2_of(&a, &l1, &mid, 156); + let l4 = edit(&l2, |w| w.manifest.refines = Some(Refines { base: hex(&a.hash()), prev: one(&[7; 32]), layer: 4, span: None }.to_value())); + out.push(simple("remove_readd_replay", "layers 1 and 2 apply and a layer 4 is held; the sink drops the base's chain, discarding layer 4 (base-absent), and decodes the base again: each unit starts at stage 0 with its record, and layers 1 and 2 offered again replay it", a, + vec![("L1.wai".into(), l1), ("L2.wai".into(), l2), ("L4.wai".into(), l4)], + vec![base_at(0, 0), offer("L1.wai", 1), offer("L2.wai", 2), offer("L4.wai", 3), Step::Remove { index: 0, at_ms: 4 }, base_at(0, 5), offer("L1.wai", 6), offer("L2.wai", 7)])); + } + { + let a = small(157); + let (la, mid) = l1_of(&a, 158, 4); + let (lb, _) = l1_of(&a, 159, 4); + let l2 = l2_of(&a, &la, &mid, 158); + out.push(simple("remove_readd_frozen", "layer 1a applies and 1b conflicts, freezing the units at stage 1; the chain is dropped, 1b arrives and is held for the base, and the base is decoded again: the units start frozen at stage 0 and may only replay their record, in order and no further, so 1b and a layer 2 are refused, and 1a replays and presents the frozen stage again; then layer 2 is still refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb), ("L2.wai".into(), l2)], + vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), Step::Remove { index: 0, at_ms: 3 }, offer("L1b.wai", 4), base_at(0, 5), offer("L2.wai", 6), offer("L1a.wai", 7), offer("L2.wai", 8)])); + } + { + let a = small(163); + let (la, _) = l1_of(&a, 164, 4); + let (lb, _) = l1_of(&a, 165, 4); + out.push(simple("remove_readd_batch_frozen", "layers 1a and 1b, held for the base, conflict in one batch and freeze the units with nothing applied; the chain is dropped and the base decoded again: the frozen flags are part of the record, so 1a is refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], + vec![offer("L1a.wai", 0), offer("L1b.wai", 1), base_at(0, 2), Step::Remove { index: 0, at_ms: 3 }, base_at(0, 4), offer("L1a.wai", 5)])); + } + { + // Three bases; a record budget with room for one base's records (32 bytes and, + // for each of its two units, 8 + 32). + let bases: Vec = (0..3).map(|i| gop_base(ctx, 8, 8, 2, 170 + i, 0, 8)).collect(); + let mut files = Vec::new(); + for (i, b) in bases.iter().enumerate() { + let bh = b.hash(); + for (tag, seed) in [("a", 180), ("b", 190)] { + let belows: Vec = (0..2).map(|f| rgb_unit(b, f)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, seed + i as u32, f))).collect(); + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, None, &belows, (8, 8, 3), &targets, (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1{tag}_{i}.wai"), l)); + } + } + let one_base = 32 + 2 * (8 + 32); + let o = |f: &str, at| offer(f, at); + out.push(Case { name: "record_budget", about: "a record budget with room for one dropped base's records: bases 0 and 1 each apply a layer 1 and are dropped, so base 0, dropped first, is forgotten; decoded again it starts with no history and another layer 1 applies, while base 1 keeps its record and the other layer conflicts. Base 2, dropped with nothing recorded, keeps nothing and costs no room", + bases, files, cfg: ChainConfig { record_budget: one_base, ..cfg() }, + steps: vec![ + base_at(2, 0), Step::Remove { index: 2, at_ms: 1 }, + base_at(0, 2), o("L1a_0.wai", 3), base_at(1, 4), o("L1a_1.wai", 5), + Step::Remove { index: 0, at_ms: 6 }, Step::Remove { index: 1, at_ms: 7 }, + base_at(0, 8), o("L1b_0.wai", 9), base_at(1, 10), o("L1b_1.wai", 11), + ] }); + } + { + let a = small(160); + let (la, _) = l1_of(&a, 161, 4); + let (lb, _) = l1_of(&a, 162, 4); + out.push(simple("remove_readd_diverge", "layer 1a applies; the chain is dropped, a different layer 1b arrives and is held for the base, and the base is decoded again: 1b differs from the producer each unit's record holds at stage 1, so it conflicts and freezes the units; 1a, the recorded producer, still replays", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], + vec![base_at(0, 0), offer("L1a.wai", 1), Step::Remove { index: 0, at_ms: 2 }, offer("L1b.wai", 3), base_at(0, 4), offer("L1a.wai", 5)])); + } + { + let a = small(139); + let (la, _) = l1_of(&a, 140, 4); + let (lb, _) = l1_of(&a, 141, 4); + out.push(simple("base_readded", "layer 1a applies and 1b conflicts, freezing the units; the base decoded again keeps them as they are and reports nothing; 1b offered again is still refused", a, + vec![("L1a.wai".into(), la), ("L1b.wai".into(), lb)], vec![base_at(0, 0), offer("L1a.wai", 1), offer("L1b.wai", 2), base_at(0, 3), offer("L1b.wai", 4)])); + } + { + let a = small(142); + let bh = a.hash(); + let (l0, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 0, count: 1 }), &[rgb_unit(&a, 0)], (8, 8, 3), &[u8s(picture(8, 8, 143, 0))], (8, 8, 3), params(4, pid, 4095)); + let (l1, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: 1, count: 1 }), &[rgb_unit(&a, 1)], (8, 8, 3), &[u8s(picture(8, 8, 143, 1))], (8, 8, 3), params(4, pid, 4095)); + let cut = edit(&l0, |w| { + let n = w.payload.len(); + w.payload.truncate(n - 4); + }); + out.push(Case { name: "refused_on_release", about: "two layers held for their base; when it arrives, the first is payload-malformed and has that one report, and the second applies", bases: vec![a], + files: vec![("L1_cut.wai".into(), cut), ("L1_f1.wai".into(), l1)], cfg: cfg(), steps: vec![offer("L1_cut.wai", 0), offer("L1_f1.wai", 1), base_at(0, 2)] }); + } + { + let a = small(153); + let bh = a.hash(); + let mut att = a.clone(); + att.attested = true; + let mut files = Vec::new(); + for f in 0..2u32 { + let (l, _) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 154, f))], (8, 8, 3), params(4, pid, 4095)); + files.push((format!("L1_f{f}.wai"), l)); + } + let dv = |at| Step::Offer { file: "L1_f1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_after_decode", about: "a base decoded before its receipt is verified: frame 0's layer with no receipt applies; the base decoded again with its receipt verified is attested from then on, so frame 1's layer with no receipt is refinement-unattested; decoding it again without the receipt leaves it attested; delivered-verified, the layer applies", + bases: vec![a, att], files, cfg: cfg(), + steps: vec![base_at(0, 0), offer("L1_f0.wai", 1), base_at(1, 2), offer("L1_f1.wai", 3), base_at(0, 4), offer("L1_f1.wai", 5), dv(6)] }); + } + { + let mut a = small(166); + a.attested = true; + let mut plain = a.clone(); + plain.attested = false; + let (l1, _) = l1_of(&a, 167, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_across_drop", about: "an attested base's layer 1 applies with its receipt; the chain is dropped and the base decoded again by a decode that carries no receipt: it is still attested, so layer 1 with no receipt is refinement-unattested, and with its receipt it replays", + bases: vec![a, plain], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![base_at(0, 0), dv(1), Step::Remove { index: 0, at_ms: 2 }, base_at(1, 3), offer("L1.wai", 4), dv(5)] }); + } + { + let a = small(168); + let (l1, _) = l1_of(&a, 169, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "attested_before_decode", about: "the sink verifies the base's receipt before it decodes the base: the decode, which carries no receipt, gives an attested base, so layer 1 with no receipt is refinement-unattested and applies with its receipt", + bases: vec![a], files: vec![("L1.wai".into(), l1)], cfg: cfg(), + steps: vec![Step::Attest { index: 0, at_ms: 0 }, base_at(0, 1), offer("L1.wai", 2), dv(3)] }); + } + { + let mut a = small(144); + a.attested = true; + let (l1, _) = l1_of(&a, 145, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "evidence_upgrade", about: "an attested base: a layer held for it with no receipt evidence is offered again delivered-verified, keeps that evidence, and applies when the base arrives", bases: vec![a], + files: vec![("L1.wai".into(), l1)], cfg: cfg(), steps: vec![offer("L1.wai", 0), dv(5), base_at(0, 6)] }); + } + { + let mut a = small(146); + a.attested = true; + let (l1, _) = l1_of(&a, 147, 4); + let dv = |at| Step::Offer { file: "L1.wai".into(), at_ms: at, evidence: ReceiptEvidence::DeliveredVerified }; + out.push(Case { name: "evidence_upgrade_deadline", about: "units presented at 100 and 140 with a 96 ms lead: the layer arrives at 0 with no evidence and is verified at 5, so over the attested base it counts as arriving at 5: late for unit 0, in time for unit 1", bases: vec![a], + files: vec![("L1.wai".into(), l1)], + cfg: ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 40, target_latency_ms: 100 }, lead_ms: vec![96] }), ..cfg() }, + steps: vec![offer("L1.wai", 0), dv(5), base_at(0, 6)] }); + } + out +} + +/// A copy of the `WAI1` envelope `env` with the manifest text `text`, written as it is. +fn raw_manifest(env: &[u8], text: &str) -> Vec { + let ml = u32::from_le_bytes(env[4..8].try_into().expect("a length")) as usize; + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(text.len() as u32).to_le_bytes()); + out.extend_from_slice(text.as_bytes()); + out.extend_from_slice(&env[8 + ml..]); + out +} + +/// What a layer adds to its chain's state while retained (staged-delivery §4.5). +fn reserve(env: &[u8]) -> u64 { + let w = Wai::from_bytes(env).expect("WAI1"); + let p = wir1::parse(&w.payload, wai::staged::unit_kind(w.capability()).expect("a refinement")).expect("WIR1"); + let c = p.declared_cost(); + w.payload.len() as u64 + c.output_bytes + c.working_bytes + 32 * p.n_units as u64 +} + +// ───────────────────────── replay ───────────────────────── + +fn report_json(r: &OfferReport) -> Value { + json!({ + "object": hex(&r.content_hash), + "envelope": r.envelope.map(|x| x.code()), + "detail": r.detail, + "held_for_base": r.held_for_base, + "applied": r.applied, + "held": r.held, + "refused": r.refused.iter().map(|(u, w)| json!([u, w.code()])).collect::>(), + }) +} + +fn evidence_token(e: ReceiptEvidence) -> &'static str { + e.token() +} + +/// The inputs of a replay: the bases' infos and units, the files, the config, the steps. +struct Replay { + bases: Vec<(BaseInfo, Vec)>, + files: BTreeMap>, + cfg: ChainConfig, + steps: Vec, +} + +/// Run a replay: the reports of every step, and every base's final units. +fn run(r: &Replay) -> (Vec, Vec) { + let mut sink = StageSink::new(r.cfg.clone()); + let mut reports = Vec::new(); + for s in &r.steps { + let got = match s { + Step::Offer { file, at_ms, evidence } => match sink.offer(&r.files[file], *at_ms, *evidence) { + Ok(got) => got, + Err(e) => { + // Not a WAI1 this sink reads: the step's outcome is the error. + reports.push(json!({"error": match e { OfferError::NotWai1(_) => "not-wai1", OfferError::Encrypted => "encrypted" }})); + continue; + } + }, + Step::Base { index, .. } => { + let (info, units) = r.bases[*index].clone(); + sink.add_base(info, units) + } + Step::Expire { at_ms } => sink.expire(*at_ms), + Step::Remove { index, .. } => sink.remove_base(&r.bases[*index].0.content_hash).unwrap_or_default(), + Step::Attest { index, .. } => { + sink.mark_attested(&r.bases[*index].0.content_hash); + vec![] + } + }; + reports.push(Value::Array(got.iter().map(report_json).collect())); + } + let finals = r + .bases + .iter() + .map(|(info, units)| match sink.chain(&info.content_hash) { + None => json!(null), + Some(c) => json!({ + "units": c.units().iter().enumerate().map(|(u, s)| json!({ + "stage": s.stage, + "producer": hex(&s.producer), + "frozen": s.frozen, + "tier": tier::token(s.tier), + "shape": [s.buf.shape.0, s.buf.shape.1, s.buf.shape.2], + "stage_sha256": c.unit_sha256(u as u32).map(|h| hex(&h)), + })).collect::>(), + "clip_sha256": c.clip_sha256().map(|h| hex(&h)), + "n_units": units.len(), + }), + }) + .collect(); + (reports, finals) +} + +fn step_json(s: &Step) -> Value { + match s { + Step::Offer { file, at_ms, evidence } => json!({"op": "offer", "file": file, "at_ms": at_ms, "evidence": evidence_token(*evidence)}), + Step::Base { index, at_ms } => json!({"op": "base", "index": index, "at_ms": at_ms}), + Step::Expire { at_ms } => json!({"op": "expire", "at_ms": at_ms}), + Step::Remove { index, at_ms } => json!({"op": "remove", "index": index, "at_ms": at_ms}), + Step::Attest { index, at_ms } => json!({"op": "attest", "index": index, "at_ms": at_ms}), + } +} + +fn cfg_json(c: &ChainConfig) -> Value { + json!({ + "bound": {"max_envelopes": c.bound.max_envelopes, "max_age_ms": c.bound.max_age_ms}, + "cost_limit": c.cost_limit.map(|l| json!({"output_bytes": l.output_bytes, "symbols": l.symbols, "working_bytes": l.working_bytes})), + "budget": c.budget, + "record_budget": c.record_budget, + "live": c.live.as_ref().map(|l| json!({"t0_ms": l.clock.t0_ms, "unit_ms": l.clock.unit_ms, "target_latency_ms": l.clock.target_latency_ms, "lead_ms": l.lead_ms})), + }) +} + +fn cfg_from(v: &Value) -> ChainConfig { + let u = |x: &Value| x.as_u64().expect("an integer"); + ChainConfig { + bound: HoldBound { max_envelopes: u(&v["bound"]["max_envelopes"]) as usize, max_age_ms: u(&v["bound"]["max_age_ms"]) }, + cost_limit: (!v["cost_limit"].is_null()).then(|| Wir1Cost { output_bytes: u(&v["cost_limit"]["output_bytes"]), symbols: u(&v["cost_limit"]["symbols"]), working_bytes: u(&v["cost_limit"]["working_bytes"]) }), + budget: u(&v["budget"]), + record_budget: u(&v["record_budget"]), + live: (!v["live"].is_null()).then(|| Live { + clock: UnitClock { t0_ms: u(&v["live"]["t0_ms"]), unit_ms: u(&v["live"]["unit_ms"]), target_latency_ms: u(&v["live"]["target_latency_ms"]) }, + lead_ms: v["live"]["lead_ms"].as_array().expect("leads").iter().map(u).collect(), + }), + } +} + +fn step_from(v: &Value) -> Step { + let at = v["at_ms"].as_u64().expect("at_ms"); + match v["op"].as_str().expect("op") { + "offer" => Step::Offer { file: v["file"].as_str().expect("file").into(), at_ms: at, evidence: ReceiptEvidence::from_token(v["evidence"].as_str().expect("evidence")).expect("an evidence token") }, + "base" => Step::Base { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, + "expire" => Step::Expire { at_ms: at }, + "remove" => Step::Remove { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, + "attest" => Step::Attest { index: v["index"].as_u64().expect("index") as usize, at_ms: at }, + o => panic!("unknown op {o}"), + } +} + +fn json_bytes(v: &Value) -> Vec { + let mut s = serde_json::to_string_pretty(v).expect("serialise"); + s.push('\n'); + s.into_bytes() +} + +/// Every file of the corpus, by path below the corpus directory. +fn generate(ctx: &Ctx) -> BTreeMap> { + let mut out = BTreeMap::new(); + let mut index = Vec::new(); + for c in cases(ctx) { + let dir = format!("cases/{}", c.name); + let mut bases_json = Vec::new(); + for (i, b) in c.bases.iter().enumerate() { + let suffix = if c.bases.len() == 1 { String::new() } else { format!("_{i}") }; + let file = format!("base{suffix}.wai"); + let s0 = format!("stage0{suffix}.bin"); + out.insert(format!("{dir}/{file}"), b.envelope.clone()); + let stage0 = b.stage0(); + let decode = match &b.decode { + Decode::Int { set } => json!({"kind": "int", "set": set.iter().map(|(m, p)| (m.to_string(), Value::from(p.clone()))).collect::>()}), + Decode::Png => json!({"kind": "png"}), + Decode::Flac => json!({"kind": "flac"}), + }; + bases_json.push(json!({ + "file": file, + "object": hex(&b.hash()), + "decoded_via": b.decoded_via, + "via_fallback": b.via_fallback, + "attested": b.attested, + "first_unit": b.first_unit, + "pin_resolved": b.pin_resolved, + "tier": tier::token(b.tier), + "units": b.units.iter().map(|u| json!({"shape": [u.shape.0, u.shape.1, u.shape.2], "elem": match u.samples { Samples::U8(_) => "u8", Samples::I16(_) => "i16" }})).collect::>(), + "stage0": s0, + "stage0_sha256": sha(&stage0), + "decode": decode, + })); + out.insert(format!("{dir}/{s0}"), stage0); + } + for (name, bytes) in &c.files { + out.insert(format!("{dir}/{name}"), bytes.clone()); + } + let replay = Replay { + bases: c.bases.iter().map(|b| (b.info(), b.units.clone())).collect(), + files: c.files.iter().cloned().collect(), + cfg: c.cfg.clone(), + steps: c.steps.clone(), + }; + let (reports, finals) = run(&replay); + let case = json!({ + "format": "wai-staged-case/1", + "name": c.name, + "about": c.about, + "bases": bases_json, + "files": c.files.iter().map(|(n, b)| json!({"file": n, "object": hex(&chash(b)), "bytes": b.len()})).collect::>(), + "config": cfg_json(&c.cfg), + "steps": c.steps.iter().map(step_json).collect::>(), + "expect": {"reports": reports, "bases": finals}, + }); + index.push(json!({"name": c.name, "about": c.about, "clip_sha256": finals.iter().map(|f| f.get("clip_sha256").cloned().unwrap_or(Value::Null)).collect::>()})); + out.insert(format!("{dir}/case.json"), json_bytes(&case)); + } + let mut w2 = Vec::new(); + let files = wai2_files(ctx); + let packs: Vec = pack_attempts(&files[0].2) + .into_iter() + .map(|(name, sources, packs)| { + let names: Vec = sources.iter().enumerate().map(|(i, _)| format!("{name}.src{i}.wai")).collect(); + for (n, b) in names.iter().zip(&sources) { + out.insert(format!("wai2/{n}"), b.clone()); + } + json!({"name": name, "sources": names, "packs": packs}) + }) + .collect(); + for (name, bytes, sources) in files { + let mut src_names = Vec::new(); + for (i, s) in sources.iter().enumerate() { + let n = format!("wai2/{name}.src{i}.wai"); + out.insert(n.clone(), s.clone()); + src_names.push(n.trim_start_matches("wai2/").to_string()); + } + w2.push(json!({"file": format!("{name}.wai"), "sources": src_names, "expect": wai2_expect(&bytes)})); + out.insert(format!("wai2/{name}.wai"), bytes); + } + out.insert( + "wai2/index.json".into(), + json_bytes(&json!({ + "about": "WAI2 envelopes with refinement components (SPEC §7.1): what check_component_roles reports, whether each envelope_manifest matches its entry, the content hash of each component's exploded envelope (null for an entry that cannot be exploded; the others are exploded all the same), and entry 0's refinements. Where `sources` is given, the components explode to those WAI1 envelopes byte for byte. `packs` lists WAI1 envelopes and whether a packer packs them: it refuses bytes around a manifest's JSON value or after a payload, and a manifest that inside a WAI2 entry passes the JSON depth limit.", + "files": w2, + "packs": packs, + })), + ); + out.insert("schedule.json".into(), json_bytes(&schedule_json())); + out.insert( + "index.json".into(), + json_bytes(&json!({ + "format": "wai-staged-vectors/1", + "about": "The staged-delivery corpus (extensions/staged-delivery.md §12). Each case's case.json states its bases, layers, sink configuration and steps, and what a conforming sink reports and holds. Written by wai_staged_vectors gen.", + "cases": index, + })), + ); + out +} + +/// Arrival traces and what a sink presents for each (§8). +fn schedule_json() -> Value { + let c = UnitClock { t0_ms: 0, unit_ms: 40, target_latency_ms: 120 }; + let traces: Vec<(Vec, Vec>>, Vec)> = vec![ + (vec![0, 0, 0, 0], vec![vec![Some(10), Some(10), Some(10), Some(10)], vec![Some(20), Some(20), Some(20), Some(20)]], vec![20, 20]), + (vec![0, 0, 0, 0], vec![vec![Some(110), Some(110), Some(110), Some(110)], vec![Some(130), Some(130), Some(170), Some(240)]], vec![20, 20]), + (vec![0, 50, 200, 210], vec![vec![Some(0), None, Some(0), Some(0)], vec![Some(0), Some(0), None, Some(300)]], vec![10, 10]), + (vec![130, 130, 130], vec![vec![Some(100), Some(140), Some(190)]], vec![0]), + ]; + let out: Vec = traces + .iter() + .map(|(b, l, lead)| { + let p = present(&c, b, l, lead); + json!({ + "base_ms": b, + "layers_ms": l, + "lead_ms": lead, + "presented": p.iter().map(|x| json!({"at_ms": x.at_ms, "stage": x.stage, "expected": x.expected})).collect::>(), + "deadline_misses": deadline_misses(&p), + }) + }) + .collect(); + json!({"clock": {"t0_ms": c.t0_ms, "unit_ms": c.unit_ms, "target_latency_ms": c.target_latency_ms}, "traces": out}) +} + +// ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── + +/// The `wai2/` files: name → (bytes, the WAI1 sources it packs, if any). +fn wai2_files(ctx: &Ctx) -> Vec<(&'static str, Vec, Vec>)> { + let pid = Predictor::Identity; + let a = gop_base(ctx, 8, 8, 2, 80, 0, 8); + let bh = a.hash(); + // A live-style chain: a layer 1 per frame, then a layer 2 over their run. + let mut l1s = Vec::new(); + let mut mid = Vec::new(); + for f in 0..2u32 { + let (l, r) = layer(CAP_VIDEO_INT_REFINE, &bh, one(&bh), 1, Some(Span { from: f, count: 1 }), &[rgb_unit(&a, f as usize)], (8, 8, 3), &[u8s(picture(8, 8, 81, f))], (8, 8, 3), params(4, pid, 4095)); + l1s.push(l); + mid.push(r.into_iter().next().unwrap()); + } + let hs: Vec<[u8; 32]> = l1s.iter().map(|l| chash(l)).collect(); + let targets: Vec = (0..2).map(|f| u8s(picture(8, 8, 81, f + 3))).collect(); + let (l2, _) = layer(CAP_VIDEO_INT_REFINE, &bh, runs(&hs), 2, None, &mid, (8, 8, 3), &targets, (8, 8, 3), params(2, pid, 4095)); + let sources = vec![a.envelope.clone(), l1s[0].clone(), l1s[1].clone(), l2.clone()]; + let refs: Vec<&[u8]> = sources.iter().map(|v| v.as_slice()).collect(); + let packed = WaiMulti::pack(&refs).expect("pack"); + // The synthesised form: ids, no envelope_manifest. + let payload_of = |e: &[u8]| Wai::from_bytes(e).expect("WAI1").payload; + let refinement = |id: &str, refines: Value, payload: Vec| { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id(id).with_role("refinement"); + m.refines = Some(refines); + Rendition::from_meta(m, payload).with_payload_digest() + }; + let synth = |l2_prev: &str, extra: bool, tamper: bool| -> Vec { + let mut rs = vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload_of(&a.envelope)).with_payload_digest(), + refinement("l1", json!({"base": "base", "prev": "base", "layer": 1}), payload_of(&l1s[0])), + ]; + let mut l2p = payload_of(&l2); + let mut two = refinement("l2", json!({"base": "base", "prev": l2_prev, "layer": 2, "span": {"from": 0, "count": 1}}), l2p.clone()); + if tamper { + l2p[60] ^= 1; + two.payload = l2p; + } + rs.push(two); + if extra { + rs.push(Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("late"), payload_of(&a.envelope)).with_payload_digest()); + } + WaiMulti::new("video", "replicate", json!({"frames": 2, "dur": 1.0}), rs).to_bytes().expect("WAI2") + }; + // The packed chain with entry 1 turned into a selectable entry: its envelope_manifest + // no longer matches it. + let mut w = WaiMulti::from_bytes(&packed).expect("WAI2"); + w.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into(); + w.manifest.renditions[1].role = None; + w.manifest.renditions[1].refines = None; + let mismatch = w.to_bytes().expect("WAI2"); + // The packed chain with a fallback in a refinement's envelope_manifest. + let mut w = WaiMulti::from_bytes(&packed).expect("WAI2"); + w.manifest.renditions[1].envelope_manifest.as_mut().expect("packed")["model_requirement"]["fallback"] = json!(CAP_VIDEO_INT_MOTION); + let fallback = w.to_bytes().expect("WAI2"); + // One break of SPEC §7.1's refinement rules each, on the synthesised form. + let base_entry = || Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload_of(&a.envelope)).with_payload_digest(); + let l1_refines = json!({"base": "base", "prev": "base", "layer": 1}); + let wai2 = |rs: Vec| WaiMulti::new("video", "replicate", json!({"frames": 2, "dur": 1.0}), rs).to_bytes().expect("WAI2"); + let not_refinement_only = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("l1").with_role("refinement"); + m.refines = Some(l1_refines.clone()); + wai2(vec![base_entry(), Rendition::from_meta(m, payload_of(&l1s[0])).with_payload_digest()]) + }; + let identity_missing = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1").with_role("refinement"); + m.refines = Some(l1_refines.clone()); + wai2(vec![base_entry(), Rendition::from_meta(m, payload_of(&l1s[0]))]) + }; + let base_missing = wai2(vec![base_entry(), refinement("l1", json!({"base": "nobody", "prev": "nobody", "layer": 1}), payload_of(&l1s[0]))]); + let base_not_admitted = wai2(vec![ + Rendition::from_meta(RenditionMeta::new(CAP_IMAGE_PNG, "png").with_id("base"), payload_of(&a.envelope)).with_payload_digest(), + refinement("l1", l1_refines.clone(), payload_of(&l1s[0])), + ]); + let refines_on_base = { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"); + m.refines = Some(l1_refines.clone()); + wai2(vec![Rendition::from_meta(m, payload_of(&a.envelope)).with_payload_digest(), refinement("l1", l1_refines.clone(), payload_of(&l1s[0]))]) + }; + let as_component = wai2(vec![ + base_entry(), + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1"), payload_of(&l1s[0])).with_payload_digest(), + ]); + let malformed = wai2(vec![base_entry(), refinement("l1", json!({"base": "base", "prev": "base", "layer": 0}), payload_of(&l1s[0]))]); + vec![ + ("pack_live_chain", packed, sources), + ("ok_explode", synth("l1", false, false), vec![]), + ("digest_mismatch", synth("l1", false, true), vec![]), + ("component_manifest_mismatch", mismatch, vec![]), + ("prev_missing", synth("zz", false, false), vec![]), + ("refinement_before_selectable", synth("l1", true, false), vec![]), + ("refinement_manifest_fallback", fallback, vec![]), + ("refinement_not_refinement_only", not_refinement_only, vec![]), + ("refinement_identity_missing", identity_missing, vec![]), + ("refinement_base_missing", base_missing, vec![]), + ("refinement_base_not_admitted", base_not_admitted, vec![]), + ("refines_on_non_refinement", refines_on_base, vec![]), + ("refinement_only_as_component", as_component, vec![]), + ("refinement_refines_malformed", malformed, vec![]), + ] +} + +/// WAI1 envelopes and whether `WaiMulti::pack` packs them (SPEC §7.1): it refuses an +/// envelope with bytes around its manifest's JSON value or after its payload, and a +/// manifest nested so deep that inside a `WAI2` entry it passes the JSON depth limit. +fn pack_attempts(chain: &[Vec]) -> Vec<(&'static str, Vec>, bool)> { + let base = &chain[0]; + let text = String::from_utf8(wai::container::manifest_bytes(base).expect("WAI1").to_vec()).expect("UTF-8"); + let deep = |d: usize| raw_manifest(base, &format!("{},\"zz_deep\":{}{}}}", &text[..text.len() - 1], "[".repeat(d), "]".repeat(d))); + let attempts = vec![ + ("pack_trailing_newline", vec![raw_manifest(base, &format!("{text}\n"))]), + ("pack_leading_space", vec![raw_manifest(base, &format!(" {text}"))]), + ("pack_after_payload", vec![[&base[..], b"xyz"].concat()]), + ("pack_deep_120", vec![deep(120)]), + ("pack_deep_124", vec![deep(124)]), + ]; + attempts + .into_iter() + .map(|(name, srcs)| { + let refs: Vec<&[u8]> = srcs.iter().map(|v| v.as_slice()).collect(); + let packs = WaiMulti::pack(&refs).is_ok(); + (name, srcs, packs) + }) + .collect() +} + +/// What a sink and a verifier decide about a `WAI2`: its role check, its envelope +/// manifests, its exploded envelopes' content hashes, and entry 0's refinements. +fn wai2_expect(bytes: &[u8]) -> Value { + let w = WaiMulti::from_bytes(bytes).expect("WAI2"); + json!({ + "roles": w.check_component_roles().err().map(|e| e.code()), + "manifests": WaiMulti::check_envelope_manifests(bytes).err().map(|(i, c)| json!([i, c])), + "exploded": WaiMulti::explode_each(bytes).ok().map(|v| v.iter().map(|e| e.as_ref().ok().map(|e| hex(&chash(e)))).collect::>()), + "refinements_of_0": w.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), + }) +} + +fn wai_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn units_from_stage0(base: &Value, stage0: &[u8]) -> Vec { + let mut at = 0usize; + base["units"] + .as_array() + .expect("units") + .iter() + .map(|u| { + let s: Vec = u["shape"].as_array().expect("shape").iter().map(|x| x.as_u64().expect("dim") as u32).collect(); + let n = s[0] as usize * s[1] as usize * s[2] as usize; + let samples = match u["elem"].as_str().expect("elem") { + "u8" => { + let v = stage0[at..at + n].to_vec(); + at += n; + Samples::U8(v) + } + _ => { + let v = stage0[at..at + 2 * n].chunks_exact(2).map(|c| i16::from_le_bytes([c[0], c[1]])).collect(); + at += 2 * n; + Samples::I16(v) + } + }; + UnitBuf { shape: (s[0], s[1], s[2]), samples } + }) + .collect() +} + +fn verify(dir: &Path) -> Result<(), Vec> { + let ctx = Ctx { wai: wai_dir() }; + let mut fails = Vec::new(); + let mut fail = |m: String| { + eprintln!("FAIL {m}"); + fails.push(m); + }; + // 1. Byte-identical regeneration, and no committed file the generator does not write. + let regen = generate(&ctx); + for (rel, bytes) in ®en { + match std::fs::read(dir.join(rel)) { + Ok(b) if &b == bytes => {} + Ok(_) => fail(format!("{rel}: differs from its regeneration")), + Err(e) => fail(format!("{rel}: {e}")), + } + } + let mut committed = Vec::new(); + if let Ok(rd) = std::fs::read_dir(dir.join("wai2")) { + for f in rd.flatten() { + committed.push(format!("wai2/{}", f.file_name().to_string_lossy())); + } + } + for sub in ["cases"] { + let root = dir.join(sub); + if let Ok(rd) = std::fs::read_dir(&root) { + for case in rd.flatten() { + for f in std::fs::read_dir(case.path()).into_iter().flatten().flatten() { + committed.push(format!("{sub}/{}/{}", case.file_name().to_string_lossy(), f.file_name().to_string_lossy())); + } + } + } + } + for rel in committed { + if !regen.contains_key(&rel) { + fail(format!("{rel}: committed but not generated")); + } + } + // 2. Replay every case from its committed files. + let mut n_cases = 0; + for rel in regen.keys().filter(|k| k.ends_with("/case.json")) { + n_cases += 1; + let cdir = dir.join(rel).parent().unwrap().to_path_buf(); + let case: Value = match std::fs::read(dir.join(rel)).ok().and_then(|b| serde_json::from_slice(&b).ok()) { + Some(v) => v, + None => { + fail(format!("{rel}: unreadable")); + continue; + } + }; + let mut bases = Vec::new(); + for b in case["bases"].as_array().expect("bases") { + let env = std::fs::read(cdir.join(b["file"].as_str().unwrap())).unwrap_or_default(); + let stage0 = std::fs::read(cdir.join(b["stage0"].as_str().unwrap())).unwrap_or_default(); + if sha(&stage0) != b["stage0_sha256"] { + fail(format!("{rel}: {} does not have its stated SHA-256", b["stage0"])); + } + if hex(&chash(&env)) != b["object"] { + fail(format!("{rel}: {} does not have its stated content hash", b["file"])); + } + let units = units_from_stage0(b, &stage0); + let via = b["decoded_via"].as_str().unwrap().to_string(); + // The base's own decode, where this build has the decoder. + match b["decode"]["kind"].as_str() { + Some("int") => { + let set: Vec<(String, String)> = b["decode"]["set"] + .as_object() + .unwrap() + .iter() + .map(|(m, p)| (m.clone(), p.as_str().unwrap().to_string())) + .collect(); + let w = Wai::from_bytes(&env).expect("a base envelope"); + let pin = w.prior().cloned(); + let decoded = decode_with(&ctx, &via, &w.payload, &set, pin.as_ref()); + if decoded != units { + fail(format!("{rel}: the base's decode is not stage0")); + } + let t = tier::base_tier(&via, if b["pin_resolved"] == true { pin.as_ref() } else { None }); + if tier::token(t) != b["tier"] { + fail(format!("{rel}: the base tier is {}, the case states {}", tier::token(t), b["tier"])); + } + } + Some(k) => println!(" {rel}: the {k} base is a stage-0 fixture in this build (staged_verify.py decodes it)"), + None => fail(format!("{rel}: no decode kind")), + } + let info = BaseInfo { + content_hash: chash(&env), + capability: via, + via_fallback: b["via_fallback"] == true, + tier: tier::from_token(b["tier"].as_str().unwrap()), + attested: b["attested"] == true, + first_unit: b["first_unit"].as_u64().expect("first_unit"), + }; + bases.push((info, units)); + } + let files: BTreeMap> = case["files"] + .as_array() + .unwrap() + .iter() + .map(|f| { + let n = f["file"].as_str().unwrap().to_string(); + (n.clone(), std::fs::read(cdir.join(&n)).unwrap_or_default()) + }) + .collect(); + let replay = Replay { bases, files, cfg: cfg_from(&case["config"]), steps: case["steps"].as_array().unwrap().iter().map(step_from).collect() }; + let (reports, finals) = run(&replay); + if Value::Array(reports) != case["expect"]["reports"] { + fail(format!("{rel}: the reports differ from the case's")); + } + if Value::Array(finals) != case["expect"]["bases"] { + fail(format!("{rel}: the final units differ from the case's")); + } + } + // 3. The WAI2 files. + let w2: Value = std::fs::read(dir.join("wai2/index.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); + for f in w2["files"].as_array().into_iter().flatten() { + let name = f["file"].as_str().unwrap_or_default(); + let bytes = std::fs::read(dir.join("wai2").join(name)).unwrap_or_default(); + if wai2_expect(&bytes) != f["expect"] { + fail(format!("wai2/{name}: the checks differ from the index's")); + } + let srcs: Vec> = f["sources"].as_array().unwrap().iter().map(|s| std::fs::read(dir.join("wai2").join(s.as_str().unwrap())).unwrap_or_default()).collect(); + if !srcs.is_empty() && WaiMulti::explode_all(&bytes).ok() != Some(srcs) { + fail(format!("wai2/{name}: does not explode to its sources")); + } + } + for p in w2["packs"].as_array().into_iter().flatten() { + let srcs: Vec> = p["sources"].as_array().unwrap().iter().map(|s| std::fs::read(dir.join("wai2").join(s.as_str().unwrap())).unwrap_or_default()).collect(); + let refs: Vec<&[u8]> = srcs.iter().map(|v| v.as_slice()).collect(); + let got = WaiMulti::pack(&refs); + if got.is_ok() != (p["packs"] == true) { + fail(format!("wai2 {}: pack {}", p["name"], if got.is_ok() { "packs it" } else { "refuses it" })); + } + if let Ok(packed) = got { + if WaiMulti::explode_all(&packed).ok() != Some(srcs) { + fail(format!("wai2 {}: the packed envelope does not explode to its sources", p["name"])); + } + } + } + // 4. The schedule. + let sched: Value = std::fs::read(dir.join("schedule.json")).ok().and_then(|b| serde_json::from_slice(&b).ok()).unwrap_or(Value::Null); + if sched != schedule_json() { + fail("schedule.json: the traces do not replay to the stated presentation".into()); + } + println!("{n_cases} cases replayed"); + if fails.is_empty() { Ok(()) } else { Err(fails) } +} + +fn main() -> ExitCode { + let args: Vec = std::env::args().collect(); + let (Some(cmd), Some(dir)) = (args.get(1), args.get(2)) else { + eprintln!("usage: wai_staged_vectors gen|verify "); + return ExitCode::from(2); + }; + let dir = PathBuf::from(dir); + match cmd.as_str() { + "gen" => { + let ctx = Ctx { wai: wai_dir() }; + for (rel, bytes) in generate(&ctx) { + let p = dir.join(&rel); + std::fs::create_dir_all(p.parent().unwrap()).expect("mkdir"); + std::fs::write(&p, bytes).expect("write"); + } + println!("wrote {}", dir.display()); + ExitCode::SUCCESS + } + "verify" => match verify(&dir) { + Ok(()) => { + println!("staged-conformance: OK"); + ExitCode::SUCCESS + } + Err(f) => { + eprintln!("staged-conformance: {} failure(s)", f.len()); + ExitCode::from(1) + } + }, + _ => { + eprintln!("usage: wai_staged_vectors gen|verify "); + ExitCode::from(2) + } + } +} diff --git a/wai/wai-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index c2d42196a..45cf1c157 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -102,6 +102,10 @@ const GLOSSARY_NO_ENTRY: &[(&str, &str)] = &[ (CAP_WORLD_MODEL, WAI_NATIVE), (CAP_WORLD_REPLAY, WAI_NATIVE), (CAP_WORLD_SCENE, WAI_NATIVE), + (CAP_IMAGE_INT_REFINE, WAI_NATIVE), + (CAP_VIDEO_INT_REFINE, WAI_NATIVE), + (CAP_AUDIO_INT_REFINE, WAI_NATIVE), + (CAP_SPLAT_INT_REFINE, WAI_NATIVE), (CAP_NEURAL_BMSHJ2018, "the 2018 scale-hyperprior entry describes that paper's hyperprior model, not its factorized-prior baseline, which this capability carries"), (CAP_NEURAL_MBT2018_MEAN, "the entry for the 2018 joint-prior paper describes its autoregressive context model, not the mean-scale hyperprior this capability carries"), (CAP_NEURAL_VIDEO_BMSHJ2018, "per-frame use of the factorized-prior image model; no glossary codec describes it"), @@ -314,8 +318,10 @@ fn glossary_links_agree_with_the_registry() { fn glossary_check_names_each_broken_rule() { use serde_json::json; let bodies = vec!["ITU-T".to_string()]; + // The fixture links the payload and companion capabilities a codec could + // describe; a refinement is never linked (its payload format is WAI's own). let caps: Vec<&str> = known_capabilities().iter().copied() - .filter(|c| !matches!(capability_media(c), Some(Media::Interactive | Media::Binary))) + .filter(|c| !matches!(capability_media(c), Some(Media::Interactive | Media::Binary)) && !is_refinement_only(c)) .collect(); let links: Vec<(&str, &str)> = caps.iter().map(|c| (*c, *c)).collect(); let no_entry: Vec<(&str, &str)> = known_capabilities().iter().copied() diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index 506fb5211..1edc0f3da 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -186,6 +186,74 @@ pub fn pinned_int_payload(cap: &str) -> Option<[u8; 4]> { PINNED_INT_PAYLOADS.iter().find(|(c, _)| *c == cap).map(|&(_, m)| m) } +// Refinement capabilities (SPEC §5, *Refinement capabilities*; +// extensions/staged-delivery.md): each is **refinement-only**, a payload applied to the +// output of the stage below it, bound to that output's producer by content hash, and +// never decoded on its own. The residual form, payload WIR1 (staged-delivery §6.1). +pub const CAP_IMAGE_INT_REFINE: &str = "wai.image.int_refine"; +pub const CAP_VIDEO_INT_REFINE: &str = "wai.video.int_refine"; +pub const CAP_AUDIO_INT_REFINE: &str = "wai.audio.int_refine"; +pub const CAP_SPLAT_INT_REFINE: &str = "wai.splat.int_refine"; + +/// How a capability applied to a base's output computes what it gives (SPEC §5, *Base +/// admission*). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AppliedForm { + /// A refinement-only capability of the residual form (staged-delivery §6.1): the + /// next stage is the stage below, predicted and corrected by a decoded residual. + Residual, + /// An enhancement layer (SPEC §5, *Enhancement layers*): its standard's own + /// decode over one base encode's output. Carried as a companion (SPEC §7.1). + Enhancement, +} + +/// SPEC §5, *Base admission*: the one table of every capability applied to a base's +/// output, with its form and the base capabilities it applies over. A refinement-only +/// capability is admitted over a base when `refines.base` names an envelope decoded +/// through one of these (staged-delivery §4.1 step 4); an enhancement layer binds to a +/// rendition of one of them (SPEC §7.1, rule 2). Admitting another base widens a +/// capability (SPEC §8). Every admission check of the reference implementation reads +/// this table ([`admits`], [`enhancement_bases`], [`companion_applies_to`]). +/// +/// cbindgen:ignore +pub static BASE_ADMISSION: &[(&str, AppliedForm, &[&str])] = &[ + (CAP_IMAGE_INT_REFINE, AppliedForm::Residual, + &[CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG]), + (CAP_VIDEO_INT_REFINE, AppliedForm::Residual, &[CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_HYPER]), + (CAP_AUDIO_INT_REFINE, AppliedForm::Residual, &[CAP_AUDIO_INT_CODEC, CAP_AUDIO_FLAC]), + (CAP_SPLAT_INT_REFINE, AppliedForm::Residual, &[CAP_SPLAT_INT_CODEC]), + (CAP_VIDEO_LCEVC, AppliedForm::Enhancement, LCEVC_BASES), +]; + +/// The form of a capability applied to a base's output ([`BASE_ADMISSION`]), or `None` +/// for any other string. +pub fn applied_form(cap: &str) -> Option { + BASE_ADMISSION.iter().find(|(c, ..)| *c == cap).map(|&(_, f, _)| f) +} + +/// The base capabilities `cap` applies over ([`BASE_ADMISSION`]); empty for a +/// capability not applied to a base. +pub fn admitted_bases(cap: &str) -> &'static [&'static str] { + BASE_ADMISSION.iter().find(|(c, ..)| *c == cap).map(|&(.., b)| b).unwrap_or(&[]) +} + +/// Whether the capability `applied` applies over a base decoded through `base` +/// (SPEC §5, *Base admission*). +pub fn admits(applied: &str, base: &str) -> bool { + admitted_bases(applied).contains(&base) +} + +/// Whether a registered capability is **refinement-only** (SPEC §5, §8): a payload +/// applied, in a declared order, to the output of the stage below it, bound to that +/// output's producer by content hash (`extensions/staged-delivery.md`), and never +/// decoded on its own. Such a capability is never a `WAI1` `capability` dispatched by +/// SPEC §4 or a `fallback`, never a selectable rendition and never a companion; it is +/// carried as a `WAI1` with `refines`, or as a `WAI2` component with +/// `role: "refinement"`. +pub fn is_refinement_only(cap: &str) -> bool { + applied_form(cap) == Some(AppliedForm::Residual) +} + // Haptics (extensions/haptics): per-channel intensity/sharpness envelope, // reconstructed to an actuator signal (sample-equivalence). Engine // behind the `haptic` feature. @@ -536,6 +604,8 @@ pub fn known_capabilities() -> &'static [&'static str] { CAP_VIDEO_LCEVC, // broadcast and streaming audio (sink-supplied; none is a default) CAP_AUDIO_MPEGH, CAP_AUDIO_AVS3, CAP_AUDIO_AAC, + // refinement-only: residual refinements of an integer or lossless base + CAP_IMAGE_INT_REFINE, CAP_VIDEO_INT_REFINE, CAP_AUDIO_INT_REFINE, CAP_SPLAT_INT_REFINE, ] } @@ -577,9 +647,10 @@ pub fn is_companion_only(cap: &str) -> bool { /// whose payload was encoded against one specific base encode, so it binds to /// exactly one selectable rendition of one of its [`enhancement_bases`], never /// to "every rendition of that codec" (SPEC §5, §7.1). Every enhancement layer -/// is also companion-only. +/// is also companion-only. Read from [`BASE_ADMISSION`]: the capabilities applied +/// to a base as an enhancement. pub fn is_enhancement_layer(cap: &str) -> bool { - cap == CAP_VIDEO_LCEVC + applied_form(cap) == Some(AppliedForm::Enhancement) } /// The base codecs WAI registers `wai.video.lcevc` over (SPEC §5): AVC, HEVC, @@ -600,10 +671,10 @@ const LCEVC_BASES: &[&str] = &[ /// The capabilities an enhancement layer may be bound to: the base codecs its /// registration names (SPEC §5), which for LCEVC are WAI's choice rather than a -/// restriction of its standard. Empty for a capability that is not an -/// enhancement layer. +/// restriction of its standard, read from [`BASE_ADMISSION`]. Empty for a +/// capability that is not an enhancement layer. pub fn enhancement_bases(cap: &str) -> &'static [&'static str] { - if cap == CAP_VIDEO_LCEVC { LCEVC_BASES } else { &[] } + if applied_form(cap) == Some(AppliedForm::Enhancement) { admitted_bases(cap) } else { &[] } } /// The registered capabilities outside the `wai.video.*` namespace whose payload @@ -621,7 +692,8 @@ const VIDEO_CLASS_OUTSIDE_NAMESPACE: &[&str] = &[ /// manifest's `media` field is informational; the capability is /// authoritative): /// -/// - every registered `wai.video.*` capability that is not companion-only; +/// - every registered `wai.video.*` capability that is neither companion-only nor +/// refinement-only; /// - the neural video codecs `wai.neural.video_bmshj2018`, `wai.neural.dcvc_rt` /// and `wai.neural.dcvc_fm`; /// - the films `wai.film.linear` and `wai.film.reel`. @@ -631,7 +703,7 @@ const VIDEO_CLASS_OUTSIDE_NAMESPACE: &[&str] = &[ /// in that revision. pub fn is_video_class(cap: &str) -> bool { is_known_capability(cap) - && ((cap.starts_with("wai.video.") && !is_companion_only(cap)) + && ((cap.starts_with("wai.video.") && !is_companion_only(cap) && !is_refinement_only(cap)) || VIDEO_CLASS_OUTSIDE_NAMESPACE.contains(&cap)) } @@ -654,9 +726,9 @@ pub fn is_video_class(cap: &str) -> bool { /// companion (rule 1), and for a capability this revision does not register the /// registry knows no bases — [`crate::container::WaiMulti::check_component_roles`] /// checks such a companion only for a present base (rule 3). No companion -/// applies to a companion-only `base`. +/// applies to a companion-only or a refinement-only `base`. pub fn companion_applies_to(companion: &str, base: &str) -> bool { - if is_companion_only(base) { + if is_companion_only(base) || is_refinement_only(base) { return false; } match companion { @@ -758,7 +830,10 @@ CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH // guarantee. LCEVC is lossy residual enhancement of a lossy base. | CAP_VIDEO_AV2 | CAP_VIDEO_VVC | CAP_VIDEO_HEVC | CAP_VIDEO_AVC | CAP_VIDEO_VP9 | CAP_VIDEO_EVC | CAP_VIDEO_EVC_BASELINE | CAP_VIDEO_AVS3 | CAP_VIDEO_LCEVC - | CAP_AUDIO_MPEGH | CAP_AUDIO_AVS3 | CAP_AUDIO_AAC => Lossy, + | CAP_AUDIO_MPEGH | CAP_AUDIO_AVS3 | CAP_AUDIO_AAC + // A residual refinement quantises its residual by its step q: lossy against + // its target, whatever its base; its decode is exact given the stage below. + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE => Lossy, // Generative / similarity-conformant. CAP_SEMANTIC_CLIP | CAP_SEMANTIC_PERCO2 => Perceptual, _ => return None, @@ -861,6 +936,7 @@ pub fn capability_license(cap: &str) -> Option { | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT | CAP_PRIOR_SET + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE // Open asset formats: glTF (Khronos), SPZ (open source). | CAP_MESH_GLB | CAP_SPLAT_SPZ // JPEG 2000 Part 1 is fee-free under ITU-T policy 2.1 (extensions / @@ -1202,7 +1278,11 @@ pub fn capability_replicate_class(cap: &str) -> Option { | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT // A parameter-set listing: SPEC §3.1 fixes its bytes exactly, and they are // its own output. - | CAP_PRIOR_SET => StandardDefined, + | CAP_PRIOR_SET + // A residual refinement: integer prediction, residual and clamp + // (staged-delivery §6.1), exact given the stage below; it composes its base's + // decoded output (`composes_decoded_sources`), so it is as exact as its base. + | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE => StandardDefined, // A criterion around a reference, not identical output (clauses in the // variant's doc and in research/media-codec-registry.md): Opus (RFC 6716 // §6), JPEG (T.81 §7, A.3.3), JPEG XL (18181-3 §4.2), JPEG 2000 (15444-4), @@ -1481,14 +1561,16 @@ pub fn replicate_refusal( /// Whether a capability's output is computed from **other envelopes' decoded /// output** — sources it references rather than carries — so it is only as exact -/// as they are. `wai.audio.scene` is the one such capability: its payload names -/// each object's source by the content hash of a `wai.audio.*` envelope, and its -/// mixdown is exact given the PCM those sources decode to -/// (`extensions/spatial-audio.md` §4), whatever their codec. The other WAI-native -/// capabilities hash only state they carry: a world's, a film's or an avatar's -/// assets are presentation and never enter its state hash. +/// as they are. `wai.audio.scene`'s payload names each object's source by the +/// content hash of a `wai.audio.*` envelope, and its mixdown is exact given the PCM +/// those sources decode to (`extensions/spatial-audio.md` §4), whatever their codec. +/// A refinement-only capability names, by content hash, the base whose output it +/// refines (`extensions/staged-delivery.md` §3), and its next stage is exact given +/// that output. The other WAI-native capabilities hash only state they carry: a +/// world's, a film's or an avatar's assets are presentation and never enter its +/// state hash. pub fn composes_decoded_sources(cap: &str) -> bool { - cap == CAP_AUDIO_SCENE + cap == CAP_AUDIO_SCENE || is_refinement_only(cap) } /// Whether the class promises byte identity for the capability's own output. @@ -1529,10 +1611,12 @@ pub fn is_cross_decoder_exact(cap: &str) -> bool { /// Whether `source` is a capability a composing capability's payload may /// reference: for `wai.audio.scene`, a `wai.audio.*` envelope — the asset table -/// names each object's source as one (`extensions/spatial-audio.md` §2.2). +/// names each object's source as one (`extensions/spatial-audio.md` §2.2); for a +/// refinement-only capability, a base it admits (SPEC §5, *Base admission*). fn is_source_of(cap: &str, source: &str) -> bool { match cap { CAP_AUDIO_SCENE => source.starts_with("wai.audio."), + c if is_refinement_only(c) => admits(c, source), _ => false, } } @@ -1810,6 +1894,58 @@ mod tests { assert!(!is_companion_only("wai.image.nope")); } + /// Test 16: every refinement-only capability is complete in the registry, and + /// *Base admission* is one table. Each refinement has the residual form, its + /// admitted bases (pinned here in both directions), the royalty-free licence, the + /// StandardDefined class, the Lossy tier, the refinement kind and role; none is a + /// candidate, a companion, dispatchable or in the video media class; and it is as + /// exact as the base it refines. LCEVC's bases are its row of the same table: + /// `enhancement_bases` and `companion_applies_to` read it, and no other string has + /// an applied form. + #[test] + fn refinement_registry_is_complete() { + use registry::{capability_kind, capability_role, Kind}; + let pinned: &[(&str, &[&str])] = &[ + (CAP_IMAGE_INT_REFINE, &[CAP_NEURAL_INT_HYPER, CAP_NEURAL_INT_MLICPP, CAP_NEURAL_INT_SYNTH, CAP_IMAGE_PNG]), + (CAP_VIDEO_INT_REFINE, &[CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_HYPER]), + (CAP_AUDIO_INT_REFINE, &[CAP_AUDIO_INT_CODEC, CAP_AUDIO_FLAC]), + (CAP_SPLAT_INT_REFINE, &[CAP_SPLAT_INT_CODEC]), + ]; + let refinements: Vec<&str> = known_capabilities().iter().copied().filter(|c| is_refinement_only(c)).collect(); + assert_eq!(refinements, pinned.iter().map(|(c, _)| *c).collect::>()); + for &(cap, bases) in pinned { + assert_eq!(applied_form(cap), Some(AppliedForm::Residual), "{cap}"); + assert_eq!(admitted_bases(cap), bases, "{cap}"); + assert_eq!(capability_license(cap), Some(LicenseClass::RoyaltyFree), "{cap}"); + assert_eq!(capability_replicate_class(cap), Some(ReplicateClass::StandardDefined), "{cap}"); + assert_eq!(capability_tier(cap), Some(FidelityTier::Lossy), "{cap}"); + assert_eq!(capability_kind(cap), Some(Kind::Refinement), "{cap}"); + assert_eq!(capability_role(cap), Some("refinement"), "{cap}"); + assert!(!is_candidate(cap) && !is_companion_only(cap) && !is_dispatchable(cap), "{cap}"); + assert!(!is_video_class(cap) && pinned_int_payload(cap).is_none(), "{cap}"); + assert!(!decodes_against_parameter_set(cap), "{cap}: a refinement reads no parameter set"); + assert!(!is_cross_decoder_exact(cap), "{cap}: exact only given its base"); + for &other in known_capabilities() { + assert_eq!(admits(cap, other), bases.contains(&other), "{cap} over {other}"); + assert_eq!(is_cross_decoder_exact_with_sources(cap, &[other]), + bases.contains(&other) && is_cross_decoder_exact(other), "{cap} over {other}"); + assert!(!companion_applies_to(other, cap), "no companion applies to {cap}"); + } + } + // LCEVC: one row of the same table, with the bases SPEC §5 lists. + assert_eq!(applied_form(CAP_VIDEO_LCEVC), Some(AppliedForm::Enhancement)); + assert_eq!(enhancement_bases(CAP_VIDEO_LCEVC), admitted_bases(CAP_VIDEO_LCEVC)); + assert_eq!(admitted_bases(CAP_VIDEO_LCEVC), LCEVC_BASES); + for &b in known_capabilities() { + assert_eq!(companion_applies_to(CAP_VIDEO_LCEVC, b), admits(CAP_VIDEO_LCEVC, b), "{b}"); + } + // No other string has an applied form, and the table names each once. + let applied: Vec<&str> = known_capabilities().iter().copied().filter(|c| applied_form(c).is_some()).collect(); + assert_eq!(applied.len(), pinned.len() + 1); + assert_eq!(BASE_ADMISSION.len(), applied.len()); + assert!(applied_form("wai.image.nope").is_none() && admitted_bases("wai.image.nope").is_empty()); + } + /// SPEC §5 and §7.1, rule 2: what each companion-only capability applies to. /// Pinned in both directions, so widening or narrowing a base set is a /// reviewed change to this test. @@ -1818,7 +1954,7 @@ mod tests { // The video media class, pinned: every standalone wai.video.* capability, // the neural video codecs SPEC §5 lists under *Video:*, and the films. let mut video: Vec<&str> = known_capabilities().iter().copied() - .filter(|c| c.starts_with("wai.video.") && !is_companion_only(c)).collect(); + .filter(|c| c.starts_with("wai.video.") && !is_companion_only(c) && !is_refinement_only(c)).collect(); assert!(video.contains(&CAP_VIDEO_J2K) && video.contains(&CAP_VIDEO_AV2)); video.extend([CAP_NEURAL_VIDEO_BMSHJ2018, CAP_NEURAL_DCVC_RT, CAP_NEURAL_DCVC_FM, CAP_FILM_LINEAR, CAP_FILM_REEL]); @@ -2171,6 +2307,10 @@ mod tests { (CAP_QUANTUM_CIRCUIT, "extensions/quantum-sim.md: statevector-equivalence"), (CAP_TERNARY_TT, "extensions/ternary-tt.md: reconstruction-equivalence"), (CAP_PRIOR_SET, "SPEC.md §3.1: a listing's bytes are fixed exactly and are their own output"), + (CAP_IMAGE_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_VIDEO_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_AUDIO_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), + (CAP_SPLAT_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), ]; /// Every capability classed NeuralIntegerExact, with the basis its class rests @@ -2267,10 +2407,12 @@ mod tests { /// does not make it portable by itself. #[test] fn a_scene_is_only_as_exact_as_its_sources() { - // The one capability that composes other envelopes' decoded output. + // The capabilities that compose other envelopes' decoded output: the scene, + // and the refinements, each exact given the base it refines. let composing: Vec<&str> = known_capabilities().iter().copied() .filter(|c| composes_decoded_sources(c)).collect(); - assert_eq!(composing, vec![CAP_AUDIO_SCENE]); + assert_eq!(composing, vec![CAP_AUDIO_SCENE, CAP_IMAGE_INT_REFINE, CAP_VIDEO_INT_REFINE, + CAP_AUDIO_INT_REFINE, CAP_SPLAT_INT_REFINE]); assert_eq!(capability_replicate_class(CAP_AUDIO_SCENE), Some(ReplicateClass::StandardDefined)); // Not portable from the capability alone... diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 29381fcff..1b2c5b8fd 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -52,6 +52,11 @@ pub enum Kind { /// A companion-only capability ([`is_companion_only`]): its payload is applied /// with a base rendition, never on its own. Companion, + /// A refinement-only capability ([`is_refinement_only`]): its payload is applied, + /// in a declared order, to the output of the stage below it, bound to that + /// output's producer by content hash, never on its own + /// (`extensions/staged-delivery.md`). + Refinement, /// A derivation operation of ISO/IEC 23001-16 (`wai.derive.*`): it acts on /// samples another capability decoded and names no payload format. Derivation, @@ -62,14 +67,15 @@ pub enum Kind { impl Kind { /// Every kind, in declaration order. - pub const fn all() -> [Self; 4] { - [Self::Payload, Self::Companion, Self::Derivation, Self::Record] + pub const fn all() -> [Self; 5] { + [Self::Payload, Self::Companion, Self::Refinement, Self::Derivation, Self::Record] } /// The label the export uses. pub fn label(self) -> &'static str { match self { Self::Payload => "payload", Self::Companion => "companion", + Self::Refinement => "refinement", Self::Derivation => "derivation", Self::Record => "record", } @@ -164,15 +170,19 @@ impl ReplicateClass { } /// The role labels `wai capabilities` prints and the export records for a -/// payload or companion capability. -pub const ROLE_LABELS: [&str; 3] = ["standalone", "companion", "enhancement"]; +/// payload, companion or refinement capability. +pub const ROLE_LABELS: [&str; 4] = ["standalone", "companion", "enhancement", "refinement"]; -/// How dispatch treats a payload or companion capability's payload: standalone -/// media, a companion applied with a base rendition, or an enhancement layer bound -/// to exactly one base encode (SPEC §5, §7.1). `None` for any other string. +/// How dispatch treats a payload, companion or refinement capability's payload: +/// standalone media, a companion applied with a base rendition, an enhancement layer +/// bound to exactly one base encode (SPEC §5, §7.1), or a refinement applied to the +/// output of the stage below it (SPEC §5, *Refinement capabilities*). `None` for any +/// other string. pub fn capability_role(cap: &str) -> Option<&'static str> { if !is_known_capability(cap) { None + } else if is_refinement_only(cap) { + Some("refinement") } else if is_enhancement_layer(cap) { Some("enhancement") } else if is_companion_only(cap) { @@ -299,6 +309,10 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_AUDIO_MPEGH, S_BROADCAST, None, "2026-09-28"), (CAP_AUDIO_AVS3, S_BROADCAST, Some("T/AI 109.3-2023"), "2026-09-28"), (CAP_AUDIO_AAC, S_BROADCAST, None, "2026-09-28"), + (CAP_IMAGE_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_VIDEO_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_AUDIO_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), + (CAP_SPLAT_INT_REFINE, S_REFINEMENT, None, "2026-10-01"), ]; const S_FLOOR: &str = "§5 Mandatory floor"; @@ -308,6 +322,7 @@ const S_PRIOR: &str = "§5 Prior artifacts, general tensors and derivation opera const S_INTERACTIVE: &str = "§5 Interactive, temporal & computed content"; const S_IMMERSIVE: &str = "§5 Immersive audio, cinema, volumetric, haptics, HDR"; const S_BROADCAST: &str = "§5 Broadcast and streaming codecs"; +const S_REFINEMENT: &str = "§5 Refinement capabilities (staged delivery)"; /// The date the derivation operations were registered. const DERIVATIONS_REGISTERED: &str = "2026-09-28"; @@ -375,8 +390,8 @@ pub struct Registration { pub status: RegistrationStatus, /// The media class, for a payload or companion capability. pub media: Option, - /// `standalone`, `companion` or `enhancement`, for a payload or companion - /// capability ([`capability_role`]). + /// `standalone`, `companion`, `enhancement` or `refinement`, for a payload, + /// companion or refinement capability ([`capability_role`]). pub role: Option<&'static str>, pub tier: Option, pub licence: Option, @@ -435,7 +450,13 @@ pub fn registrations() -> Vec { /// not register. pub fn capability_kind(cap: &str) -> Option { if is_known_capability(cap) { - Some(if is_companion_only(cap) { Kind::Companion } else { Kind::Payload }) + Some(if is_companion_only(cap) { + Kind::Companion + } else if is_refinement_only(cap) { + Kind::Refinement + } else { + Kind::Payload + }) } else if derivation::is_derivation_operation(cap) { Some(Kind::Derivation) } else if RECORDS.contains(&cap) { @@ -463,8 +484,10 @@ pub fn is_candidate(cap: &str) -> bool { /// Whether a sink may dispatch or select `cap`, as far as this revision's /// registry decides. False for every string this revision registers as something /// that is not a pinned payload: a companion-only capability (applied with a base, -/// never on its own), a candidate (SPEC §5, *Candidate capabilities*), a record and -/// a derivation (neither names a payload format). True for a registered payload +/// never on its own), a refinement-only capability (applied to the stage below it, +/// never on its own; `extensions/staged-delivery.md` §4 processes it), a candidate +/// (SPEC §5, *Candidate capabilities*), a record and a derivation (neither names a +/// payload format). True for a registered payload /// capability whose payload is pinned, and for a string this revision does not /// register: a later MINOR revision may register it (SPEC §8), and whether a sink /// can decode it is the sink's to say through what it advertises (SPEC §4, step 3). @@ -479,7 +502,7 @@ pub fn is_dispatchable(cap: &str) -> bool { match capability_kind(cap) { None => true, Some(Kind::Payload) => !is_candidate(cap), - Some(Kind::Companion | Kind::Derivation | Kind::Record) => false, + Some(Kind::Companion | Kind::Refinement | Kind::Derivation | Kind::Record) => false, } } @@ -855,13 +878,14 @@ pub fn vocab_v2_ttl() -> String { o } -/// The registered strings a manifest may name as its `capability`: payload and -/// companion capabilities whose status is registered (a candidate MUST NOT be -/// emitted). A `WAI1` `fallback` additionally excludes the companions. +/// The registered strings a manifest may name as its `capability`: payload, +/// companion and refinement capabilities whose status is registered (a candidate +/// MUST NOT be emitted). A `WAI1` `fallback` additionally excludes the companions +/// and the refinements. fn emittable() -> Vec { registrations() .into_iter() - .filter(|r| matches!(r.kind, Kind::Payload | Kind::Companion) && r.status == RegistrationStatus::Registered) + .filter(|r| matches!(r.kind, Kind::Payload | Kind::Companion | Kind::Refinement) && r.status == RegistrationStatus::Registered) .collect() } @@ -894,10 +918,11 @@ pub fn vocab_v2_shapes() -> String { o.push_str("@prefix xsd: .\n\n"); o.push_str("# A WAI manifest, typed or not: every manifest has a wai (version) field. A WAI1 manifest names a\n"); o.push_str("# capability, a WAI2 manifest a renditions list, and never both. The capability is a\n"); - o.push_str("# registered payload or companion capability that is not a candidate; the fallback a\n"); - o.push_str("# registered payload capability that is not a candidate. (A WAI1 naming a companion-only\n"); - o.push_str("# capability parses but does not conform; the reference implementation's role check\n"); - o.push_str("# reports it.) media is informational, and a reader accepts a value it does not know\n"); + o.push_str("# registered payload, companion or refinement capability that is not a candidate; the\n"); + o.push_str("# fallback a registered payload capability that is not a candidate. (A WAI1 naming a\n"); + o.push_str("# companion-only capability, or a refinement-only one without refines, parses but does\n"); + o.push_str("# not conform; the reference implementation's role check reports it.) media is\n"); + o.push_str("# informational, and a reader accepts a value it does not know\n"); o.push_str("# (SPEC sections 3 and 8), so only its form is checked.\n"); o.push_str("wai:ManifestShape a sh:NodeShape ;\n"); o.push_str(" sh:targetClass wai:Manifest ;\n"); @@ -908,9 +933,9 @@ pub fn vocab_v2_shapes() -> String { o.push_str(" sh:property [ sh:path wai:media ; sh:nodeKind sh:IRI ; sh:maxCount 1 ] ;\n"); o.push_str(" sh:property [ sh:path wai:intent ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ] ;\n"); o.push_str(" sh:xone (\n [ sh:property [ sh:path wai:capability ; sh:minCount 1 ] ]\n [ sh:property [ sh:path wai:renditions ; sh:minCount 1 ] ]\n ) .\n\n"); - o.push_str("# A WAI2 component, in either role: its capability is a registered payload or companion\n"); - o.push_str("# capability that is not a candidate, and the base a companion names is a registered\n"); - o.push_str("# payload capability that is not a candidate.\n"); + o.push_str("# A WAI2 component, in any role: its capability is a registered payload, companion or\n"); + o.push_str("# refinement capability that is not a candidate, and the base a companion names is a\n"); + o.push_str("# registered payload capability that is not a candidate.\n"); o.push_str("wai:ComponentShape a sh:NodeShape ;\n"); o.push_str(" sh:targetClass wai:Component ;\n"); o.push_str(" sh:targetObjectsOf wai:renditions ;\n"); @@ -1073,10 +1098,11 @@ mod tests { } assert!(!is_candidate("wai.image.nope")); assert_eq!(capability_status("wai.image.nope"), None); - // Among registered payload and companion capabilities, dispatchable is - // exactly: not companion-only, not a candidate. + // Among registered payload, companion and refinement capabilities, + // dispatchable is exactly: not companion-only, not refinement-only, not a + // candidate. for &cap in known_capabilities() { - assert_eq!(is_dispatchable(cap), !is_companion_only(cap) && !is_candidate(cap), "{cap}"); + assert_eq!(is_dispatchable(cap), !is_companion_only(cap) && !is_refinement_only(cap) && !is_candidate(cap), "{cap}"); } // Records and derivations name no payload format: never dispatched. for &cap in RECORDS { @@ -1266,7 +1292,7 @@ mod tests { for r in registrations() { let iri = format!("wai:{}\n", r.capability); let in_shapes = shapes.contains(&format!(" {iri}")); - let emittable = matches!(r.kind, Kind::Payload | Kind::Companion) && r.status == RegistrationStatus::Registered; + let emittable = matches!(r.kind, Kind::Payload | Kind::Companion | Kind::Refinement) && r.status == RegistrationStatus::Registered; assert_eq!(in_shapes, emittable, "{}", r.capability); assert!(vocab_v2_ttl().contains(&format!("wai:{} a wai:Capability", r.capability)), "{}", r.capability); } diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index 1d5223d6d..4be7bdc96 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -753,6 +753,12 @@ pub struct Manifest { pub conditioning: Conditioning, #[serde(default, skip_serializing_if = "is_none")] pub security: Option, + /// `refines` (`extensions/staged-delivery.md` §3): what a refinement-only + /// capability's payload refines, by content hash. Kept as the JSON it is, so a + /// malformed value still parses ([`Refines::from_value`] reports it, + /// `refines-malformed`). + #[serde(default, skip_serializing_if = "is_none")] + pub refines: Option, #[serde(default)] pub target: Value, /// Members this revision does not define, kept so they survive a round trip. @@ -775,6 +781,7 @@ impl Manifest { model_requirement, conditioning, security: None, + refines: None, target: Value::Null, extra: Map::new(), } @@ -784,6 +791,11 @@ impl Manifest { self.target = target; self } + /// Set `refines` (`extensions/staged-delivery.md` §3). + pub fn with_refines(mut self, refines: &Refines) -> Self { + self.refines = Some(refines.to_value()); + self + } /// Set the `wai` version string. pub fn with_wai(mut self, wai: impl Into) -> Self { self.wai = wai.into(); @@ -796,6 +808,231 @@ impl Manifest { } } +/// How the hashes of a `refines` member name objects: a content hash +/// (`jwp-receipts` §2.1, 64 lowercase hexadecimal digits) in a `WAI1` manifest, a +/// component `id` ([`is_component_id`]) in a `WAI2` entry, which names the entry +/// whose exploded envelope is the object (SPEC §7.1). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RefForm { + ContentHash, + ComponentId, +} + +impl RefForm { + fn accepts(self, s: &str) -> bool { + match self { + RefForm::ContentHash => s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)), + RefForm::ComponentId => is_component_id(s), + } + } +} + +/// `refines.prev` (`extensions/staged-delivery.md` §3): the producer each unit of +/// the span is expected to have — one for every unit, or runs in span order. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Prev { + One(String), + Runs(Vec), +} + +/// `count` consecutive units of a span whose producer is `producer`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrevRun { + pub producer: String, + pub count: u32, +} + +/// `refines.span`: `count` consecutive units from `from`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Span { + pub from: u32, + pub count: u32, +} + +/// A well-formed `refines` member (`extensions/staged-delivery.md` §3). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Refines { + pub base: String, + pub prev: Prev, + pub layer: u8, + pub span: Option, +} + +/// Why a `refines` member is malformed (`refines-malformed`): what failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefinesMalformed(pub &'static str); + +impl std::fmt::Display for RefinesMalformed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "refines-malformed: {}", self.0) + } +} + +impl std::error::Error for RefinesMalformed {} + +impl Refines { + /// Read `refines` as staged-delivery §3 defines it, with its references in `form`. + /// Members it does not define are ignored (SPEC §8). Malformed: a value that is + /// not an object; `base` or a producer that is not a string of `form`; `prev` + /// neither such a string nor a non-empty array of runs; a run that is not an + /// object of `producer` and an integer `count` ≥ 1; two adjacent runs with the + /// same producer, or an array of one run (a single producer is written as a + /// string); `layer` not an integer in 1..=255; at layer 1 a producer other than + /// `base`; `span` neither absent, `null`, nor an object of integer `from` ≥ 0 and + /// `count` ≥ 1, or past `u32`; with a span, run counts that do not sum to its + /// `count`. An integer is written as one (SPEC §2): `-0`, a fraction or an exponent + /// is not an integer, and `serde_json` reads each as a float, which no `as_u64` + /// accepts. Whether the + /// span lies within the base's units, and, with no span, whether the runs sum to + /// them, needs the base ([`Refines::check_units`]). + pub fn from_value(v: &Value, form: RefForm) -> Result { + let bad = |why: &'static str| RefinesMalformed(why); + let o = v.as_object().ok_or(bad("refines is not an object"))?; + let base = o.get("base").and_then(Value::as_str).filter(|b| form.accepts(b)).ok_or(bad("base is not a reference"))?; + let layer = o + .get("layer") + .and_then(Value::as_u64) + .filter(|l| (1..=255).contains(l)) + .ok_or(bad("layer is not an integer in 1..=255"))? as u8; + let span = match o.get("span") { + // `null` in an OPTIONAL member is the member absent (SPEC §3.1). + None | Some(Value::Null) => None, + Some(sp) => { + let so = sp.as_object().ok_or(bad("span is not an object"))?; + let from = so.get("from").and_then(Value::as_u64).and_then(|f| u32::try_from(f).ok()).ok_or(bad("span.from is not an integer ≥ 0 within u32"))?; + let count = so + .get("count") + .and_then(Value::as_u64) + .and_then(|c| u32::try_from(c).ok()) + .filter(|&c| c >= 1) + .ok_or(bad("span.count is not an integer ≥ 1 within u32"))?; + if from.checked_add(count).is_none() { + return Err(bad("the span runs past u32")); + } + Some(Span { from, count }) + } + }; + let prev = match o.get("prev") { + Some(Value::String(p)) if form.accepts(p) => Prev::One(p.clone()), + Some(Value::Array(a)) if !a.is_empty() => { + let mut runs = Vec::with_capacity(a.len().min(1 << 16)); + for r in a { + let ro = r.as_object().ok_or(bad("a prev run is not an object"))?; + let producer = ro.get("producer").and_then(Value::as_str).filter(|p| form.accepts(p)).ok_or(bad("a run's producer is not a reference"))?; + let count = ro + .get("count") + .and_then(Value::as_u64) + .and_then(|c| u32::try_from(c).ok()) + .filter(|&c| c >= 1) + .ok_or(bad("a run's count is not an integer ≥ 1 within u32"))?; + if runs.last().is_some_and(|l: &PrevRun| l.producer == producer) { + return Err(bad("two adjacent runs name the same producer")); + } + runs.push(PrevRun { producer: producer.to_string(), count }); + } + if runs.len() == 1 { + return Err(bad("prev is an array of one run; a single producer is written as a string")); + } + Prev::Runs(runs) + } + _ => return Err(bad("prev is neither a reference nor an array of runs")), + }; + let r = Refines { base: base.to_string(), prev, layer, span }; + if layer == 1 && r.producers().any(|p| p != r.base) { + return Err(bad("at layer 1 every producer prev names must be base")); + } + if let (Some(sp), Prev::Runs(runs)) = (span, &r.prev) { + if runs.iter().map(|x| x.count as u64).sum::() != sp.count as u64 { + return Err(bad("the run counts do not sum to the span's count")); + } + } + Ok(r) + } + + /// The JSON this member is written as: `base`, `prev`, `layer`, then `span` when + /// present. + pub fn to_value(&self) -> Value { + let mut o = Map::new(); + o.insert("base".into(), Value::from(self.base.clone())); + o.insert( + "prev".into(), + match &self.prev { + Prev::One(p) => Value::from(p.clone()), + Prev::Runs(runs) => Value::Array( + runs.iter() + .map(|r| { + let mut m = Map::new(); + m.insert("producer".into(), Value::from(r.producer.clone())); + m.insert("count".into(), Value::from(r.count)); + Value::Object(m) + }) + .collect(), + ), + }, + ); + o.insert("layer".into(), Value::from(self.layer)); + if let Some(sp) = self.span { + let mut m = Map::new(); + m.insert("from".into(), Value::from(sp.from)); + m.insert("count".into(), Value::from(sp.count)); + o.insert("span".into(), Value::Object(m)); + } + Value::Object(o) + } + + /// Every producer `prev` names, in span order. + pub fn producers(&self) -> impl Iterator { + let v: Vec<&str> = match &self.prev { + Prev::One(p) => vec![p.as_str()], + Prev::Runs(runs) => runs.iter().map(|r| r.producer.as_str()).collect(), + }; + v.into_iter() + } + + /// The units the layer covers, `(from, count)`, of a base of `base_units` units: + /// its span, or every unit. + pub fn units(&self, base_units: u32) -> (u32, u32) { + match self.span { + Some(sp) => (sp.from, sp.count), + None => (0, base_units), + } + } + + /// The checks that need the base's unit count (staged-delivery §3): the span lies + /// within the base's units, and the runs sum to the units the layer covers. + pub fn check_units(&self, base_units: u32) -> Result<(), RefinesMalformed> { + let (from, count) = self.units(base_units); + if from as u64 + count as u64 > base_units as u64 || count == 0 { + return Err(RefinesMalformed("the span lies outside the base's units")); + } + if let Prev::Runs(runs) = &self.prev { + if runs.iter().map(|x| x.count as u64).sum::() != count as u64 { + return Err(RefinesMalformed("the run counts do not sum to the units the layer covers")); + } + } + Ok(()) + } + + /// The producer `prev` gives for the `i`-th unit of the span (0-based), or + /// `None` past the runs. Linear in the runs; a sink resolving every unit of a span + /// indexes the runs once (`staged::StageChain` keeps their ends and bisects). + pub fn producer_for(&self, i: u32) -> Option<&str> { + match &self.prev { + Prev::One(p) => Some(p), + Prev::Runs(runs) => { + let mut at = 0u64; + for r in runs { + at += r.count as u64; + if (i as u64) < at { + return Some(&r.producer); + } + } + None + } + } + } +} + pub struct Wai { pub manifest: Manifest, pub payload: Vec, @@ -904,6 +1141,8 @@ impl Wai { need(4, 4, "manifest length")?; let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; need(8, ml, "manifest body")?; + check_unique_members(&bytes[8..8 + ml]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let m: Manifest = serde_json::from_slice(&bytes[8..8 + ml]) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let p_off = 8 + ml; @@ -922,6 +1161,12 @@ impl Wai { self.manifest.model_requirement.fallback.as_deref() } + /// The manifest's `refines`, read with content-hash references, or `None` when it + /// carries none (`extensions/staged-delivery.md` §3). + pub fn refines(&self) -> Option> { + self.manifest.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ContentHash)) + } + /// SPEC §4 dispatch at a sink that advertises the capabilities `advertises` /// accepts and verifies pins with `verify` (SPEC §3.1). First, SPEC §6.4 step 2: /// a manifest that declares an encryption other than `"none"` is inert here @@ -941,6 +1186,10 @@ impl Wai { /// fallback, step 5 is skipped, and step 3 is unaffected. So an envelope that /// [`Self::check_component_roles`] reports for its fallback alone still /// decodes at a sink that advertises its capability; + /// - a refinement-only capability ([`crate::codecs::is_refinement_only`]) is + /// handled alike (SPEC §4): its payload refines the stage below it and is + /// processed by `extensions/staged-delivery.md` §4 (`crate::staged`), never + /// decoded here; /// - a candidate ([`crate::codecs::is_candidate`]) is treated as unsupported /// (SPEC §5, *Candidate capabilities*): as the capability, step 5 may still /// take a usable fallback; as the fallback, it is never taken; @@ -962,7 +1211,10 @@ impl Wai { return Err(encryption_inert(format!("{:?}", sec.encryption.as_deref().unwrap_or_default()))); } let primary = self.capability(); - if crate::codecs::is_companion_only(primary) { + // A companion-only payload is companion data, and a refinement-only payload + // refines the stage below it (staged-delivery §4 processes it): neither is + // decoded on its own, so step 5 is not taken either. + if crate::codecs::is_companion_only(primary) || crate::codecs::is_refinement_only(primary) { return Err(Inert::MissingCapability); } let mut first_refusal: Option = None; @@ -1040,6 +1292,12 @@ impl Wai { /// MUST NOT be the envelope's `capability` — nor its `fallback`, because /// dispatch step 5 would decode the payload as that capability on its own. /// The v1.1 counterpart is [`WaiMulti::check_component_roles`]. + /// + /// A refinement-only capability ([`crate::codecs::is_refinement_only`]) is carried + /// as a `WAI1` `capability` only with a well-formed `refines` and a `null` + /// `fallback`, and is never a `fallback`; a manifest whose capability is not + /// refinement-only carries no `refines` (`extensions/staged-delivery.md` §3; SPEC + /// §8). Checked in that order, after the companion rules. pub fn check_component_roles(&self) -> Result<(), RoleError> { let capability = self.capability(); if crate::codecs::is_companion_only(capability) { @@ -1050,7 +1308,21 @@ impl Wai { return Err(RoleError::CompanionOnlyAsFallback { capability: fb.to_string() }); } } - Ok(()) + let refinement = crate::codecs::is_refinement_only(capability); + if let Some(fb) = self.fallback() { + if crate::codecs::is_refinement_only(fb) { + return Err(RoleError::RefinementOnlyAsFallback { capability: fb.to_string() }); + } + } + match (refinement, self.refines()) { + (true, None) => Err(RoleError::RefinementWithoutRefines { capability: capability.to_string() }), + (true, Some(Err(e))) => Err(RoleError::RefinesMalformed { index: None, detail: e.0 }), + (true, Some(Ok(_))) if self.fallback().is_some() => { + Err(RoleError::RefinementWithFallback { capability: capability.to_string() }) + } + (false, Some(_)) => Err(RoleError::RefinesWithoutRefinement { capability: capability.to_string() }), + _ => Ok(()), + } } /// Check that this envelope names no candidate capability @@ -1097,6 +1369,64 @@ impl Wai { } } +/// Whether the JSON text `text` names a member twice in one object, at any depth +/// (SPEC §2: member names are unique within an object). `serde_json` keeps the last of +/// two equal names in a `Value` and refuses them only in a struct's own fields, so a +/// manifest is read once more by this check, which refuses any repeat. Names compare +/// after escapes are read, so `"a"` and `"\u0061"` are one name. The text's own JSON +/// errors are reported as they are. +pub fn check_unique_members(text: &[u8]) -> Result<(), serde_json::Error> { + use serde::de::{DeserializeSeed, Deserializer, Error as _, MapAccess, SeqAccess, Visitor}; + struct Unique; + impl<'de> DeserializeSeed<'de> for Unique { + type Value = (); + fn deserialize>(self, d: D) -> Result<(), D::Error> { + d.deserialize_any(Unique) + } + } + impl<'de> Visitor<'de> for Unique { + type Value = (); + fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("a JSON value") + } + fn visit_bool(self, _: bool) -> Result<(), E> { + Ok(()) + } + fn visit_i64(self, _: i64) -> Result<(), E> { + Ok(()) + } + fn visit_u64(self, _: u64) -> Result<(), E> { + Ok(()) + } + fn visit_f64(self, _: f64) -> Result<(), E> { + Ok(()) + } + fn visit_str(self, _: &str) -> Result<(), E> { + Ok(()) + } + fn visit_unit(self) -> Result<(), E> { + Ok(()) + } + fn visit_seq>(self, mut seq: A) -> Result<(), A::Error> { + while seq.next_element_seed(Unique)?.is_some() {} + Ok(()) + } + fn visit_map>(self, mut map: A) -> Result<(), A::Error> { + let mut seen = std::collections::HashSet::new(); + while let Some(k) = map.next_key::()? { + if !seen.insert(k) { + return Err(A::Error::custom("an object names a member twice")); + } + map.next_value_seed(Unique)?; + } + Ok(()) + } + } + let mut de = serde_json::Deserializer::from_slice(text); + Unique.deserialize(&mut de)?; + de.end() +} + /// The manifest bytes of a `WAI1` or `WAI2` envelope, exactly as they were /// written, bounds-checked as the parsers check them. A tool that hands a manifest /// on (the C FFI's unpack calls) returns these, so nothing the Rust types do not @@ -1160,6 +1490,18 @@ pub struct RenditionMeta { /// binds by capability. #[serde(default, skip_serializing_if = "is_none")] pub companion_of_id: Option, + /// For a refinement (`role: "refinement"`): what it refines + /// (`extensions/staged-delivery.md` §3), its base and producers named by the `id` + /// of entries in this envelope (SPEC §7.1, *Refinement components*). Kept as the + /// JSON it is, so a malformed value still parses ([`Refines::from_value`] with + /// [`RefForm::ComponentId`] reports it). + #[serde(default, skip_serializing_if = "is_none")] + pub refines: Option, + /// The exact manifest of this entry as a `WAI1` envelope (SPEC §7.1, *Exploding a + /// component*): what [`WaiMulti::explode`] writes as the exploded envelope's + /// manifest, verbatim from the envelope bytes. + #[serde(default, skip_serializing_if = "is_none")] + pub envelope_manifest: Option, /// Members this revision does not define, kept so they survive a round trip. #[serde(flatten)] pub extra: Map, @@ -1178,6 +1520,8 @@ impl RenditionMeta { role: None, companion_of: None, companion_of_id: None, + refines: None, + envelope_manifest: None, extra: Map::new(), } } @@ -1219,6 +1563,11 @@ impl RenditionMeta { self.role.as_deref() == Some("companion") } + /// Whether this component is a refinement (`role: "refinement"`, SPEC §7.1). + pub fn is_refinement(&self) -> bool { + self.role.as_deref() == Some("refinement") + } + /// Whether `pick`/`primary` may select this component: it is not marked a /// companion **and** [`crate::codecs::is_dispatchable`] accepts its /// capability — so not a companion-only capability, a candidate, a record or @@ -1227,7 +1576,7 @@ impl RenditionMeta { /// own. [`WaiMulti::check_component_roles`] reports the mislabelling, and /// [`WaiMulti::check_candidates`] a candidate. pub fn is_selectable(&self) -> bool { - !self.is_companion() && crate::codecs::is_dispatchable(&self.capability) + !self.is_companion() && !self.is_refinement() && crate::codecs::is_dispatchable(&self.capability) } /// Whether `payload` matches this entry's stated `sha256` (true when none is @@ -1280,6 +1629,71 @@ pub enum RoleError { /// A companion whose `companion_of_id` names a component whose capability is /// `capability`, not the companion's `companion_of`. CompanionIdCapabilityMismatch { index: usize, id: String, capability: String }, + /// v1.0: a refinement-only capability named as the `WAI1` `fallback`. It is never + /// decoded on its own. + RefinementOnlyAsFallback { capability: String }, + /// v1.0: a refinement-only `capability` with no `refines` + /// (`extensions/staged-delivery.md` §3). + RefinementWithoutRefines { capability: String }, + /// v1.0: a refinement-only `capability` with a `fallback`; its fallback MUST be + /// `null`. + RefinementWithFallback { capability: String }, + /// v1.0: `refines` on a manifest whose capability is not refinement-only. + RefinesWithoutRefinement { capability: String }, + /// A `refines` member that is malformed (`refines-malformed`): the `WAI2` + /// component's index, or `None` for a `WAI1` manifest. + RefinesMalformed { index: Option, detail: &'static str }, + /// v1.1: a refinement-only capability carried as a rendition or a companion. + RefinementOnlyAsComponent { index: usize, capability: String }, + /// v1.1: `refines` on an entry whose role is not `"refinement"`. + RefinesOnNonRefinement { index: usize }, + /// v1.1: a `role: "refinement"` entry whose capability is not refinement-only. + RefinementNotRefinementOnly { index: usize, capability: String }, + /// v1.1: a refinement, or an entry it names, without an `id` or a `sha256`. + RefinementIdentityMissing { index: usize }, + /// v1.1: a refinement whose `refines.base` names no selectable entry. + RefinementBaseMissing { index: usize, base: String }, + /// v1.1: a refinement whose capability does not admit its base entry's (SPEC §5, + /// *Base admission*). + RefinementBaseNotAdmitted { index: usize, capability: String, base: String }, + /// v1.1: a producer in a refinement's `prev` that names neither a selectable entry + /// nor a refinement before it. + RefinementPrevMissing { index: usize, producer: String }, + /// v1.1: a refinement placed before a selectable entry. + RefinementBeforeSelectable { index: usize, selectable: usize }, +} + +impl RoleError { + /// A token for the error, for tools and corpora. + pub fn code(&self) -> &'static str { + match self { + RoleError::CompanionOnlyAsCapability { .. } => "companion-only-as-capability", + RoleError::CompanionOnlyAsFallback { .. } => "companion-only-as-fallback", + RoleError::CompanionOnlyAsRendition { .. } => "companion-only-as-rendition", + RoleError::StandaloneAsCompanion { .. } => "standalone-as-companion", + RoleError::UnboundCompanion { .. } => "unbound-companion", + RoleError::IncompatibleBase { .. } => "incompatible-base", + RoleError::EnhancementBaseNotUnique { .. } => "enhancement-base-not-unique", + RoleError::DuplicateEnhancementLayer { .. } => "duplicate-enhancement-layer", + RoleError::ComponentIdMalformed { .. } => "component-id-malformed", + RoleError::DuplicateComponentId { .. } => "duplicate-component-id", + RoleError::CompanionIdUnresolved { .. } => "companion-id-unresolved", + RoleError::CompanionIdCapabilityMismatch { .. } => "companion-id-capability-mismatch", + RoleError::RefinementOnlyAsFallback { .. } => "refinement-only-as-fallback", + RoleError::RefinementWithoutRefines { .. } => "refinement-without-refines", + RoleError::RefinementWithFallback { .. } => "refinement-with-fallback", + RoleError::RefinesWithoutRefinement { .. } => "refines-without-refinement", + RoleError::RefinesMalformed { .. } => "refines-malformed", + RoleError::RefinementOnlyAsComponent { .. } => "refinement-only-as-component", + RoleError::RefinesOnNonRefinement { .. } => "refines-on-non-refinement", + RoleError::RefinementNotRefinementOnly { .. } => "refinement-not-refinement-only", + RoleError::RefinementIdentityMissing { .. } => "refinement-identity-missing", + RoleError::RefinementBaseMissing { .. } => "refinement-base-missing", + RoleError::RefinementBaseNotAdmitted { .. } => "refinement-base-not-admitted", + RoleError::RefinementPrevMissing { .. } => "refinement-prev-missing", + RoleError::RefinementBeforeSelectable { .. } => "refinement-before-selectable", + } + } } impl std::fmt::Display for RoleError { @@ -1318,6 +1732,32 @@ impl std::fmt::Display for RoleError { f, "companion {index} names companion_of_id '{id}', which no component has"), RoleError::CompanionIdCapabilityMismatch { index, id, capability } => write!( f, "companion {index} names companion_of_id '{id}', whose capability {capability} is not its companion_of"), + RoleError::RefinementOnlyAsFallback { capability } => write!( + f, "{capability} is refinement-only and cannot be a WAI1 fallback; it is never decoded on its own"), + RoleError::RefinementWithoutRefines { capability } => write!( + f, "{capability} is refinement-only and its manifest carries no refines (extensions/staged-delivery.md \u{a7}3)"), + RoleError::RefinementWithFallback { capability } => write!( + f, "{capability} is refinement-only, so its fallback must be null"), + RoleError::RefinesWithoutRefinement { capability } => write!( + f, "refines is carried with {capability}, which is not refinement-only"), + RoleError::RefinesMalformed { index: None, detail } => write!(f, "refines-malformed: {detail}"), + RoleError::RefinesMalformed { index: Some(i), detail } => write!(f, "component {i}: refines-malformed: {detail}"), + RoleError::RefinementOnlyAsComponent { index, capability } => write!( + f, "component {index} ({capability}) is refinement-only and is not carried with role=\"refinement\""), + RoleError::RefinesOnNonRefinement { index } => write!( + f, "component {index} carries refines but its role is not \"refinement\""), + RoleError::RefinementNotRefinementOnly { index, capability } => write!( + f, "component {index} has role=\"refinement\" but {capability} is not refinement-only"), + RoleError::RefinementIdentityMissing { index } => write!( + f, "refinement {index}, or an entry it names, carries no id or no sha256 (SPEC \u{a7}7.1)"), + RoleError::RefinementBaseMissing { index, base } => write!( + f, "refinement {index} names base '{base}', which is no selectable entry"), + RoleError::RefinementBaseNotAdmitted { index, capability, base } => write!( + f, "refinement {index} ({capability}) does not apply over {base} (SPEC \u{a7}5, Base admission)"), + RoleError::RefinementPrevMissing { index, producer } => write!( + f, "refinement {index} names producer '{producer}', which is neither a selectable entry nor a refinement before it"), + RoleError::RefinementBeforeSelectable { index, selectable } => write!( + f, "refinement {index} precedes selectable entry {selectable}; refinements follow every selectable entry"), } } } @@ -1414,7 +1854,11 @@ pub struct FramingError { impl std::fmt::Display for FramingError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let want = crate::codecs::pinned_int_payload(&self.capability).map(|m| String::from_utf8_lossy(&m).into_owned()); + let want = if crate::codecs::is_refinement_only(&self.capability) { + Some("WIR1".to_string()) + } else { + crate::codecs::pinned_int_payload(&self.capability).map(|m| String::from_utf8_lossy(&m).into_owned()) + }; write!(f, "the payload is not the {} payload SPEC \u{a7}5 pins {} to ({}: {}; extensions/integer-payloads.md)", want.as_deref().unwrap_or("?"), self.capability, self.code, self.detail) } @@ -1433,6 +1877,9 @@ impl std::error::Error for FramingError {} /// it is decrypted (SPEC §6.4), and its emit paths skip the check. pub fn check_int_payload_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> { use crate::int_wire as w; + if crate::codecs::is_refinement_only(capability) { + return check_refinement_framing(capability, payload); + } let parsed = match crate::codecs::pinned_int_payload(capability) { None => return Ok(()), Some(w::MAGIC_WIV1) => w::parse_wiv1(payload, Some((capability == crate::codecs::CAP_VIDEO_INT_HYPER) as u8)).map(|_| ()), @@ -1443,6 +1890,26 @@ pub fn check_int_payload_framing(capability: &str, payload: &[u8]) -> Result<(), parsed.map_err(|e| FramingError { capability: capability.to_string(), code: e.code(), detail: e.detail }) } +/// The framing check of a refinement-only capability's payload: `WIR1`, parsed for +/// the capability's unit kind (`extensions/staged-delivery.md` Appendix A.1). A build +/// without the `staged` feature has no `WIR1` parser, so it refuses to emit one +/// (`unsupported_capability`). +#[cfg(feature = "staged")] +fn check_refinement_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> { + let fail = |code: &'static str, detail: &'static str| FramingError { capability: capability.to_string(), code, detail }; + let kind = crate::staged::unit_kind(capability).ok_or(fail("unsupported_capability", "no unit kind for this refinement"))?; + crate::staged::wir1::parse(payload, kind).map(|_| ()).map_err(|e| fail(e.code(), e.detail)) +} + +#[cfg(not(feature = "staged"))] +fn check_refinement_framing(capability: &str, _: &[u8]) -> Result<(), FramingError> { + Err(FramingError { + capability: capability.to_string(), + code: "unsupported_capability", + detail: "this build cannot check a WIR1 payload (feature staged)", + }) +} + /// What an integer payload's header declares about its decode, read before any /// symbol is decoded and with no parameter set (`extensions/integer-payloads.md` §7). /// A sink applies its own size policy to it and refuses a payload past that policy @@ -1763,6 +2230,8 @@ impl WaiMulti { need(4, 4, "manifest length")?; let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize; need(8, ml, "manifest body")?; + check_unique_members(&bytes[8..8 + ml]) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; let manifest: ManifestV2 = serde_json::from_slice(&bytes[8..8 + ml]) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; @@ -1968,6 +2437,71 @@ impl WaiMulti { } } + /// SPEC §7.1, *Refinement components*, for the entry at `index`, in this order: + /// its capability is refinement-only; it carries `id` and `sha256`; its `refines` + /// is well formed with component ids; `refines.base` names a selectable entry with + /// an `id` and a `sha256`; its capability admits that entry's (SPEC §5, *Base + /// admission*); every producer of `prev` names a selectable entry or a refinement + /// before it, with an `id` and a `sha256`; and it follows every selectable entry. + fn check_refinement(&self, index: usize) -> Result<(), RoleError> { + let metas = &self.manifest.renditions; + let m = &metas[index]; + if !crate::codecs::is_refinement_only(&m.capability) { + return Err(RoleError::RefinementNotRefinementOnly { index, capability: m.capability.clone() }); + } + if m.id.is_none() || m.sha256.is_none() { + return Err(RoleError::RefinementIdentityMissing { index }); + } + let refines = match m.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ComponentId)) { + Some(Ok(r)) => r, + Some(Err(e)) => return Err(RoleError::RefinesMalformed { index: Some(index), detail: e.0 }), + None => return Err(RoleError::RefinesMalformed { index: Some(index), detail: "a refinement carries no refines" }), + }; + let by_id = |id: &str| metas.iter().position(|e| e.id.as_deref() == Some(id)); + let Some(b) = by_id(&refines.base).filter(|&b| metas[b].is_selectable()) else { + return Err(RoleError::RefinementBaseMissing { index, base: refines.base.clone() }); + }; + if metas[b].sha256.is_none() { + return Err(RoleError::RefinementIdentityMissing { index }); + } + if !crate::codecs::admits(&m.capability, &metas[b].capability) { + return Err(RoleError::RefinementBaseNotAdmitted { index, capability: m.capability.clone(), base: metas[b].capability.clone() }); + } + for p in refines.producers() { + let ok = by_id(p).filter(|&e| metas[e].is_selectable() || (e < index && metas[e].is_refinement())); + match ok { + None => return Err(RoleError::RefinementPrevMissing { index, producer: p.to_string() }), + Some(e) if metas[e].sha256.is_none() => return Err(RoleError::RefinementIdentityMissing { index }), + Some(_) => {} + } + } + if let Some(selectable) = (index + 1..metas.len()).find(|&j| metas[j].is_selectable()) { + return Err(RoleError::RefinementBeforeSelectable { index, selectable }); + } + Ok(()) + } + + /// The refinements of the entry at `index`, in table order: each entry with + /// `role: "refinement"` whose `refines.base` names that entry's `id`, and whose + /// payload matches its `sha256` (a refinement that does not match is not applied, + /// SPEC §7.1). A sink offers their exploded envelopes to staged-delivery §4 + /// ([`WaiMulti::explode`]) after [`Self::check_component_roles`] passes. + pub fn refinements_of(&self, index: usize) -> Vec<(usize, &RenditionMeta, &[u8])> { + let metas = &self.manifest.renditions; + let Some(base_id) = metas.get(index).and_then(|b| b.id.as_deref()) else { return Vec::new() }; + metas + .iter() + .zip(self.renditions.iter()) + .enumerate() + .filter(|(_, (m, p))| { + m.is_refinement() + && m.payload_digest_matches(p) + && m.refines.as_ref().and_then(|v| Refines::from_value(v, RefForm::ComponentId).ok()).is_some_and(|r| r.base == base_id) + }) + .map(|(i, (m, p))| (i, m, p.as_slice())) + .collect() + } + /// Check the component table against SPEC §5 / §7.1, using the capability /// registry, in component order (the first violation is returned): /// @@ -1975,6 +2509,10 @@ impl WaiMulti { /// ([`RoleError::ComponentIdMalformed`], [`RoleError::DuplicateComponentId`]); /// - a companion-only capability is carried with `role = "companion"` /// ([`RoleError::CompanionOnlyAsRendition`]); + /// - a `role = "refinement"` entry obeys *Refinement components* + /// ([`Self::check_refinement`]'s rules); `refines` sits on no other entry, and a + /// refinement-only capability on no other role + /// ([`RoleError::RefinesOnNonRefinement`], [`RoleError::RefinementOnlyAsComponent`]); /// /// and, for each `role = "companion"` component, SPEC §7.1's three rules: /// @@ -2009,6 +2547,16 @@ impl WaiMulti { } for (index, m) in metas.iter().enumerate() { let capability = m.capability.clone(); + if m.is_refinement() { + self.check_refinement(index)?; + continue; + } + if m.refines.is_some() { + return Err(RoleError::RefinesOnNonRefinement { index }); + } + if crate::codecs::is_refinement_only(&m.capability) { + return Err(RoleError::RefinementOnlyAsComponent { index, capability }); + } let companion_only = crate::codecs::is_companion_only(&m.capability); if !m.is_companion() { if companion_only { diff --git a/wai/wai-rs/src/container_explode.rs b/wai/wai-rs/src/container_explode.rs new file mode 100644 index 000000000..6b3f283bf --- /dev/null +++ b/wai/wai-rs/src/container_explode.rs @@ -0,0 +1,311 @@ +//! Exploding a `WAI2` component into its `WAI1` envelope, and packing `WAI1` envelopes +//! into a `WAI2` (SPEC §7.1, *Exploding a component*; *Refinement components*). +//! +//! A component and its exploded envelope are the same object for every rule that +//! names an object by content hash, so a refinement carried in a `WAI2` and the same +//! layer carried alone reach the same stages (`extensions/staged-delivery.md` §9). +//! Explode reads the `WAI2` manifest's members as the exact text they are in the +//! envelope (`serde_json::value::RawValue`), never re-serialised, so a component packed +//! with its manifest in `envelope_manifest` explodes back to the bytes it was packed +//! from. Identity is the entry's `id`, `sha256` and `prior` (SPEC §7.1, *Component +//! identity*): a refinement entry names its base and producers by `id`, and its +//! exploded envelope names them by the content hashes of their exploded envelopes. + +use std::collections::BTreeMap; + +use serde::Deserialize; +use serde_json::value::RawValue; +use serde_json::Value; + +use crate::container::{is_component_id, manifest_bytes, Prev, PrevRun, RefForm, Refines, Wai, WaiMulti, MAGIC_V2}; + +fn bad(m: impl Into) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, m.into()) +} + +#[derive(Deserialize)] +struct RawV2<'a> { + #[serde(borrow)] + wai: &'a RawValue, + #[serde(borrow)] + media: &'a RawValue, + #[serde(borrow)] + intent: &'a RawValue, + #[serde(borrow)] + renditions: Vec>, + #[serde(default, borrow)] + target: Option<&'a RawValue>, + #[serde(default, borrow)] + security: Option<&'a RawValue>, +} + +#[derive(Deserialize)] +struct RawEntry<'a> { + #[serde(borrow)] + capability: &'a RawValue, + #[serde(borrow)] + kind: &'a RawValue, + #[serde(default, borrow)] + prior: Option<&'a RawValue>, + #[serde(default, borrow)] + envelope_manifest: Option<&'a RawValue>, +} + +/// A `WAI1` envelope with manifest text `manifest` and `payload`. +fn wai1(manifest: &[u8], payload: &[u8]) -> Vec { + let mut out = Vec::with_capacity(12 + manifest.len() + payload.len()); + out.extend_from_slice(crate::container::MAGIC); + out.extend_from_slice(&(manifest.len() as u32).to_le_bytes()); + out.extend_from_slice(manifest); + out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + out.extend_from_slice(payload); + out +} + +/// `refines` with each reference translated by `map`, written in the form +/// [`Refines::to_value`] gives it, without whitespace. +fn translate(r: &Refines, map: &dyn Fn(&str) -> Option) -> std::io::Result { + let tr = |s: &str| map(s).ok_or_else(|| bad(format!("refines names '{s}', which no entry before it is"))); + Ok(Refines { + base: tr(&r.base)?, + prev: match &r.prev { + Prev::One(p) => Prev::One(tr(p)?), + Prev::Runs(runs) => Prev::Runs(runs.iter().map(|x| Ok(PrevRun { producer: tr(&x.producer)?, count: x.count })).collect::>()?), + }, + layer: r.layer, + span: r.span, + }) +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +impl WaiMulti { + /// Every component of the `WAI2` envelope `envelope` as its exploded `WAI1` + /// envelope, in table order (SPEC §7.1, *Exploding a component*): the payload is the + /// entry's rendition bytes; the manifest is the entry's `envelope_manifest` exactly + /// as written, or else + /// `{"wai":W,"media":M,"intent":I,"model_requirement":{"capability":C,"fallback":null},` + /// `"conditioning":{"kind":K[,"prior":P]}[,"security":S][,"refines":R][,"target":T]}`, + /// where `W`, `M`, `I`, `S` and `T` are the `WAI2` manifest's members as written, `C`, + /// `K` and `P` the entry's, and `R` the entry's `refines` with each component id + /// replaced by the content hash of that entry's exploded envelope. An entry a + /// refinement names must come before it. + pub fn explode_all(envelope: &[u8]) -> std::io::Result>> { + Self::explode_each(envelope)?.into_iter().collect() + } + + /// [`Self::explode_all`], entry by entry: an entry whose `refines` cannot be + /// translated (it is malformed, or names an entry that is not before it, or one + /// that could not be exploded) has its own error, and the others their envelopes. + /// Entry `i`'s envelope is defined by entry `i` and the entries it names, so one + /// entry that cannot be exploded leaves every entry that does not name it intact. + pub fn explode_each(envelope: &[u8]) -> std::io::Result>>> { + let w = WaiMulti::from_bytes(envelope)?; + let mtext = manifest_bytes(envelope)?; + let raw: RawV2 = serde_json::from_slice(mtext).map_err(|e| bad(format!("WAI2 manifest: {e}")))?; + if raw.renditions.len() != w.manifest.renditions.len() { + return Err(bad("the manifest's entries do not read alike")); + } + let mut ids: BTreeMap = BTreeMap::new(); + let mut out = Vec::with_capacity(raw.renditions.len()); + for (i, (e, meta)) in raw.renditions.iter().zip(&w.manifest.renditions).enumerate() { + let payload = &w.renditions[i]; + let manifest = (|| -> std::io::Result> { Ok(match e.envelope_manifest { + Some(m) => m.get().as_bytes().to_vec(), + None => { + let mut s = String::new(); + s.push_str("{\"wai\":"); + s.push_str(raw.wai.get()); + s.push_str(",\"media\":"); + s.push_str(raw.media.get()); + s.push_str(",\"intent\":"); + s.push_str(raw.intent.get()); + s.push_str(",\"model_requirement\":{\"capability\":"); + s.push_str(e.capability.get()); + s.push_str(",\"fallback\":null},\"conditioning\":{\"kind\":"); + s.push_str(e.kind.get()); + if let Some(p) = e.prior { + s.push_str(",\"prior\":"); + s.push_str(p.get()); + } + s.push('}'); + if let Some(sec) = raw.security { + s.push_str(",\"security\":"); + s.push_str(sec.get()); + } + if let Some(v) = meta.refines.as_ref() { + let r = Refines::from_value(v, RefForm::ComponentId).map_err(|e| bad(format!("entry {i}: {e}")))?; + let t = translate(&r, &|id| ids.get(id).cloned())?; + s.push_str(",\"refines\":"); + s.push_str(&serde_json::to_string(&t.to_value()).map_err(|e| bad(e.to_string()))?); + } + if let Some(t) = raw.target { + s.push_str(",\"target\":"); + s.push_str(t.get()); + } + s.push('}'); + s.into_bytes() + } + }) })(); + let env = manifest.map(|m| wai1(&m, payload)); + if let (Some(id), Ok(env)) = (meta.id.as_deref(), env.as_ref()) { + ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(env))); + } + out.push(env); + } + Ok(out) + } + + /// Component `index` of the `WAI2` envelope `envelope` as its exploded `WAI1` + /// envelope ([`Self::explode_each`]): its own result, whatever the other entries'. + pub fn explode(envelope: &[u8], index: usize) -> std::io::Result> { + Self::explode_each(envelope)?.into_iter().nth(index).ok_or_else(|| bad(format!("no component {index}")))? + } + + /// Pack `WAI1` envelopes into one `WAI2`, in order: each entry carries the + /// envelope's capability, kind and pin, the id `c`, its payload's `sha256`, and + /// its manifest verbatim as `envelope_manifest`; an entry whose capability is + /// refinement-only takes `role: "refinement"` and its `refines` with each content + /// hash replaced by the id of the entry whose envelope has it. `wai`, `media`, + /// `intent` and `target` come from the first envelope. So every envelope explodes + /// back to its own bytes ([`Self::explode_all`]). Refused: an encrypted envelope; + /// a refinement whose base or producers are not among the envelopes before it; an + /// envelope with bytes around its manifest's JSON value or after its payload, which + /// no explode writes back (SPEC §7.1); and an output that does not parse, explode + /// back to every source byte for byte, and pass + /// [`Self::check_envelope_manifests`], which pack checks before it returns (a + /// manifest nested near the JSON depth limit as a `WAI1` passes it as a member of + /// a `WAI2` manifest). + pub fn pack(w1s: &[&[u8]]) -> std::io::Result> { + let out = Self::pack_unchecked(w1s)?; + WaiMulti::from_bytes(&out).map_err(|e| bad(format!("the packed WAI2 does not parse: {e}")))?; + let back = Self::explode_all(&out).map_err(|e| bad(format!("the packed WAI2 does not explode: {e}")))?; + if back.len() != w1s.len() || back.iter().zip(w1s).any(|(a, b)| a.as_slice() != *b) { + return Err(bad("the packed WAI2 does not explode back to its sources")); + } + Self::check_envelope_manifests(&out).map_err(|(i, c)| bad(format!("the packed WAI2's entry {i}: {c}")))?; + Ok(out) + } + + fn pack_unchecked(w1s: &[&[u8]]) -> std::io::Result> { + if w1s.is_empty() { + return Err(bad("nothing to pack")); + } + let mut entries = Vec::new(); + let mut payloads: Vec> = Vec::new(); + let mut by_hash: BTreeMap = BTreeMap::new(); + let mut first: Option<(String, String, String)> = None; + for (i, b) in w1s.iter().enumerate() { + let w = Wai::from_bytes(b)?; + if w.manifest.security.as_ref().is_some_and(|s| !s.is_plaintext()) { + return Err(bad(format!("envelope {i} is encrypted"))); + } + let mtext = std::str::from_utf8(manifest_bytes(b)?).map_err(|e| bad(e.to_string()))?; + // The manifest is copied as the value of `envelope_manifest`, where bytes + // around a JSON value are not part of it: such a manifest cannot come back. + let value: &RawValue = serde_json::from_str(mtext).map_err(|e| bad(e.to_string()))?; + if value.get().len() != mtext.len() { + return Err(bad(format!("envelope {i}'s manifest has bytes around its JSON value"))); + } + if b.len() != 12 + mtext.len() + w.payload.len() { + return Err(bad(format!("envelope {i} has bytes after its payload"))); + } + let id = format!("c{i}"); + debug_assert!(is_component_id(&id)); + let mut e = serde_json::Map::new(); + e.insert("capability".into(), Value::from(w.capability())); + e.insert("kind".into(), Value::from(w.kind())); + e.insert("id".into(), Value::from(id.clone())); + e.insert("sha256".into(), Value::from(crate::pin::sha256_hex(&w.payload))); + if let Some(p) = &w.manifest.conditioning.prior { + e.insert("prior".into(), serde_json::to_value(p).map_err(|x| bad(x.to_string()))?); + } + if crate::codecs::is_refinement_only(w.capability()) { + e.insert("role".into(), Value::from("refinement")); + let r = w.refines().ok_or_else(|| bad(format!("envelope {i} is a refinement with no refines")))?.map_err(|x| bad(x.to_string()))?; + let t = translate(&r, &|h| by_hash.get(h).cloned())?; + e.insert("refines".into(), t.to_value()); + } + let mut entry = serde_json::to_string(&Value::Object(e)).map_err(|x| bad(x.to_string()))?; + entry.pop(); + entry.push_str(",\"envelope_manifest\":"); + entry.push_str(mtext); + entry.push('}'); + entries.push(entry); + if first.is_none() { + let tv = |v: &Value| serde_json::to_string(v).unwrap_or_else(|_| "null".into()); + first = Some((tv(&Value::from(w.media())), tv(&Value::from(w.intent())), tv(&w.manifest.target))); + } + by_hash.insert(hex(&crate::jwp_hash::content_hash(b)), id); + payloads.push(w.payload); + } + let (media, intent, target) = first.expect("one envelope"); + let manifest = format!("{{\"wai\":\"1.1\",\"media\":{media},\"intent\":{intent},\"renditions\":[{}],\"target\":{target}}}", entries.join(",")); + if payloads.len() > u16::MAX as usize { + return Err(bad("too many components")); + } + let mut out = Vec::new(); + out.extend_from_slice(MAGIC_V2); + out.extend_from_slice(&(manifest.len() as u32).to_le_bytes()); + out.extend_from_slice(manifest.as_bytes()); + out.extend_from_slice(&(payloads.len() as u16).to_le_bytes()); + let mut off = 0u32; + for p in &payloads { + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&(p.len() as u32).to_le_bytes()); + off = off.checked_add(p.len() as u32).ok_or_else(|| bad("payloads past u32"))?; + } + for p in &payloads { + out.extend_from_slice(p); + } + Ok(out) + } + + /// Check every entry's `envelope_manifest` against the entry (SPEC §7.1): it is a + /// `WAI1` manifest whose `model_requirement.capability`, `conditioning.kind` and + /// `conditioning.prior` are the entry's, whose `refines` is the entry's with each + /// id replaced by the content hash of that entry's exploded envelope (or both are + /// absent), and, for a refinement-only capability, whose `fallback` is `null`. The + /// first entry that fails, with `component-manifest-mismatch`. + pub fn check_envelope_manifests(envelope: &[u8]) -> Result<(), (usize, &'static str)> { + const CODE: &str = "component-manifest-mismatch"; + let w = WaiMulti::from_bytes(envelope).map_err(|_| (0, CODE))?; + let exploded = Self::explode_each(envelope).map_err(|_| (0, CODE))?; + let mut ids: BTreeMap = BTreeMap::new(); + for (i, (meta, env)) in w.manifest.renditions.iter().zip(&exploded).enumerate() { + if let Some(m) = &meta.envelope_manifest { + let mm: crate::container::Manifest = serde_json::from_value(m.clone()).map_err(|_| (i, CODE))?; + let as_json = |p: Option<&crate::container::Prior>| p.map(|p| serde_json::to_value(p).ok()); + let prior_eq = as_json(mm.conditioning.prior.as_ref()) == as_json(meta.prior.as_ref()); + if mm.model_requirement.capability != meta.capability || mm.conditioning.kind != meta.kind || !prior_eq { + return Err((i, CODE)); + } + // A refinement-only capability's manifest names no fallback + // (staged-delivery §3). + if crate::codecs::is_refinement_only(&meta.capability) && mm.model_requirement.fallback.is_some() { + return Err((i, CODE)); + } + let entry_refines = match &meta.refines { + None => None, + Some(v) => { + let r = Refines::from_value(v, RefForm::ComponentId).map_err(|_| (i, CODE))?; + Some(translate(&r, &|id| ids.get(id).cloned()).map_err(|_| (i, CODE))?) + } + }; + let wai1_refines = match &mm.refines { + None => None, + Some(v) => Some(Refines::from_value(v, RefForm::ContentHash).map_err(|_| (i, CODE))?), + }; + if entry_refines != wai1_refines { + return Err((i, CODE)); + } + } + if let (Some(id), Ok(env)) = (meta.id.as_deref(), env.as_ref()) { + ids.insert(id.to_string(), hex(&crate::jwp_hash::content_hash(env))); + } + } + Ok(()) + } +} diff --git a/wai/wai-rs/src/int_entropy.rs b/wai/wai-rs/src/int_entropy.rs index c36bb5325..88272b1ba 100644 --- a/wai/wai-rs/src/int_entropy.rs +++ b/wai/wai-rs/src/int_entropy.rs @@ -96,6 +96,53 @@ pub fn dequantize(sym: i32, frac: u32, base: i64) -> Option { i64::try_from(((sym as i128) << frac) + base as i128).ok() } +/// The quantised CDF an encoder writes for a histogram, integer only: one table of +/// `hist.len() + 2` entries over the `hist.len()` symbols and the escape, the rANS +/// decoder's layout (`extensions/integer-payloads.md` §2; the escape is the last +/// symbol, `cdf_len − 2`). Every symbol, the escape included, gets frequency at least +/// 1, and the frequencies sum to exactly `2^precision`: the escape gets 1, each +/// symbol `1 + ⌊R·h_i / H⌋` with `R = 2^precision − hist.len() − 1` and `H` the +/// histogram's total, and the `R − Σ⌊·⌋` left over go one each to the symbols with +/// the largest remainders `R·h_i mod H`, ties to the lower index. An all-zero +/// histogram counts every symbol once. So the table is strictly increasing, from 0 to +/// `2^precision`, and the same histogram gives the same table on every machine. +/// +/// `None` when the histogram is empty, when `precision` is outside `1..=16` (the +/// decoder's precision is 16), or when the symbols and the escape cannot each have +/// frequency 1 (`hist.len() + 1 > 2^precision`). +pub fn hist_to_cdf(hist: &[u64], precision: u32) -> Option> { + if hist.is_empty() || !(1..=16).contains(&precision) { + return None; + } + let total: u128 = 1u128 << precision; + let n = hist.len() as u128; + if n + 1 > total { + return None; + } + let r = total - n - 1; + let all_zero = hist.iter().all(|&h| h == 0); + let weight = |h: u64| -> u128 { if all_zero { 1 } else { h as u128 } }; + let h_total: u128 = hist.iter().map(|&h| weight(h)).sum(); + let mut freq: Vec = hist.iter().map(|&h| 1 + r * weight(h) / h_total).collect(); + let given: u128 = freq.iter().map(|f| f - 1).sum(); + let mut order: Vec = (0..hist.len()).collect(); + // Largest remainder first; equal remainders to the lower index (a stable sort). + order.sort_by_key(|&i| std::cmp::Reverse(r * weight(hist[i]) % h_total)); + for &i in order.iter().take((r - given) as usize) { + freq[i] += 1; + } + freq.push(1); // the escape + let mut cdf = Vec::with_capacity(freq.len() + 1); + let mut acc: u128 = 0; + cdf.push(0i32); + for f in freq { + acc += f; + cdf.push(acc as i32); + } + debug_assert_eq!(acc, total); + Some(cdf) +} + // ───────────────────────────────────────────────────────────────────── // Kani proof harnesses — the soundness hinge, checked rather than asserted // ───────────────────────────────────────────────────────────────────── @@ -212,6 +259,57 @@ mod tests { const FRAC: u32 = 16; // Q16: grid step ~1.5e-5 + /// `hist_to_cdf`'s table: total exactly 2^16, every frequency at least 1, + /// strictly increasing, the escape last with frequency 1, the same table for the + /// same histogram, and frequencies ordered as the counts are. Includes histograms + /// far larger than the total, all-zero and one-symbol histograms, and a ladder + /// where the remainders tie. + #[test] + fn hist_to_cdf_properties() { + let cases: Vec> = vec![ + vec![0], + vec![5], + vec![0, 0, 0], + vec![1, 1, 1, 1, 1, 1, 1], + vec![1_000_000_000_000, 1, 0, 3, u64::MAX / 4], + (0..4095u64).map(|i| (i * 2654435761) % 1000).collect(), + (0..300u64).map(|i| if i == 150 { 1 << 40 } else { 0 }).collect(), + ]; + for hist in &cases { + let cdf = hist_to_cdf(hist, 16).expect("a table"); + assert_eq!(cdf.len(), hist.len() + 2); + assert_eq!(cdf[0], 0); + assert_eq!(*cdf.last().unwrap(), 1 << 16, "the total is exactly 2^16"); + assert!(cdf.windows(2).all(|w| w[1] > w[0]), "every frequency is at least 1"); + assert_eq!(cdf[cdf.len() - 1] - cdf[cdf.len() - 2], 1, "the escape is the last bin, frequency 1"); + assert_eq!(hist_to_cdf(hist, 16).unwrap(), cdf, "deterministic"); + let f: Vec = cdf.windows(2).map(|w| w[1] - w[0]).collect(); + for i in 0..hist.len() { + for j in 0..hist.len() { + if hist[i] > hist[j] { + assert!(f[i] >= f[j], "a larger count never gets a smaller frequency"); + } + } + } + } + // Equal counts: the leftover goes to the lower indices. + let cdf = hist_to_cdf(&[1, 1, 1], 16).unwrap(); + let f: Vec = cdf.windows(2).map(|w| w[1] - w[0]).collect(); + assert_eq!(f, vec![21845, 21845, 21845, 1]); + assert_eq!(hist_to_cdf(&[1, 1, 1, 1], 16).unwrap()[1..].iter().zip(&cdf_ladder()).filter(|(a, b)| a != b).count(), 0); + // Refusals: empty, a precision the decoder does not use, too many symbols. + assert!(hist_to_cdf(&[], 16).is_none()); + assert!(hist_to_cdf(&[1], 0).is_none()); + assert!(hist_to_cdf(&[1], 17).is_none()); + assert!(hist_to_cdf(&vec![1; 65536], 16).is_none()); + assert!(hist_to_cdf(&vec![1; 65535], 16).is_some()); + } + + /// Four equal counts: R = 65531, 16382 each with 3 left over, to indices 0, 1, 2. + fn cdf_ladder() -> Vec { + vec![16384, 32768, 49152, 65535, 65536] + } + #[test] fn integer_bucketing_matches_float_in_bucket_interiors() { let st = scale_table(); diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 5782eddfd..aa96d6beb 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -299,6 +299,17 @@ pub mod c2pa_emit; #[cfg(feature = "jwp_hash")] pub mod jwp_hash; +// SPEC §7.1: a WAI2 component exploded into its WAI1 envelope, WAI1 envelopes packed +// into a WAI2, and the check of an entry's envelope_manifest. Content hashes need BLAKE3. +#[cfg(feature = "jwp_hash")] +mod container_explode; + +// Staged delivery (extensions/staged-delivery.md): a base first, then refinements as +// their own objects, bound to what they refine by content hash; the residual payload +// WIR1, its predictors, the per-unit application algorithm and the live schedule. +#[cfg(feature = "staged")] +pub mod staged; + // Typed receipt classes (extensions/jwp-receipts.md §2.4.3–§2.4.7, §2.6, §3.1–§3.2): // energy, lawful-basis, log-inclusion, soft-binding and binding-recovery statements // about delivered objects, on one `claim` envelope and one signature rule beside the diff --git a/wai/wai-rs/src/rans.rs b/wai/wai-rs/src/rans.rs index 5303e1f16..0af9a457f 100644 --- a/wai/wai-rs/src/rans.rs +++ b/wai/wai-rs/src/rans.rs @@ -296,44 +296,84 @@ fn kani_decoder_new_is_total() { } } -/// rANS **encoder** matching [`RansDecoder`] byte-for-byte (rans64, precision 16): -/// the missing half for an entropy-coded integer codec. Symbols are buffered in -/// forward order and emitted in reverse on [`flush`](Self::flush) — the LIFO -/// rANS discipline. Encode + decode are pure integer, so the round-trip is +/// rANS **encoder** matching [`RansDecoder`] byte-for-byte (rans64, precision 16, +/// bypass 4): the encoder half of the integer codecs' entropy layer. Symbols are +/// buffered in forward order and emitted in reverse on [`flush`](Self::flush), the +/// LIFO rANS discipline. Encode and decode are pure integer, so the round trip is /// lossless and deterministic on every machine. /// -/// This core covers the **in-range** path (the symbol's value lands inside the -/// CDF's `[0, cdf_len-2)` span); the decoder's out-of-range *bypass* escape is -/// not produced here, so callers keep latents within range (the common case for -/// a clamped Gaussian-conditional codec). Out-of-range encoding is a follow-on. -// The decode side is wired into every neural codec; the encode side is the -// entropy half of the not-yet-assembled conditional integer codec (and the -// round-trip tests), so it is unused in the library build for now. -#[allow(dead_code)] +/// A value whose symbol lies outside the table's span (`value − offset` below 0 or +/// at least `cdf_len − 2`) is coded through the **bypass escape**, exactly as the +/// decoder reads it (`extensions/integer-payloads.md` §2): the escape symbol +/// `cdf_len − 2`; then the count of 4-bit groups, as a run of 15s closed by a group +/// below 15; then the groups of the raw value, least significant first. The raw value +/// is `2·(v − max)` for `v ≥ max` and `2·(−v − 1) + 1` for `v < 0`, where +/// `v = value − offset` and `max = cdf_len − 2`. Each group is a symbol of uniform +/// frequency at precision 4. The encoder's state starts at `2^31` and it writes the +/// words it flushes and no others, so every stream it writes ends canonically +/// ([`RansDecoder::is_canonical_end`]). #[derive(Default)] pub struct RansEncoder { - syms: Vec<(u32, u32)>, // (start, freq), forward order + syms: Vec, // forward order +} + +/// One buffered symbol: a table symbol `(start, freq)` at precision 16, or a 4-bit +/// bypass group. +#[derive(Clone, Copy)] +enum Sym { + Table { start: u32, freq: u32 }, + Bits(u32), } -#[allow(dead_code)] impl RansEncoder { pub fn new() -> Self { Self::default() } - /// Buffer one symbol: `value` against `cdf[0..cdf_len]` with `offset`, - /// exactly the arguments [`RansDecoder::decode`] consumes. Returns `false` - /// if the symbol is out of range (bypass not produced) — the caller must - /// keep `value - offset` in `[0, cdf_len-2)`. + /// Buffer one symbol: `value` against `cdf[0..cdf_len]` with `offset`, exactly + /// the arguments [`RansDecoder::decode`] consumes, so the decoder returns `value`. + /// A symbol outside the table's span is coded through the bypass escape. Returns + /// `false`, buffering nothing, only where no stream can code `value`: a table + /// shorter than 3 entries or longer than `cdf`, a frequency that is not positive, + /// or `value − offset` outside `i32` (the decoder forms the escaped value in + /// `i32`). pub fn encode(&mut self, value: i32, cdf: &[i32], cdf_len: usize, offset: i32) -> bool { - let max_value = cdf_len as i32 - 2; - let v = value - offset; - if v < 0 || v >= max_value { + if cdf_len < 3 || cdf_len > cdf.len() { return false; } - let start = cdf[v as usize] as u32; - let freq = (cdf[v as usize + 1] - cdf[v as usize]) as u32; - self.syms.push((start, freq)); + let max_value = cdf_len as i64 - 2; + let Ok(v) = i32::try_from(value as i64 - offset as i64) else { + return false; + }; + let v = v as i64; + let (sym, raw) = if v < 0 { + (max_value, Some((-2 * v - 1) as u64)) + } else if v >= max_value { + (max_value, Some(2 * (v - max_value) as u64)) + } else { + (v, None) + }; + let (lo, hi) = (cdf[sym as usize], cdf[sym as usize + 1]); + if lo < 0 || hi <= lo || hi > (1 << PRECISION) { + return false; + } + self.syms.push(Sym::Table { start: lo as u32, freq: (hi - lo) as u32 }); + if let Some(raw) = raw { + // The raw value is at most 2^32 − 1, so at most eight groups. + let mut n_bypass = 0u32; + while (raw >> (n_bypass * BYPASS_PRECISION)) != 0 { + n_bypass += 1; + } + let mut count = n_bypass; + while count >= MAX_BYPASS_VAL { + self.syms.push(Sym::Bits(MAX_BYPASS_VAL)); + count -= MAX_BYPASS_VAL; + } + self.syms.push(Sym::Bits(count)); + for j in 0..n_bypass { + self.syms.push(Sym::Bits(((raw >> (j * BYPASS_PRECISION)) & MAX_BYPASS_VAL as u64) as u32)); + } + } true } @@ -342,14 +382,30 @@ impl RansEncoder { pub fn flush(self) -> Vec { let mut state: u64 = RANS64_L; let mut renorm: Vec = Vec::new(); - for &(start, freq) in self.syms.iter().rev() { - // renorm so the division stays in range: emit low 32 bits while too big. - let x_max = ((RANS64_L >> PRECISION) << 32) * freq as u64; - while state >= x_max { - renorm.push(state as u32); - state >>= 32; + for &s in self.syms.iter().rev() { + match s { + Sym::Table { start, freq } => { + // renorm so the division stays in range: emit low 32 bits while too big. + let x_max = ((RANS64_L >> PRECISION) << 32) * freq as u64; + while state >= x_max { + renorm.push(state as u32); + state >>= 32; + } + state = ((state / freq as u64) << PRECISION) + (state % freq as u64) + start as u64; + } + Sym::Bits(val) => { + // A group of BYPASS_PRECISION bits is a symbol of frequency + // 2^(PRECISION − BYPASS_PRECISION): the state takes the bits + // directly, the inverse of `RansDecoder::get_bits`. + let freq = 1u64 << (PRECISION - BYPASS_PRECISION); + let x_max = ((RANS64_L >> PRECISION) << 32) * freq; + if state >= x_max { + renorm.push(state as u32); + state >>= 32; + } + state = (state << BYPASS_PRECISION) | val as u64; + } } - state = ((state / freq as u64) << PRECISION) + (state % freq as u64) + start as u64; } renorm.reverse(); // decoder consumes these forward let mut words = vec![state as u32, (state >> 32) as u32]; // flush: lo, hi (decoder seeds hi<<32|lo) @@ -401,6 +457,55 @@ mod tests { assert_eq!(out, syms, "rANS round-trip must be lossless"); } + /// The bypass escape round-trips every `i32` the decoder can return, against + /// tables of several lengths and offsets, the boundaries `±2^31` included, and + /// every stream ends canonically. A value no stream can code (`value − offset` + /// outside `i32`) is refused and buffers nothing. + #[test] + fn encoder_bypass_roundtrip() { + let mut x: u64 = 0x9e37_79b9_7f4a_7c15; + let mut next = || { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x + }; + for (n, offset) in [(1usize, 0i32), (4, -2), (16, -8), (255, 7)] { + let cdf = gauss_cdf(2.0, n); + let cdf_len = cdf.len(); + let mut vals: Vec = vec![i32::MIN, i32::MAX, i32::MIN + 1, i32::MAX - 1, -1, 0, 1, offset, offset - 1, + offset + n as i32, offset + n as i32 - 1, offset + n as i32 + 1]; + for _ in 0..400 { + let r = next(); + vals.push(match r % 4 { + 0 => r as i32, + 1 => ((r >> 8) % (n as u64 + 4)) as i32 + offset - 2, + 2 => ((r >> 8) as i32) >> ((r >> 40) % 31), + _ => offset + ((r >> 8) % n as u64) as i32, + }); + } + let mut enc = RansEncoder::new(); + let mut coded = Vec::new(); + for &v in &vals { + let fits = i32::try_from(v as i64 - offset as i64).is_ok(); + assert_eq!(enc.encode(v, &cdf, cdf_len, offset), fits, "value {v} offset {offset}"); + if fits { + coded.push(v); + } + } + let bits = enc.flush(); + let mut dec = RansDecoder::new(&bits).unwrap(); + let out: Vec = (0..coded.len()).map(|_| dec.decode(&cdf, cdf_len, offset)).collect(); + assert_eq!(out, coded, "bypass round-trip (n={n}, offset={offset})"); + assert!(dec.is_canonical_end(), "the encoder's stream ends canonically (n={n})"); + } + // A table too short to have an escape, or a length past the slice, codes nothing. + let mut enc = RansEncoder::new(); + assert!(!enc.encode(0, &[0, 65536], 2, 0)); + assert!(!enc.encode(0, &[0, 1, 65536], 4, 0)); + assert_eq!(enc.flush(), RansEncoder::new().flush()); + } + #[test] fn conditional_integer_entropy_round_trips_via_int_entropy() { // The codec's entropy LAYER end-to-end, pure integer: each latent's CDF diff --git a/wai/wai-rs/src/staged/mod.rs b/wai/wai-rs/src/staged/mod.rs new file mode 100644 index 000000000..d91fc1857 --- /dev/null +++ b/wai/wai-rs/src/staged/mod.rs @@ -0,0 +1,1287 @@ +//! Staged delivery (`extensions/staged-delivery.md`): a base first, refinements +//! after, each its own object bound to what it refines by content hash. +//! +//! [`StageSink`] runs the extension's §4.1 for every layer a sink receives: it holds +//! a layer whose base it has not decoded, and releases it when the base arrives. +//! [`StageChain`] holds one base's units (their stage, producer, tier and current +//! output) and applies layers to them per unit. Every outcome is a reason from the +//! closed list of §4.2 ([`StageRefusal`]), reported per envelope and per unit +//! ([`OfferReport`]). The residual form's payload is [`wir1`]; its predictors are +//! [`predict`]; tiers compose by [`tier`]; the live schedule is [`schedule`]. + +pub mod predict; +pub mod schedule; +pub mod tier; +pub mod wir1; +pub mod wir1_arith; + +#[cfg(test)] +mod tests; + +use std::collections::BTreeMap; + +use crate::codecs::{admits, is_refinement_only, DeterminismTier, CAP_AUDIO_INT_REFINE, CAP_IMAGE_INT_REFINE, CAP_SPLAT_INT_REFINE, CAP_VIDEO_INT_REFINE}; +use crate::container::{Prev, RefForm, Refines, Wai}; +use wir1::{Below, Next, Wir1Cost, Wir1Refusal}; + +/// The unit kind of a residual refinement capability: what its `WIR1` payload's units +/// hold (`extensions/staged-delivery.md` §6.1). `None` for any other string. +pub fn unit_kind(cap: &str) -> Option { + match cap { + CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE => Some(wir1::UnitKind::Rgb8), + CAP_AUDIO_INT_REFINE => Some(wir1::UnitKind::Pcm), + CAP_SPLAT_INT_REFINE => Some(wir1::UnitKind::Attr), + _ => None, + } +} + +/// A base's units from its integer decode (§2): every frame of a clip, the whole +/// picture, the whole clip of PCM (samples, 1, channels), the whole attribute tensor. +pub fn units_from_decoded(d: crate::int_payload::IntDecoded) -> Option> { + use crate::int_payload::IntDecoded; + Some(match d { + IntDecoded::Video { frames, width, height } => frames.into_iter().map(|f| UnitBuf { shape: (height, width, 3), samples: Samples::U8(f) }).collect(), + IntDecoded::Image { rgb, width, height } => vec![UnitBuf { shape: (height, width, 3), samples: Samples::U8(rgb) }], + IntDecoded::Pcm { samples, channels, .. } => { + let n = (samples.len() as u32).checked_div(channels)?; + vec![UnitBuf { shape: (n, 1, channels), samples: Samples::I16(samples) }] + } + IntDecoded::Attr { bytes, n_attr, s, .. } => vec![UnitBuf { shape: (n_attr, s, s), samples: Samples::U8(bytes) }], + }) +} + +/// Why a layer, or a layer for one unit, was not applied: the closed list of +/// staged-delivery §4.2, in the order of §4.1's steps. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[non_exhaustive] +/// cbindgen:ignore +pub enum StageRefusal { + RefinesMalformed, + CapabilityNotRefinement, + BaseAbsent, + BaseViaFallback, + BaseNotAdmitted, + RefinementUnattested, + PayloadMalformed, + CostOverLimit, + ChainOverLimit, + LayerConflict, + PrevMismatch, + LayerGap, + GeometryMismatch, + DeadlineMissed, +} + +impl StageRefusal { + /// Every reason, in §4.2's order. + /// + /// cbindgen:ignore + pub const ALL: [StageRefusal; 14] = [ + Self::RefinesMalformed, Self::CapabilityNotRefinement, Self::BaseAbsent, Self::BaseViaFallback, + Self::BaseNotAdmitted, Self::RefinementUnattested, Self::PayloadMalformed, Self::CostOverLimit, + Self::ChainOverLimit, Self::LayerConflict, Self::PrevMismatch, Self::LayerGap, Self::GeometryMismatch, Self::DeadlineMissed, + ]; + + /// The token §4.2 names. + pub fn code(self) -> &'static str { + match self { + Self::RefinesMalformed => "refines-malformed", + Self::CapabilityNotRefinement => "capability-not-refinement", + Self::BaseAbsent => "base-absent", + Self::BaseViaFallback => "base-via-fallback", + Self::BaseNotAdmitted => "base-not-admitted", + Self::RefinementUnattested => "refinement-unattested", + Self::PayloadMalformed => "payload-malformed", + Self::CostOverLimit => "cost-over-limit", + Self::ChainOverLimit => "chain-over-limit", + Self::PrevMismatch => "prev-mismatch", + Self::LayerGap => "layer-gap", + Self::LayerConflict => "layer-conflict", + Self::GeometryMismatch => "geometry-mismatch", + Self::DeadlineMissed => "deadline-missed", + } + } + + /// The reason whose token is `code`. + pub fn from_code(code: &str) -> Option { + Self::ALL.into_iter().find(|r| r.code() == code) + } + + /// Whether the reason decides for the whole envelope (§4.1 steps 1–6) rather than + /// for one unit. + pub fn is_envelope_scoped(self) -> bool { + self < Self::LayerConflict + } +} + +/// What the sink verified of a layer's delivery receipt (§10): through acceptance over +/// the layer's envelope bytes, by inclusion alone (without the bytes), or nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReceiptEvidence { + DeliveredVerified, + InclusionVerified, + None, +} + +impl ReceiptEvidence { + /// `delivered-verified`, `inclusion-verified` or `none`. + pub fn token(self) -> &'static str { + match self { + Self::DeliveredVerified => "delivered-verified", + Self::InclusionVerified => "inclusion-verified", + Self::None => "none", + } + } + /// The evidence a token names. + pub fn from_token(s: &str) -> Option { + match s { + "delivered-verified" => Some(Self::DeliveredVerified), + "inclusion-verified" => Some(Self::InclusionVerified), + "none" => Some(Self::None), + _ => None, + } + } +} + +/// A unit's samples, in the element type of its kind. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Samples { + U8(Vec), + I16(Vec), +} + +impl Samples { + /// The canonical buffer of SPEC §7 *Digests*: the bytes for u8; for i16, each + /// sample as two little-endian bytes in order. + pub fn canonical_bytes(&self) -> Vec { + match self { + Samples::U8(v) => v.clone(), + Samples::I16(v) => v.iter().flat_map(|s| s.to_le_bytes()).collect(), + } + } + fn below(&self) -> Below<'_> { + match self { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + } + } + fn len(&self) -> usize { + match self { + Samples::U8(v) => v.len(), + Samples::I16(v) => v.len(), + } + } + /// The bytes the samples hold: 1 per u8 element, 2 per i16 sample. + pub fn byte_len(&self) -> u64 { + match self { + Samples::U8(v) => v.len() as u64, + Samples::I16(v) => 2 * v.len() as u64, + } + } +} + +/// One unit of a base's output as decoded: its shape `(d0, d1, d2)` (rows, columns +/// and channels of a picture; samples, 1 and channels of audio; planes and the planes' +/// sides of a splat attribute tensor) and its samples. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitBuf { + pub shape: (u32, u32, u32), + pub samples: Samples, +} + +/// A unit's state (§1): its stage, its producer, whether it is frozen (§4.1 step 7), +/// its tier (§5) and its current output. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitState { + pub stage: u8, + pub producer: [u8; 32], + pub frozen: bool, + pub tier: Option, + pub buf: UnitBuf, + /// The producers of the stages the unit's record holds, in order: the envelopes + /// that passed step 8 for it and were applied, which step 7 compares a later layer + /// with. As long as `stage` at least; longer while a base decoded again (§4.4) + /// replays its record. + producers: Vec<[u8; 32]>, +} + +impl UnitState { + /// The producers of the stages the unit's record holds, in order (§4.1 step 7, + /// §4.4): stages 1 to `stage`, and past it while the unit replays its record. + pub fn producers(&self) -> &[[u8; 32]] { + &self.producers + } +} + +/// What a sink keeps of a unit when it drops its base's chain (§4.4): the producers of +/// the stages it reached, the envelope that passed step 8 at its last stage without +/// being applied, and whether it is frozen. Counted as 8 bytes (its index and flag), +/// 32 for each recorded stage and 32 for a producer that passed; an empty record (no +/// stage, nothing passed, not frozen) is not kept. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnitRecord { + pub producers: Vec<[u8; 32]>, + pub passed: Option<[u8; 32]>, + pub frozen: bool, +} + +impl UnitRecord { + /// Whether the record holds nothing a re-decoded base would differ by. + pub fn is_empty(&self) -> bool { + self.producers.is_empty() && self.passed.is_none() && !self.frozen + } + + /// The bytes §4.4 counts for it. + pub fn counted_bytes(&self) -> u64 { + 8 + 32 * self.producers.len() as u64 + if self.passed.is_some() { 32 } else { 0 } + } +} + +/// How long and how much a sink holds (§4.3). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct HoldBound { + /// The most envelopes held at once. + pub max_envelopes: usize, + /// How long an envelope is held from its arrival. + pub max_age_ms: u64, +} + +/// A live schedule (§8): the units' clock, and the lead of each layer (`lead_ms[l]` +/// for layer `l + 1`; a layer past the list has lead 0). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Live { + pub clock: schedule::UnitClock, + pub lead_ms: Vec, +} + +/// What a sink applies to a base's layers besides §4.1: its hold bound, the cost limit +/// it applies to a layer's declared cost (Appendix A.5), its chain budget (§4.5), and +/// its live schedule. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ChainConfig { + pub bound: HoldBound, + pub cost_limit: Option, + /// The most bytes a chain's state reaches (§4.5): a layer that would take it past + /// this is refused before any stream is decoded (`chain-over-limit`). + pub budget: u64, + /// The most bytes a sink keeps of the bases it does not hold (§4.4): their unit + /// records and their attestation ([`StageSink::kept_bytes`]). Past it, the bases + /// dropped or marked longest ago are forgotten, whole. The budget is in §4.4's + /// counted bytes, which every sink counts alike; this implementation allocates at + /// most four bytes for each counted byte (`tests/staged_resources.rs` checks it). + pub record_budget: u64, + pub live: Option, +} + +/// The outcome of offering, releasing or expiring one envelope: the reason for the +/// envelope, or per unit what happened. A refusal of the envelope names no unit. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OfferReport { + pub content_hash: [u8; 32], + /// An envelope-scoped reason (§4.1 steps 1–6), or `None`. + pub envelope: Option, + /// For `payload-malformed` and `cost-over-limit`, the `WIR1` code or the figure + /// (Appendix A.6); `None` otherwise. + pub detail: Option<&'static str>, + /// The envelope is held for its base (`base-absent` until the bound). + pub held_for_base: bool, + /// Units it was applied to, ascending. + pub applied: Vec, + /// Units it is held for (`layer-gap` at the bound), ascending. + pub held: Vec, + /// Units it was not applied to, with the reason, ascending by unit. + pub refused: Vec<(u32, StageRefusal)>, +} + +impl OfferReport { + fn new(content_hash: [u8; 32]) -> Self { + OfferReport { content_hash, envelope: None, detail: None, held_for_base: false, applied: vec![], held: vec![], refused: vec![] } + } + fn refuse(content_hash: [u8; 32], why: StageRefusal, detail: Option<&'static str>) -> Self { + OfferReport { envelope: Some(why), detail, ..Self::new(content_hash) } + } + /// Units in order; a unit held and then decided within the same step is reported + /// by its decision alone. + /// Linear in the units up to a log factor: membership is a binary search in the + /// sorted lists. + fn sort(&mut self) { + self.applied.sort_unstable(); + self.refused.sort_unstable(); + let (applied, refused) = (&self.applied, &self.refused); + self.held.retain(|u| applied.binary_search(u).is_err() && refused.binary_search_by_key(u, |&(x, _)| x).is_err()); + self.held.sort_unstable(); + self.held.dedup(); + } +} + +/// Why an envelope could not be offered at all: it is not a `WAI1` this sink reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OfferError { + /// The bytes are not a `WAI1` envelope. + NotWai1(String), + /// The manifest declares an encryption this sink cannot open (SPEC §6.4); a layer + /// is decrypted before §4.1 step 6 (§14). + Encrypted, +} + +impl std::fmt::Display for OfferError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OfferError::NotWai1(e) => write!(f, "not a WAI1 envelope: {e}"), + OfferError::Encrypted => f.write_str("an encrypted envelope this sink cannot open"), + } + } +} + +impl std::error::Error for OfferError {} + +/// The producers `prev` gives the units of a layer's span: each run's producer and the +/// span offset it ends at, so one unit's producer is found by bisection. +#[derive(Debug, Clone)] +struct Producers { + producers: Vec<[u8; 32]>, + ends: Vec, +} + +impl Producers { + /// From a `refines` whose runs sum to `count` (checked at step 2). + fn new(r: &Refines, count: u32) -> Option { + match &r.prev { + Prev::One(p) => Some(Producers { producers: vec![unhex32(p)?], ends: vec![count] }), + Prev::Runs(runs) => { + let mut producers = Vec::with_capacity(runs.len()); + let mut ends = Vec::with_capacity(runs.len()); + let mut at = 0u32; + for run in runs { + producers.push(unhex32(&run.producer)?); + at = at.checked_add(run.count)?; + ends.push(at); + } + Some(Producers { producers, ends }) + } + } + } + + /// The producer of the `i`-th unit of the span. + fn at(&self, i: u32) -> Option<&[u8; 32]> { + self.producers.get(self.ends.partition_point(|&e| e <= i)) + } +} + +fn unhex32(s: &str) -> Option<[u8; 32]> { + let b = s.as_bytes(); + if b.len() != 64 { + return None; + } + let nib = |c: u8| match c { + b'0'..=b'9' => Some(c - b'0'), + b'a'..=b'f' => Some(c - b'a' + 10), + _ => None, + }; + let mut out = [0u8; 32]; + for (i, o) in out.iter_mut().enumerate() { + *o = (nib(b[2 * i])? << 4) | nib(b[2 * i + 1])?; + } + Some(out) +} + +/// A layer the chain retains (§4.5): it passed §4.1 steps 1–6, and is being evaluated +/// or has units held for it. Its payload is parsed once, when it passes step 6. +#[derive(Debug, Clone)] +struct Layer { + hash: [u8; 32], + payload: Vec, + wir: wir1::Wir1, + layer: u8, + from: u32, + count: u32, + prev: Producers, + /// When it arrived: its hold runs from here (§4.3). + arrival_ms: u64, + /// When it counts as arrived for a live deadline (§8): its arrival, or, over a base + /// whose receipt the sink verified, when the sink verified the layer's (§10). + ready_ms: u64, + /// How many units are held for it. + held: u32, + /// What it adds to the chain's state while retained (§4.5). + reserve: u64, + /// What one covered unit's application releases from `reserve`: its output and its + /// stage's 32 bytes. + share: u64, +} + +/// The base's identity, how it was decoded (§4.1 steps 2–4), and where its units sit +/// on a live clock (§8). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaseInfo { + /// The base envelope's content hash. + pub content_hash: [u8; 32], + /// The capability the base was decoded through. + pub capability: String, + /// Whether that capability was the manifest's fallback. + pub via_fallback: bool, + /// The base step's tier (§5, [`tier::base_tier`]). + pub tier: Option, + /// Whether the sink verified a delivery receipt for the base (§10). + pub attested: bool, + /// The position of the base's unit 0 on the live clock (§8): 0 for a base that + /// starts the schedule; `k` for a base whose units are the schedule's units + /// `k, k + 1, …` (a group of frames after the first). + pub first_unit: u64, +} + +/// One base and its units (§1), and the layers it retains for them (§4.5). +#[derive(Debug, Clone)] +pub struct StageChain { + base: BaseInfo, + cfg: ChainConfig, + units: Vec, + /// Per unit, the envelope that passed step 8 for it at its current stage and was + /// not applied (step 9 or 10 refused it). + passed: BTreeMap, + /// The layers retained, by the order they passed step 6. + layers: BTreeMap, + /// The same layers by content hash. + ids: BTreeMap<[u8; 32], u64>, + /// Per unit, the layers held for it (§4.1 step 8). + held: BTreeMap>, + /// How many layers have a unit held for them. + held_layers: usize, + /// The chain's state (§4.5), in bytes. + state: u64, + seq: u64, +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +impl StageChain { + /// A chain over a decoded base: `units` are the base's units in order (§2), each + /// at stage 0 with the base as its producer and the base step's tier. + pub fn new(base: BaseInfo, units: Vec, cfg: ChainConfig) -> Self { + Self::with_records(base, units, cfg, vec![]) + } + + /// A chain over a base decoded again after its chain was dropped (§4.4): each unit + /// at stage 0 with its record, so step 7 compares a layer with the producers the + /// unit had, and a frozen unit stays frozen. A unit with no record starts afresh. + /// `records` gives, by unit index, the units whose record is not empty. + pub fn with_records(base: BaseInfo, units: Vec, cfg: ChainConfig, records: Vec<(u32, UnitRecord)>) -> Self { + let mut records: BTreeMap = records.into_iter().collect(); + let mut passed = BTreeMap::new(); + let mut state = 0u64; + let units: Vec = units + .into_iter() + .enumerate() + .map(|(u, buf)| { + let r = records.remove(&(u as u32)).unwrap_or(UnitRecord { producers: vec![], passed: None, frozen: false }); + if let Some(h) = r.passed { + passed.insert(u as u32, h); + } + state = state.saturating_add(buf.samples.byte_len()).saturating_add(32 * r.producers.len() as u64); + UnitState { stage: 0, producer: base.content_hash, frozen: r.frozen, tier: base.tier, buf, producers: r.producers } + }) + .collect(); + StageChain { + base, + cfg, + units, + passed, + layers: BTreeMap::new(), + ids: BTreeMap::new(), + held: BTreeMap::new(), + held_layers: 0, + state, + seq: 0, + } + } + + /// The record of every unit whose record is not empty, by unit index (§4.4): what + /// [`StageSink::remove_base`] keeps. + pub fn records(&self) -> Vec<(u32, UnitRecord)> { + self.units + .iter() + .enumerate() + .map(|(u, s)| (u as u32, UnitRecord { producers: s.producers.clone(), passed: self.passed.get(&(u as u32)).copied(), frozen: s.frozen })) + .filter(|(_, r)| !r.is_empty()) + .collect() + } + + /// The base this chain refines. + pub fn base(&self) -> &BaseInfo { + &self.base + } + + /// Record that the sink verified a delivery receipt for the base (§10): from now + /// on step 5 applies to every envelope that arrives. A verified receipt is never + /// unverified, so there is no converse. + pub fn mark_attested(&mut self) { + self.base.attested = true; + } + + /// The number of units. + pub fn n_units(&self) -> u32 { + self.units.len() as u32 + } + + /// Unit `u`'s state. + pub fn unit(&self, u: u32) -> Option<&UnitState> { + self.units.get(u as usize) + } + + /// Every unit's state, in order. + pub fn units(&self) -> &[UnitState] { + &self.units + } + + /// Unit `u`'s tier (§5). + pub fn tier(&self, u: u32) -> Option { + self.units.get(u as usize).and_then(|s| s.tier) + } + + /// Unit `u`'s stage hash, or `None` unless it holds `decode-equivalence` (§5). + pub fn unit_sha256(&self, u: u32) -> Option<[u8; 32]> { + let s = self.units.get(u as usize)?; + (s.tier == Some(DeterminismTier::DecodeEquivalence)).then(|| crate::pin::sha256(&s.buf.samples.canonical_bytes())) + } + + /// The stage hash of every unit, the SHA-256 of their stage hashes in order + /// (SPEC §7 *Digests*), or `None` unless every unit holds `decode-equivalence`. + pub fn clip_sha256(&self) -> Option<[u8; 32]> { + let mut cat = Vec::with_capacity(32 * self.units.len()); + for u in 0..self.n_units() { + cat.extend_from_slice(&self.unit_sha256(u)?); + } + Some(crate::pin::sha256(&cat)) + } + + /// The envelopes held, with the units each is held for, in the order the envelopes + /// passed step 6 and then by unit. + pub fn held(&self) -> Vec<([u8; 32], u32)> { + let mut v: Vec<(u64, u32)> = self.held.iter().flat_map(|(u, ids)| ids.iter().map(move |id| (*id, *u))).collect(); + v.sort_unstable(); + v.into_iter().map(|(id, u)| (self.layers[&id].hash, u)).collect() + } + + /// How many envelopes hold at least one unit. + pub fn held_envelopes(&self) -> usize { + self.held_layers + } + + /// The chain's state (§4.5): every unit's output bytes and 32 bytes per stage above + /// 0, and every retained layer's payload and declared working bytes, with one unit's + /// output and 32 bytes for each unit it covers and has not been applied to. + pub fn state_bytes(&self) -> u64 { + self.state + } + + /// §4.1 steps 2–6 for an envelope whose `refines` and capability passed step 1: + /// the envelope reason, or the layer, retained. + #[allow(clippy::too_many_arguments)] + fn admit(&mut self, hash: [u8; 32], cap: &str, refines: &Refines, payload: Vec, arrival_ms: u64, ready_ms: u64, evidence: ReceiptEvidence) -> Result)> { + use StageRefusal::*; + if refines.base != hex(&self.base.content_hash) { + return Err((BaseAbsent, None)); + } + refines.check_units(self.n_units()).map_err(|_| (RefinesMalformed, None))?; + let (from, count) = refines.units(self.n_units()); + let prev = Producers::new(refines, count).ok_or((RefinesMalformed, None))?; + if self.base.via_fallback { + return Err((BaseViaFallback, None)); + } + if !admits(cap, &self.base.capability) { + return Err((BaseNotAdmitted, None)); + } + if self.base.attested && evidence != ReceiptEvidence::DeliveredVerified { + return Err((RefinementUnattested, None)); + } + let kind = unit_kind(cap).ok_or((PayloadMalformed, Some("unsupported_capability")))?; + let wir = wir1::parse(&payload, kind).map_err(|e| (PayloadMalformed, Some(e.code())))?; + if wir.n_units != count { + return Err((PayloadMalformed, Some("field_range"))); + } + let c = wir.declared_cost(); + if let Some(limit) = self.cfg.cost_limit { + let over = if c.output_bytes > limit.output_bytes { + Some("output bytes") + } else if c.symbols > limit.symbols { + Some("symbols") + } else if c.working_bytes > limit.working_bytes { + Some("working bytes") + } else { + None + }; + if let Some(what) = over { + return Err((CostOverLimit, Some(what))); + } + } + // The stage each covered unit gains when the layer applies (32 bytes) is counted + // with its output, so what an application adds was counted at admission. + let reserve = (payload.len() as u64) + .saturating_add(c.output_bytes) + .saturating_add(c.working_bytes) + .saturating_add(32 * count as u64); + if self.state.saturating_add(reserve) > self.cfg.budget { + return Err((ChainOverLimit, None)); + } + wir.check_streams(&payload).map_err(|(_, e)| (PayloadMalformed, Some(e.code())))?; + self.seq += 1; + self.state += reserve; + self.ids.insert(hash, self.seq); + let share = (c.output_bytes / count as u64).saturating_add(32); + self.layers.insert(self.seq, Layer { hash, payload, wir, layer: refines.layer, from, count, prev, arrival_ms, ready_ms, held: 0, reserve, share }); + Ok(self.seq) + } + + /// The producer layer `id`'s `prev` gives for unit `u` (§4.1 step 7, *P(u)*). + fn p_of(&self, id: u64, u: u32) -> Option<[u8; 32]> { + let l = &self.layers[&id]; + l.prev.at(u.checked_sub(l.from)?).copied() + } + + /// §4.1 step 7 against what passed step 8 before: unit `u` is frozen, or an + /// envelope other than layer `id`, of the same layer and with the same producer for + /// `u`, passed step 8 for it. A unit's stage rises by one per application, so the + /// envelope that passed step 8 for it at layer `k` is the producer of its recorded + /// stage `k` (or, at the stage above its record, the envelope that passed and was + /// not applied), and the producer it named is that of stage `k − 1`. + fn conflicted_before(&self, id: u64, u: u32) -> bool { + let s = &self.units[u as usize]; + let l = &self.layers[&id]; + let p = self.p_of(id, u); + let (k, r) = (l.layer as usize, s.producers.len()); + let producer_of = |stage: usize| if stage == 0 { self.base.content_hash } else { s.producers[stage - 1] }; + if s.frozen { + // A frozen unit replays its own record, in order, and nothing else (§4.4): + // the recorded producer of the stage above its current one, never past it. + let replay = k == s.stage as usize + 1 && k <= r && s.producers[k - 1] == l.hash && p == Some(producer_of(k - 1)); + return !replay; + } + let Some(p) = p else { return false }; + if k <= r { + p == producer_of(k - 1) && s.producers[k - 1] != l.hash + } else if k == r + 1 { + self.passed.get(&u).is_some_and(|h| p == producer_of(r) && *h != l.hash) + } else { + false + } + } + + /// §4.1 step 8 for unit `u` of layer `id`: ready, held, or not ready. + fn readiness(&self, id: u64, u: u32) -> Readiness { + let l = &self.layers[&id]; + let Some(p) = self.p_of(id, u) else { return Readiness::Not }; + let s = &self.units[u as usize]; + let want = l.layer - 1; + if s.stage == want && s.producer == p { + Readiness::Ready + } else if s.stage < want { + Readiness::Held + } else { + Readiness::Not + } + } + + /// Hold unit `u` for layer `id`. + fn hold(&mut self, id: u64, u: u32) { + let l = self.layers.get_mut(&id).expect("retained"); + if l.held == 0 { + self.held_layers += 1; + } + l.held += 1; + // Kept in id order, so membership is a binary search. + let ids = self.held.entry(u).or_default(); + let at = ids.partition_point(|&x| x < id); + ids.insert(at, id); + } + + /// Release every layer held for unit `u`. + fn unhold_all(&mut self, u: u32) -> Vec { + let ids = self.held.remove(&u).unwrap_or_default(); + for id in &ids { + let l = self.layers.get_mut(id).expect("retained"); + l.held -= 1; + if l.held == 0 { + self.held_layers -= 1; + } + } + ids + } + + /// Whether unit `u` is held for layer `id`. + fn is_held(&self, id: u64, u: u32) -> bool { + self.layers[&id].held > 0 && self.held.get(&u).is_some_and(|ids| ids.binary_search(&id).is_ok()) + } + + /// Freeze unit `u`: no further layer applies to it, and every layer held for it is + /// refused for it (`layer-conflict`). + fn freeze(&mut self, u: u32, reports: &mut Reports) { + self.units[u as usize].frozen = true; + for id in self.unhold_all(u) { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } + } + + /// Evaluate layer `id`'s units on its arrival (steps 7 and 8), then run the batch + /// of its ready units and every batch that releases. + fn evaluate(&mut self, id: u64, outside_held: usize, reports: &mut Reports) { + let (hash, from, count) = { + let l = &self.layers[&id]; + (l.hash, l.from, l.count) + }; + let already = self.layers[&id].held > 0; + let mut ready = Vec::new(); + let mut newly_held = Vec::new(); + for u in from..from + count { + if self.conflicted_before(id, u) { + if self.is_held(id, u) { + // Frozen while held: freeze reports it below. + } else { + reports.get(id, hash).refused.push((u, StageRefusal::LayerConflict)); + } + self.freeze(u, reports); + continue; + } + match self.readiness(id, u) { + Readiness::Ready => ready.push((id, u)), + Readiness::Held => newly_held.push(u), + Readiness::Not => reports.get(id, hash).refused.push((u, StageRefusal::PrevMismatch)), + } + } + if !newly_held.is_empty() { + if !already && self.held_layers + outside_held >= self.cfg.bound.max_envelopes { + for u in newly_held { + reports.get(id, hash).refused.push((u, StageRefusal::LayerGap)); + } + } else { + for u in newly_held { + if !self.is_held(id, u) { + self.hold(id, u); + } + reports.get(id, hash).held.push(u); + } + } + } + self.run_batches(ready, reports); + } + + /// Run a batch (§4.4) and every batch it releases. + fn run_batches(&mut self, mut batch: Vec<(u64, u32)>, reports: &mut Reports) { + while !batch.is_empty() { + let advanced = self.run_batch(batch, reports); + batch = self.release(&advanced, reports); + } + } + + /// One batch: step 7 over every pair, grouped by key, then steps 9 and 10 and + /// application. Returns the units whose stage advanced. + fn run_batch(&mut self, mut batch: Vec<(u64, u32)>, reports: &mut Reports) -> Vec { + // A unit's ready pairs share its key (layer = stage + 1, P(u) = its producer), + // so grouping by unit groups by key. + batch.sort_unstable_by_key(|&(id, u)| (u, id)); + batch.dedup(); + let mut advanced = Vec::new(); + let mut i = 0; + while i < batch.len() { + let u = batch[i].1; + let j = i + batch[i..].iter().take_while(|p| p.1 == u).count(); + let group = &batch[i..j]; + let conflicted = group.len() > 1 || self.conflicted_before(group[0].0, u); + if conflicted { + self.freeze(u, reports); + for &(id, _) in group { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } + } else { + let id = group[0].0; + let h = self.layers[&id].hash; + // A pair at the stage above the unit's record is recorded as passing step + // 8; one replaying the record is that record (step 7 let it through). + let s = &self.units[u as usize]; + if s.stage as usize == s.producers.len() { + self.passed.insert(u, h); + } + match self.apply(id, u) { + Ok(()) => { + reports.get(id, h).applied.push(u); + advanced.push(u); + } + Err(why) => reports.get(id, h).refused.push((u, why)), + } + } + i = j; + } + advanced + } + + /// Steps 9 and 10 and application for one pair. + fn apply(&mut self, id: u64, u: u32) -> Result<(), StageRefusal> { + let l = &self.layers[&id]; + let kind = l.wir.kind; + let s = &self.units[u as usize]; + let below_ok = matches!((kind, &s.buf.samples), (wir1::UnitKind::Pcm, Samples::I16(_)) | (wir1::UnitKind::Rgb8 | wir1::UnitKind::Attr, Samples::U8(_))); + let shape = s.buf.shape; + // Checked: a shape whose element count passes u64 is no shape a unit holds. + let elements = (shape.0 as u64).checked_mul(shape.1 as u64).and_then(|v| v.checked_mul(shape.2 as u64)); + if !below_ok || elements != Some(s.buf.samples.len() as u64) || !l.wir.accepts_below(shape) { + return Err(StageRefusal::GeometryMismatch); + } + if let Some(live) = &self.cfg.live { + let lead = live.lead_ms.get(l.layer as usize - 1).copied().unwrap_or(0); + if !schedule::on_time(&live.clock, self.base.first_unit.saturating_add(u as u64), l.ready_ms, lead) { + return Err(StageRefusal::DeadlineMissed); + } + } + let next = wir1::apply_unit(&l.wir, &l.payload, u - l.from, s.buf.samples.below(), shape).map_err(|e| match e.refusal { + Wir1Refusal::GeometryMismatch => StageRefusal::GeometryMismatch, + _ => StageRefusal::PayloadMalformed, + })?; + let (layer, hash, shape) = (l.layer, l.hash, (l.wir.d0, l.wir.d1, l.wir.d2)); + let s = &mut self.units[u as usize]; + let old = s.buf.samples.byte_len(); + s.buf = UnitBuf { shape, samples: match next { Next::U8(v) => Samples::U8(v), Next::I16(v) => Samples::I16(v) } }; + s.stage = layer; + s.producer = hash; + // A new stage is recorded; a stage the record holds is replayed (step 7 let + // only its recorded producer through). + let recorded = (layer as usize) <= s.producers.len(); + if !recorded { + s.producers.push(hash); + } + s.tier = tier::compose(s.tier, &[Some(DeterminismTier::DecodeEquivalence)], &[true]); + let new = s.buf.samples.byte_len(); + // The unit's share of the layer's reserve (its output and its stage) is now held + // by the unit, so it leaves the reserve: an application never raises the state. + let l = self.layers.get_mut(&id).expect("retained"); + let share = l.share.min(l.reserve); + l.reserve -= share; + self.state = self.state.saturating_sub(old).saturating_add(new).saturating_add(if recorded { 0 } else { 32 }).saturating_sub(share); + if !recorded { + self.passed.remove(&u); + } + Ok(()) + } + + /// The pairs held for `advanced` units that a stage change decides: those now ready + /// form the next batch; those no longer waiting are refused (`prev-mismatch`). + fn release(&mut self, advanced: &[u32], reports: &mut Reports) -> Vec<(u64, u32)> { + let mut batch = Vec::new(); + for &u in advanced { + if !self.held.contains_key(&u) { + continue; + } + let mut keep = Vec::new(); + for id in self.unhold_all(u) { + let h = self.layers[&id].hash; + if self.conflicted_before(id, u) { + self.units[u as usize].frozen = true; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + continue; + } + match self.readiness(id, u) { + Readiness::Ready => batch.push((id, u)), + Readiness::Held => keep.push(id), + Readiness::Not => reports.get(id, h).refused.push((u, StageRefusal::PrevMismatch)), + } + } + for id in keep { + if self.units[u as usize].frozen { + let h = self.layers[&id].hash; + reports.get(id, h).refused.push((u, StageRefusal::LayerConflict)); + } else { + self.hold(id, u); + } + } + } + batch + } + + /// Drop every layer no unit is held for (§4.5): the end of an offer, a release or + /// an expiry. + fn prune(&mut self) { + let gone: Vec = self.layers.iter().filter(|(_, l)| l.held == 0).map(|(id, _)| *id).collect(); + for id in gone { + let l = self.layers.remove(&id).expect("listed"); + self.ids.remove(&l.hash); + self.state = self.state.saturating_sub(l.reserve); + } + } + + /// Offer a layer that passed step 1 to this chain (§4.1 steps 2–10): the envelope's + /// report first, then a report for every held layer the offer decided units of, in + /// the order those layers passed step 6. + fn offer_checked(&mut self, p: Pending, outside_held: usize) -> Vec { + let id = match self.ids.get(&p.hash) { + // The same envelope again, still retained: steps 2–6 passed and it adds + // nothing to the state; step 7 decides again. + Some(&id) => id, + None => match self.admit(p.hash, &p.cap, &p.refines, p.payload, p.arrival_ms, p.arrival_ms, p.evidence) { + Ok(id) => id, + Err((why, detail)) => return vec![OfferReport::refuse(p.hash, why, detail)], + }, + }; + let mut reports = Reports::default(); + reports.get(id, p.hash); + self.evaluate(id, outside_held, &mut reports); + let out = reports.finish(); + self.prune(); + out + } + + /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). An + /// envelope that names another base is refused `base-absent`: [`StageSink`] holds + /// such envelopes. + pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { + match step1(bytes, now_ms, evidence)? { + Ok(p) => Ok(self.offer_checked(p, 0)), + Err(r) => Ok(vec![r]), + } + } + + /// Release the layers held for this chain's base, together (§4.4): each passes + /// steps 2–8 alone, in the order they arrived, then their ready pairs run as one + /// batch. Reports are in the order the layers arrived. + fn release_pending(&mut self, items: Vec) -> Vec { + let mut reports = Reports::default(); + let mut refused = Vec::new(); + let mut ready = Vec::new(); + let order: Vec<[u8; 32]> = items.iter().map(|p| p.hash).collect(); + for p in items { + let ready_ms = if self.base.attested { p.verified_ms } else { p.arrival_ms }; + let id = match self.ids.get(&p.hash) { + Some(&id) => id, + None => match self.admit(p.hash, &p.cap, &p.refines, p.payload, p.arrival_ms, ready_ms, p.evidence) { + Ok(id) => id, + Err((why, detail)) => { + refused.push(OfferReport::refuse(p.hash, why, detail)); + continue; + } + }, + }; + reports.get(id, p.hash); + let (from, count) = { + let l = &self.layers[&id]; + (l.from, l.count) + }; + for u in from..from + count { + if self.conflicted_before(id, u) { + reports.get(id, p.hash).refused.push((u, StageRefusal::LayerConflict)); + self.freeze(u, &mut reports); + continue; + } + match self.readiness(id, u) { + Readiness::Ready => ready.push((id, u)), + Readiness::Held => { + if !self.is_held(id, u) { + self.hold(id, u); + } + reports.get(id, p.hash).held.push(u); + } + Readiness::Not => reports.get(id, p.hash).refused.push((u, StageRefusal::PrevMismatch)), + } + } + } + self.run_batches(ready, &mut reports); + let mut out = reports.finish(); + self.prune(); + out.extend(refused); + let pos = |h: &[u8; 32]| order.iter().position(|x| x == h).unwrap_or(usize::MAX); + out.sort_by_key(|r| pos(&r.content_hash)); + out + } + + /// Discard what has been held past the bound at `now_ms` (§4.3): each unit a + /// layer is held for, with `layer-gap`. + pub fn expire(&mut self, now_ms: u64) -> Vec { + let age = self.cfg.bound.max_age_ms; + let old: Vec = self.layers.iter().filter(|(_, l)| l.held > 0 && now_ms.saturating_sub(l.arrival_ms) > age).map(|(id, _)| *id).collect(); + let mut reports = Reports::default(); + for id in old { + let (hash, from, count) = { + let l = &self.layers[&id]; + (l.hash, l.from, l.count) + }; + for u in from..from + count { + let Some(ids) = self.held.get_mut(&u) else { continue }; + let Ok(at) = ids.binary_search(&id) else { continue }; + ids.remove(at); + if ids.is_empty() { + self.held.remove(&u); + } + reports.get(id, hash).refused.push((u, StageRefusal::LayerGap)); + } + self.layers.get_mut(&id).expect("retained").held = 0; + self.held_layers -= 1; + } + let out = reports.finish(); + self.prune(); + out + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Readiness { + Ready, + Held, + Not, +} + +/// Reports under construction, by layer (the order they passed step 6), with the +/// first one asked for first. +#[derive(Default)] +struct Reports { + first: Option, + by: BTreeMap, +} + +impl Reports { + fn get(&mut self, id: u64, hash: [u8; 32]) -> &mut OfferReport { + self.first.get_or_insert(id); + self.by.entry(id).or_insert_with(|| OfferReport::new(hash)) + } + /// The first layer's report, then the others in the order they passed step 6. + fn finish(mut self) -> Vec { + let mut out = Vec::new(); + if let Some(mut r) = self.first.and_then(|f| self.by.remove(&f)) { + r.sort(); + out.push(r); + } + for mut r in self.by.into_values() { + r.sort(); + out.push(r); + } + out + } +} + +/// §4.1 step 1: the envelope as a layer to offer, or its report. `Err` when the bytes +/// are not a `WAI1` this sink reads. +fn step1(bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { + let hash = crate::jwp_hash::content_hash(bytes); + let w = Wai::from_bytes(bytes).map_err(|e| OfferError::NotWai1(e.to_string()))?; + if w.manifest.security.as_ref().is_some_and(|s| !s.is_plaintext()) { + return Err(OfferError::Encrypted); + } + let refines = match w.manifest.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ContentHash)) { + Some(Ok(r)) => r, + None | Some(Err(_)) => return Ok(Err(OfferReport::refuse(hash, StageRefusal::RefinesMalformed, None))), + }; + if !is_refinement_only(w.capability()) || w.manifest.model_requirement.fallback.is_some() { + return Ok(Err(OfferReport::refuse(hash, StageRefusal::CapabilityNotRefinement, None))); + } + let cap = w.capability().to_string(); + Ok(Ok(Pending { hash, cap, refines, payload: w.payload, arrival_ms: now_ms, verified_ms: now_ms, evidence })) +} + +/// A layer that passed step 1: held for its base (§4.1 step 2), or being offered. +#[derive(Debug, Clone)] +struct Pending { + hash: [u8; 32], + cap: String, + refines: Refines, + payload: Vec, + arrival_ms: u64, + /// When the sink verified the evidence it holds (§8, §10). + verified_ms: u64, + evidence: ReceiptEvidence, +} + +/// A sink's stage sets: every base it decoded, and the layers it holds for bases it +/// has not (§4.1 step 2). Bases and layers may arrive in any order. +#[derive(Debug, Clone)] +pub struct StageSink { + cfg: ChainConfig, + chains: BTreeMap<[u8; 32], StageChain>, + pending: Vec, + /// What the sink keeps of the bases it does not hold (§4.4): the records of dropped + /// chains and the attestation of bases dropped while attested or marked before + /// they were decoded. A held base's are its chain's. + kept: BTreeMap<[u8; 32], Kept>, + /// The same bases by when they were last written, oldest first. + kept_order: BTreeMap, + kept_bytes: u64, + tick: u64, +} + +/// What a sink keeps of one base it does not hold. +#[derive(Debug, Clone, Default)] +struct Kept { + records: Vec<(u32, UnitRecord)>, + attested: bool, + written: u64, +} + +impl Kept { + /// §4.4: 32 bytes for the base's hash and attestation, and each kept unit record. + fn counted_bytes(&self) -> u64 { + 32 + self.records.iter().map(|(_, r)| r.counted_bytes()).sum::() + } +} + +impl StageSink { + /// A sink holding no base, with the bound, cost limit, budget and schedule every + /// chain it creates uses. + pub fn new(cfg: ChainConfig) -> Self { + StageSink { cfg, chains: BTreeMap::new(), pending: vec![], kept: BTreeMap::new(), kept_order: BTreeMap::new(), kept_bytes: 0, tick: 0 } + } + + /// The chain of the base whose content hash is `base`. + pub fn chain(&self, base: &[u8; 32]) -> Option<&StageChain> { + self.chains.get(base) + } + + /// Record a decoded base and release the layers held for it, together (§4.4). A + /// base the sink already holds is kept as it is: decoding it again does not reset + /// its units, and reports nothing; if this decode comes with the base's delivery + /// receipt verified, the base is attested from now on (§10), and never the reverse. + /// A base the sink attested before, held, dropped or not yet decoded, is attested + /// whatever `base.attested` says. + pub fn add_base(&mut self, mut base: BaseInfo, units: Vec) -> Vec { + let hash = base.content_hash; + if let Some(c) = self.chains.get_mut(&hash) { + if base.attested { + c.mark_attested(); + } + return vec![]; + } + let kept = self.take_kept(&hash).unwrap_or_default(); + base.attested |= kept.attested; + let records = kept.records; + let mut chain = StageChain::with_records(base, units, self.cfg.clone(), records); + let hexed = hex(&hash); + let (mine, rest): (Vec, Vec) = std::mem::take(&mut self.pending).into_iter().partition(|p| p.refines.base == hexed); + self.pending = rest; + let reports = if mine.is_empty() { vec![] } else { chain.release_pending(mine) }; + self.chains.insert(hash, chain); + reports + } + + /// Record that the sink verified a delivery receipt for the base `base` (§10), + /// whether it holds the base, dropped its chain or has not decoded it yet: step 5 + /// applies to every envelope for it that arrives after, and the base stays attested + /// across drops and decodes until the sink forgets it. + pub fn mark_attested(&mut self, base: &[u8; 32]) { + match self.chains.get_mut(base) { + Some(c) => c.mark_attested(), + None => { + let mut k = self.take_kept(base).unwrap_or_default(); + k.attested = true; + self.put_kept(*base, k); + } + } + } + + /// The bytes the sink keeps of the bases it does not hold (§4.4), within + /// `record_budget`. + pub fn kept_bytes(&self) -> u64 { + self.kept_bytes + } + + /// Remove what the sink keeps of `base`. + fn take_kept(&mut self, base: &[u8; 32]) -> Option { + let k = self.kept.remove(base)?; + self.kept_order.remove(&k.written); + self.kept_bytes -= k.counted_bytes(); + Some(k) + } + + /// Keep `k` for `base`, written now, unless it holds nothing; then forget the + /// bases written longest ago until what is kept is within the record budget (§4.4). + fn put_kept(&mut self, base: [u8; 32], mut k: Kept) { + if k.records.is_empty() && !k.attested { + return; + } + self.tick += 1; + k.written = self.tick; + self.kept_bytes += k.counted_bytes(); + self.kept_order.insert(k.written, base); + self.kept.insert(base, k); + while self.kept_bytes > self.cfg.record_budget { + let Some((_, oldest)) = self.kept_order.pop_first() else { break }; + let gone = self.kept.remove(&oldest).expect("kept"); + self.kept_bytes -= gone.counted_bytes(); + } + } + + /// Drop the chain of a base the sink no longer presents (§4.4): every envelope held + /// for its units is discarded (`base-absent`, one report each, in the order they + /// passed step 6), and each unit's record is kept, so the base decoded again + /// advances only along the producers it had. A layer for the base is then held as + /// for any base the sink has not decoded. `None` when the sink does not hold it. + pub fn remove_base(&mut self, base: &[u8; 32]) -> Option> { + let chain = self.chains.remove(base)?; + let reports = chain.layers.values().filter(|l| l.held > 0).map(|l| OfferReport::refuse(l.hash, StageRefusal::BaseAbsent, None)).collect(); + self.put_kept(*base, Kept { records: chain.records(), attested: chain.base.attested, written: 0 }); + Some(reports) + } + + /// Discard what the sink keeps of a base it does not hold (§14): its record and its + /// attestation. Decoded again, the base starts with no history and unattested, as + /// one never decoded. Whether there was anything to discard. + pub fn forget_base(&mut self, base: &[u8; 32]) -> bool { + self.take_kept(base).is_some() + } + + /// Offer the envelope `bytes` at `now_ms` with the receipt `evidence` (§4.1). A + /// layer whose base this sink has not decoded is held for it, up to the bound + /// (§4.3); offered again while held, it keeps the strongest evidence offered. + pub fn offer(&mut self, bytes: &[u8], now_ms: u64, evidence: ReceiptEvidence) -> Result, OfferError> { + let p = match step1(bytes, now_ms, evidence)? { + Ok(p) => p, + Err(r) => return Ok(vec![r]), + }; + let base = unhex32(&p.refines.base).filter(|b| self.chains.contains_key(b)); + match base { + Some(b) => { + let outside = self.pending.len() + + self.chains.iter().filter(|(k, _)| **k != b).map(|(_, c)| c.held_envelopes()).sum::(); + Ok(self.chains.get_mut(&b).expect("present").offer_checked(p, outside)) + } + None => { + let mut r = OfferReport::new(p.hash); + r.held_for_base = true; + if let Some(q) = self.pending.iter_mut().find(|q| q.hash == p.hash) { + if evidence_rank(p.evidence) > evidence_rank(q.evidence) { + q.evidence = p.evidence; + q.verified_ms = now_ms; + } + return Ok(vec![r]); + } + let held_layers = self.pending.len() + self.chains.values().map(StageChain::held_envelopes).sum::(); + if held_layers >= self.cfg.bound.max_envelopes { + return Ok(vec![OfferReport::refuse(p.hash, StageRefusal::BaseAbsent, None)]); + } + self.pending.push(p); + Ok(vec![r]) + } + } + } + + /// Discard what has been held past the bound at `now_ms` (§4.3): layers held for an + /// absent base (`base-absent`), then, chain by chain in base order, units held for a + /// layer (`layer-gap`). + pub fn expire(&mut self, now_ms: u64) -> Vec { + let age = self.cfg.bound.max_age_ms; + let (gone, keep): (Vec, Vec) = + std::mem::take(&mut self.pending).into_iter().partition(|p| now_ms.saturating_sub(p.arrival_ms) > age); + self.pending = keep; + let mut out: Vec = gone.into_iter().map(|p| OfferReport::refuse(p.hash, StageRefusal::BaseAbsent, None)).collect(); + for c in self.chains.values_mut() { + out.extend(c.expire(now_ms)); + } + out + } +} + +/// The order of evidence (§10): none, then a receipt verified by inclusion alone, then +/// one verified over the envelope's bytes. +fn evidence_rank(e: ReceiptEvidence) -> u8 { + match e { + ReceiptEvidence::None => 0, + ReceiptEvidence::InclusionVerified => 1, + ReceiptEvidence::DeliveredVerified => 2, + } +} diff --git a/wai/wai-rs/src/staged/predict.rs b/wai/wai-rs/src/staged/predict.rs new file mode 100644 index 000000000..278e4235e --- /dev/null +++ b/wai/wai-rs/src/staged/predict.rs @@ -0,0 +1,217 @@ +//! The predictors of a residual refinement (`WIR1`, `extensions/staged-delivery.md` +//! Appendix A.1): what the stage below predicts for each element of the next stage. +//! +//! - **identity**: the element of the stage below at the same position; +//! - **up2**: a picture at twice the resolution, by the half-pel arithmetic of +//! `int_video` (`(a + b + 1) >> 1`, `(a + b + c + d + 2) >> 2`), odd sizes included; +//! - **rate**: PCM at another sample rate, by linear interpolation rounded half away +//! from zero (`rdiv`), channels interleaved. +//! +//! Every value is an integer of at most 64 bits, and every index is in bounds for the +//! shapes the caller checked (the Kani harnesses below, run by `wai-verify`, check +//! `up2_at`'s bounds, `rdiv` against its definition and `rate_index`'s indices). This file is +//! self-contained (no `crate::` or `super::` path, `core` only) so the model checker +//! and the cross-architecture byte-equality crate include it as it is. + +/// `n / d` rounded to the nearest integer, halves away from zero: +/// `sign(n)·⌊(2|n| + d) / 2d⌋`, for `d ≥ 1`, computed as the quotient `q` and remainder +/// `r` of `|n|` by `d`, plus one where `2r ≥ d`. In `u64`, where no step can overflow +/// (`r < d`, and `q < 2^62` whenever `d ≥ 2`), so it is total for every `n` and every +/// `d ≥ 1`; `d < 1` gives 0. +pub fn rdiv(n: i64, d: i64) -> i64 { + if d < 1 { + return 0; + } + let (m, d) = (n.unsigned_abs(), d as u64); + let (q, r) = (m / d, m % d); + let mag = q + (r >= d - r) as u64; + // |result| ≤ 2^63, and 2^63 only for n = i64::MIN, d = 1, whose result is n. + if n < 0 { (mag as i64).wrapping_neg() } else { mag as i64 } +} + +/// The up2 prediction of element `(y, x, ch)` of an `out_h × out_w` picture from +/// `prev`, the `h2 × w2 × c` picture below it (rows, then columns, then channels), +/// where `h2 = ⌈out_h/2⌉` and `w2 = ⌈out_w/2⌉`. A neighbour past the last row or +/// column is that row or column (`min(Y, h2 − 1)`, `min(X, w2 − 1)`). +/// +/// The caller holds `prev.len() == h2·w2·c`, `y < 2·h2`, `x < 2·w2` and `ch < c`; +/// anything else gives 0 rather than reading out of bounds. +pub fn up2_at(prev: &[u8], h2: usize, w2: usize, c: usize, y: usize, x: usize, ch: usize) -> i64 { + if h2 == 0 || w2 == 0 || c == 0 || ch >= c || y >= 2 * h2 || x >= 2 * w2 { + return 0; + } + let Some(n) = h2.checked_mul(w2).and_then(|v| v.checked_mul(c)) else { return 0 }; + if prev.len() != n { + return 0; + } + let p = |yy: usize, xx: usize| -> i64 { prev[(yy.min(h2 - 1) * w2 + xx.min(w2 - 1)) * c + ch] as i64 }; + let (iy, fy, ix, fx) = (y >> 1, y & 1, x >> 1, x & 1); + match (fy, fx) { + (0, 0) => p(iy, ix), + (0, _) => (p(iy, ix) + p(iy, ix + 1) + 1) >> 1, + (_, 0) => (p(iy, ix) + p(iy + 1, ix) + 1) >> 1, + _ => (p(iy, ix) + p(iy, ix + 1) + p(iy + 1, ix) + p(iy + 1, ix + 1) + 2) >> 2, + } +} + +/// The source indices the rate prediction of output sample `i` reads, and its +/// interpolation phase: `s = i·den`, `lo = s div num`, `Ï� = s mod num`, +/// `hi = min(lo + 1, n − 1)`, for `n` source samples per channel. `None` when +/// `num`, `den` or `n` is 0, or when `lo ≥ n` (an `i` past `⌊n·num/den⌋ − 1`). +pub fn rate_index(i: u64, n: u64, num: u64, den: u64) -> Option<(u64, u64, u64)> { + if num == 0 || den == 0 || n == 0 { + return None; + } + // Below the length `rate_len` gives, i·den < n·num, which is under 2^58 for the + // shapes a WIR1 payload allows (n ≤ 2^26, num < 2^32): checked, and None past u64. + let s = i.checked_mul(den)?; + let (lo, rho) = (s / num, s % num); + if lo >= n { + return None; + } + Some((lo, (lo + 1).min(n - 1), rho)) +} + +/// The rate prediction of channel `ch` of output sample `i` from `prev`, `n` samples of +/// `c` interleaved channels at the source rate `den`, for output at the rate `num`: +/// `rdiv(a·num + (b − a)·Ï�, num)` with `a = prev[lo·c + ch]`, `b = prev[hi·c + ch]` +/// ([`rate_index`]). This is `a + (b − a)·Ï�/num` rounded half away from zero, the +/// linear interpolation at source position `i·den/num`, in exact integer arithmetic. +/// 0 where the indices are out of range (the caller checked the shapes). +pub fn rate_at(prev: &[i16], c: usize, num: u32, den: u32, i: usize, ch: usize) -> i64 { + if c == 0 || ch >= c || prev.len() % c != 0 { + return 0; + } + let n = (prev.len() / c) as u64; + let Some((lo, hi, rho)) = rate_index(i as u64, n, num as u64, den as u64) else { return 0 }; + let a = prev[lo as usize * c + ch] as i64; + let b = prev[hi as usize * c + ch] as i64; + rdiv(a * num as i64 + (b - a) * rho as i64, num as i64) +} + +/// The number of output samples per channel the rate predictor gives from `n` source +/// samples: `⌊n·num/den⌋`, or `None` for `den` 0 or `n·num` past `u64` (a WIR1 unit +/// holds at most 2^26 samples, so `n·num` is under 2^58 there). +pub fn rate_len(n: u64, num: u32, den: u32) -> Option { + if den == 0 { + return None; + } + Some(n.checked_mul(num as u64)? / den as u64) +} + +// ───────────────────────────────────────────────────────────────────── +// Kani proof harnesses (wai-verify includes this file) +// ───────────────────────────────────────────────────────────────────── + +/// `up2_bounded`: on every picture of up to 2×2 pixels and at every output position +/// of the 2× picture (odd output sizes, which leave the last row or column unused, +/// included), the prediction lies between the least and the greatest of the +/// neighbours it reads, so the half-pel rounding never leaves their range. +#[cfg(kani)] +#[kani::proof] +#[kani::unwind(5)] +fn up2_bounded() { + let h2: usize = kani::any(); + let w2: usize = kani::any(); + kani::assume((1..=2).contains(&h2) && (1..=2).contains(&w2)); + let prev: [u8; 4] = kani::any(); + let y: usize = kani::any(); + let x: usize = kani::any(); + kani::assume(y < 2 * h2 && x < 2 * w2); + let v = up2_at(&prev[..h2 * w2], h2, w2, 1, y, x, 0); + let p = |yy: usize, xx: usize| prev[yy.min(h2 - 1) * w2 + xx.min(w2 - 1)] as i64; + let (iy, ix) = (y >> 1, x >> 1); + let ns = [p(iy, ix), p(iy, ix + 1), p(iy + 1, ix), p(iy + 1, ix + 1)]; + let (mut lo, mut hi) = (ns[0], ns[0]); + for n in ns { + lo = lo.min(n); + hi = hi.max(n); + } + kani::assert(lo <= v && v <= hi, "the prediction lies between its neighbours"); + kani::cover!(y & 1 == 1 && x & 1 == 1, "the four-neighbour case is reached"); +} + +/// `rdiv_matches_spec`: `rdiv(n, d)` is `|n|/d` rounded half away from zero with the +/// sign of `n`, stated without division: its magnitude `k` is the one integer with +/// `(2k − 1)·d ≤ 2|n| < (2k + 1)·d` (for `k = 0`, `2|n| < d`). For `|n| < 2^12` and `d` +/// in `1..=2^6`: the obligation is nonlinear, which the solver discharges only over a +/// window; the unit tests cover `i64`'s extremes. +#[cfg(kani)] +#[kani::proof] +fn rdiv_matches_spec() { + let n: i64 = kani::any(); + let d: i64 = kani::any(); + kani::assume(n > -(1i64 << 12) && n < (1i64 << 12)); + kani::assume(d >= 1 && d <= (1i64 << 6)); + let v = rdiv(n, d); + kani::assert(v == 0 || (v < 0) == (n < 0), "the sign is n's"); + let (k, m, d) = (v.unsigned_abs(), n.unsigned_abs(), d as u64); + kani::assert(2 * m < (2 * k + 1) * d, "below the next half"); + kani::assert(k == 0 || (2 * k - 1) * d <= 2 * m, "at or above the half below"); + kani::cover!(k >= 1 && (2 * k - 1) * d == 2 * m, "a half is reached"); +} + +/// `rate_index_in_bounds`: for every output sample `i` the rate predictor forms +/// (`i < ⌊n·num/den⌋`), with `n` source samples of `c` channels, both indices it reads +/// are samples of the source (`lo < n`, `hi < n`), so `lo·c + ch < n·c` for every +/// channel `ch < c`; `hi` is `min(lo + 1, n − 1)` and the phase is below `num`. For +/// `n`, `num` and `den` up to 32 and up to 4 channels: the obligation +/// (`i < ⌊n·num/den⌋ ⇒ ⌊i·den/num⌋ < n`) is nonlinear, which the solver discharges +/// only over a narrow window; the unit tests and the corpus cover larger rates. +#[cfg(kani)] +#[kani::proof] +fn rate_index_in_bounds() { + let n: u64 = kani::any(); + let num: u32 = kani::any(); + let den: u32 = kani::any(); + let i: u64 = kani::any(); + kani::assume((1..=32).contains(&n) && (1..=32).contains(&num) && (1..=32).contains(&den)); + let len = rate_len(n, num, den); + kani::assert(len.is_some(), "the output length fits u64"); + kani::assume(i < len.unwrap_or(0)); + let ix = rate_index(i, n, num as u64, den as u64); + kani::assert(ix.is_some(), "every output sample has its indices"); + if let Some((lo, hi, rho)) = ix { + kani::assert(lo < n && hi < n, "both indices are source samples"); + kani::assert(hi == (lo + 1).min(n - 1), "hi is the next sample, held at the end"); + kani::assert(rho < num as u64, "the phase is below num"); + let c: u64 = kani::any(); + let ch: u64 = kani::any(); + kani::assume((1..=4).contains(&c) && ch < c); + kani::assert(lo * c + ch < n * c && hi * c + ch < n * c, "every channel's index is in the buffer"); + } + kani::cover!(num > den, "an upsampling is reached"); + kani::cover!(num < den, "a downsampling is reached"); +} + +#[cfg(test)] +mod tests { + use super::*; + + /// `rdiv` rounds halves away from zero, on both signs. + #[test] + fn rdiv_rounds_halves_away_from_zero() { + for (n, d, want) in [(5, 2, 3), (-5, 2, -3), (4, 2, 2), (-4, 2, -2), (7, 3, 2), (-7, 3, -2), (8, 3, 3), + (-8, 3, -3), (1, 2, 1), (-1, 2, -1), (0, 5, 0), (i64::MAX, 1, i64::MAX), (i64::MIN + 1, 1, i64::MIN + 1)] { + assert_eq!(rdiv(n, d), want, "rdiv({n}, {d})"); + } + assert_eq!(rdiv(3, 0), 0); + } + + /// up2 on a 2×2 picture to 3×3 (odd), at every position: the half-pel + /// arithmetic, with the last row and column repeated. + #[test] + fn up2_matches_the_half_pel_rule_on_odd_sizes() { + let prev = [10u8, 20, 30, 41]; + let want = [[10, 15, 20], [20, 25, 31], [30, 36, 41]]; + for (y, row) in want.iter().enumerate() { + for (x, &v) in row.iter().enumerate() { + assert_eq!(up2_at(&prev, 2, 2, 1, y, x, 0), v, "({y},{x})"); + } + } + // Past the picture, a wrong buffer length, or a channel out of range: 0, no panic. + assert_eq!(up2_at(&prev, 2, 2, 1, 4, 0, 0), 0); + assert_eq!(up2_at(&prev[..3], 2, 2, 1, 0, 0, 0), 0); + assert_eq!(up2_at(&prev, 2, 2, 1, 0, 0, 1), 0); + } +} diff --git a/wai/wai-rs/src/staged/schedule.rs b/wai/wai-rs/src/staged/schedule.rs new file mode 100644 index 000000000..bc54c6caf --- /dev/null +++ b/wai/wai-rs/src/staged/schedule.rs @@ -0,0 +1,110 @@ +//! The live schedule of a stage set (`extensions/staged-delivery.md` §8): when each +//! unit is presented, whether a layer arrived in time to be applied to it, how many +//! layers a sink can fetch before a unit is due, and what a sink presents for a trace +//! of arrivals. Pure integer functions of their inputs, so the corpus's `schedule.json` +//! is replayed exactly by the Rust and the Python verifiers. + +/// The timing of a stage set's units: the unit at position `u` on the clock is +/// presented at `t0_ms + u·unit_ms + target_latency_ms`. A base's unit `i` sits at +/// position `first_unit + i` ([`super::BaseInfo::first_unit`]). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct UnitClock { + pub t0_ms: u64, + pub unit_ms: u64, + pub target_latency_ms: u64, +} + +/// One layer a publisher offers: its index, its lead (the time before a unit's +/// presentation by which it must arrive), and its bytes as delivered. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct LayerPlan { + pub layer: u8, + pub lead_ms: u64, + pub bytes: u64, +} + +/// When the unit at clock position `unit` is presented (saturating at `u64::MAX`). +pub fn presentation_ms(c: &UnitClock, unit: u64) -> u64 { + c.t0_ms.saturating_add(unit.saturating_mul(c.unit_ms)).saturating_add(c.target_latency_ms) +} + +/// Whether a layer that arrived at `arrival_ms` with lead `lead_ms` is in time for +/// the unit at clock position `unit`: it arrived at least `lead_ms` before the unit's +/// presentation (§8). +pub fn on_time(c: &UnitClock, unit: u64, arrival_ms: u64, lead_ms: u64) -> bool { + arrival_ms.checked_add(lead_ms).is_some_and(|t| t <= presentation_ms(c, unit)) +} + +/// The milliseconds `bytes` take at `throughput_bps` bits per second, rounded up; +/// `u64::MAX` at a throughput of 0. +pub fn transfer_ms(bytes: u64, throughput_bps: u64) -> u64 { + if throughput_bps == 0 { + return u64::MAX; + } + let bits = (bytes as u128) * 8 * 1000; + u64::try_from(bits.div_ceil(throughput_bps as u128)).unwrap_or(u64::MAX) +} + +/// How many layers a sink fetching from `now_ms` at `throughput_bps` can apply to a +/// unit due at `due_ms`: the largest `k` such that, for every `j ≤ k`, layers `1..=j` +/// fetched in order arrive at least `plan[j].lead_ms` before `due_ms`. `plan` is in +/// layer order. Integer arithmetic only, so the answer is the same everywhere. +pub fn choose_layers(throughput_bps: u64, now_ms: u64, due_ms: u64, plan: &[LayerPlan]) -> u8 { + let mut total: u64 = 0; + let mut k: u8 = 0; + for p in plan { + total = total.saturating_add(p.bytes); + let arrives = now_ms.saturating_add(transfer_ms(total, throughput_bps)); + if arrives.checked_add(p.lead_ms).is_none_or(|t| t > due_ms) { + break; + } + k = k.saturating_add(1); + } + k +} + +/// What a sink presents for one unit of a trace: when, and at which stage. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Presented { + pub at_ms: u64, + pub stage: u8, + /// The stage the layers the trace holds for this unit would have reached had + /// each arrived in time. + pub expected: u8, +} + +/// What a sink presents for a trace (§8). `base_ms[u]` is when unit `u`'s base +/// arrived; `layers[l][u]`, when layer `l + 1` arrived for unit `u` (`None`: it +/// never did); `lead_ms[l]`, layer `l + 1`'s lead. A unit is presented at its +/// presentation time, or when its base arrives if that is later: the sink never waits +/// for a layer. Its stage is the longest run of layers `1, 2, …` each of which arrived +/// in time; a layer that arrived late is not applied, and no layer above it is. Its +/// expected stage is the longest run of layers the trace holds for it. +pub fn present(c: &UnitClock, base_ms: &[u64], layers: &[Vec>], lead_ms: &[u64]) -> Vec { + base_ms + .iter() + .enumerate() + .map(|(u, &b)| { + let unit = u as u64; + let at_ms = presentation_ms(c, unit).max(b); + let mut stage = 0u8; + let mut expected = 0u8; + let mut in_time = true; + for (l, arrivals) in layers.iter().enumerate() { + let Some(Some(t)) = arrivals.get(u) else { break }; + expected = expected.saturating_add(1); + in_time = in_time && on_time(c, unit, *t, lead_ms.get(l).copied().unwrap_or(0)); + if in_time { + stage = stage.saturating_add(1); + } + } + Presented { at_ms, stage, expected } + }) + .collect() +} + +/// The units a trace presents below the stage their layers would have reached on +/// time (§8): the count a sink keeps. +pub fn deadline_misses(p: &[Presented]) -> u64 { + p.iter().filter(|x| x.stage < x.expected).count() as u64 +} diff --git a/wai/wai-rs/src/staged/tests.rs b/wai/wai-rs/src/staged/tests.rs new file mode 100644 index 000000000..9e2baf05d --- /dev/null +++ b/wai/wai-rs/src/staged/tests.rs @@ -0,0 +1,1536 @@ +//! Tests of the staged-delivery core. + +use super::predict::{rate_at, rdiv}; +use super::wir1::{apply_unit, encode, parse, Below, Next, Predictor, RefineParams, UnitKind, Wir1Refusal}; + +/// A deterministic byte pattern: smooth gradients plus a texture, so residuals have +/// a spread and a few large outliers. +fn picture(h: usize, w: usize, seed: u32) -> Vec { + let mut v = Vec::with_capacity(h * w * 3); + let mut x = seed.wrapping_mul(2654435761) | 1; + for y in 0..h { + for xx in 0..w { + for c in 0..3 { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + let base = (y * 255 / h.max(1) + xx * 7 + c * 40) as u32; + let noise = if x % 97 == 0 { x % 256 } else { x % 9 }; + v.push(((base + noise) % 256) as u8); + } + } + } + v +} + +fn pcm(n: usize, ch: usize, seed: i64) -> Vec { + (0..n * ch) + .map(|i| { + let t = (i / ch) as i64; + let c = (i % ch) as i64; + let v = ((t * (37 + c * 11) + seed) % 2000 - 1000) * 30 + if t % 101 == 0 { 20000 } else { 0 }; + v.clamp(-32768, 32767) as i16 + }) + .collect() +} + +fn params(q: u32, predictor: Predictor, max_symbols: u32, table_per_unit: bool) -> RefineParams { + RefineParams { q, predictor, rate: (0, 0), table_per_unit, max_symbols } +} + +fn as_u8(n: &Next) -> &[u8] { + match n { + Next::U8(v) => v, + Next::I16(_) => panic!("u8 expected"), + } +} + +fn as_i16(n: &Next) -> &[i16] { + match n { + Next::I16(v) => v, + Next::U8(_) => panic!("i16 expected"), + } +} + +/// Test 12 (`wir1::roundtrip_all_predictors`): for identity, up2 (even and odd sizes) +/// and rate (mono and stereo), on u8 and i16, with one table and with a table per +/// unit, and with tables narrow enough that most residuals take the bypass escape: +/// a sink's parse and per-unit decode give the encoder's reconstruction exactly; at +/// `q = 1` with no clamp in play the reconstruction is the target. +#[test] +fn roundtrip_all_predictors() { + // identity, RGB8, two units, q = 1 → lossless. + let below: Vec> = vec![picture(9, 7, 1), picture(9, 7, 2)]; + let target: Vec> = vec![picture(9, 7, 3), picture(9, 7, 4)]; + let b: Vec = below.iter().map(|v| Below::U8(v)).collect(); + let t: Vec = target.iter().map(|v| Below::U8(v)).collect(); + for (max_symbols, per_unit) in [(4095, false), (5, true), (1, false)] { + let (bytes, recon) = encode(UnitKind::Rgb8, &b, (9, 7, 3), &t, (9, 7, 3), ¶ms(1, Predictor::Identity, max_symbols, per_unit)).unwrap(); + let w = parse(&bytes, UnitKind::Rgb8).unwrap(); + assert_eq!(w.n_tables(), if per_unit { 2 } else { 1 }); + for u in 0..2 { + let got = apply_unit(&w, &bytes, u, b[u as usize], (9, 7, 3)).unwrap(); + assert_eq!(got, recon[u as usize]); + assert_eq!(as_u8(&got), &target[u as usize][..], "q = 1 identity is lossless (max_symbols {max_symbols})"); + } + } + // up2, even (8×8 → 16×16) and odd (8×8 → 15×15, 13×13 → 25×25), q = 1 and q = 6. + for (h2, w2, h, w) in [(8usize, 8usize, 16u32, 16u32), (8, 8, 15, 15), (13, 13, 25, 25), (1, 1, 1, 2)] { + for q in [1u32, 6] { + let below = picture(h2, w2, 5); + let target = picture(h as usize, w as usize, 6); + let (bytes, recon) = encode(UnitKind::Rgb8, &[Below::U8(&below)], (h2 as u32, w2 as u32, 3), &[Below::U8(&target)], (h, w, 3), + ¶ms(q, Predictor::Up2, 7, false)).unwrap(); + let w1 = parse(&bytes, UnitKind::Rgb8).unwrap(); + let got = apply_unit(&w1, &bytes, 0, Below::U8(&below), (h2 as u32, w2 as u32, 3)).unwrap(); + assert_eq!(got, recon[0]); + if q == 1 { + assert_eq!(as_u8(&got), &target[..], "q = 1 up2 is lossless ({h2}x{w2} → {h}x{w})"); + } else { + let worst = as_u8(&got).iter().zip(&target).map(|(a, b)| (*a as i32 - *b as i32).abs()).max().unwrap(); + assert!(worst <= (q as i32 + 1) / 2, "the error is at most q/2 ({worst})"); + } + } + } + // rate, i16, mono and stereo, up and down, with outliers that bypass. + for (n_src, ch, num, den) in [(100usize, 1usize, 3u32, 1u32), (64, 2, 3, 2), (90, 2, 1, 3), (5, 1, 48000, 8000)] { + let below = pcm(n_src, ch, 7); + let n_out = (n_src as u64 * num as u64 / den as u64) as u32; + let target = pcm(n_out as usize, ch, 9); + let p = RefineParams { q: 1, predictor: Predictor::Rate, rate: (num, den), table_per_unit: false, max_symbols: 3 }; + let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (n_src as u32, 1, ch as u32), &[Below::I16(&target)], (n_out, 1, ch as u32), &p).unwrap(); + let w = parse(&bytes, UnitKind::Pcm).unwrap(); + let got = apply_unit(&w, &bytes, 0, Below::I16(&below), (n_src as u32, 1, ch as u32)).unwrap(); + assert_eq!(got, recon[0]); + assert_eq!(as_i16(&got), &target[..], "q = 1 rate is lossless ({n_src}×{ch} at {num}/{den})"); + } + // identity, i16 at q = 300: the clamp bounds the extremes. + let below = vec![i16::MIN, -1, 0, 1, i16::MAX, 12345]; + let target = vec![i16::MAX, i16::MIN, 300, -300, i16::MIN, 0]; + let p = params(300, Predictor::Identity, 2, false); + let (bytes, recon) = encode(UnitKind::Pcm, &[Below::I16(&below)], (6, 1, 1), &[Below::I16(&target)], (6, 1, 1), &p).unwrap(); + let w = parse(&bytes, UnitKind::Pcm).unwrap(); + assert_eq!(apply_unit(&w, &bytes, 0, Below::I16(&below), (6, 1, 1)).unwrap(), recon[0]); + assert!(as_i16(&recon[0]).iter().zip(&target).all(|(a, b)| (*a as i32 - *b as i32).abs() <= 150)); + // splat attributes, identity. + let below = picture(5, 5, 8)[..14 * 4].to_vec(); + let target = picture(5, 5, 9)[..14 * 4].to_vec(); + let (bytes, recon) = encode(UnitKind::Attr, &[Below::U8(&below)], (14, 2, 2), &[Below::U8(&target)], (14, 2, 2), ¶ms(1, Predictor::Identity, 9, false)).unwrap(); + let w = parse(&bytes, UnitKind::Attr).unwrap(); + assert_eq!(as_u8(&apply_unit(&w, &bytes, 0, Below::U8(&below), (14, 2, 2)).unwrap()), &target[..]); + assert_eq!(as_u8(&recon[0]), &target[..]); +} + +/// The rate predictor is `a + (b − a)·Ï�/num` rounded half away from zero: a ramp +/// doubles exactly, and a negative half rounds away from zero. +#[test] +fn rate_interpolates_with_rdiv() { + let prev = [0i16, 10, -3, -4]; + // 2/1: positions 0, .5, 1, 1.5, 2, 2.5, 3, 3.5 (the last two hold at the end). + let got: Vec = (0..8).map(|i| rate_at(&prev, 1, 2, 1, i, 0)).collect(); + assert_eq!(got, vec![0, 5, 10, rdiv(10 * 2 + (-13), 2), -3, rdiv(-3 * 2 - 1, 2), -4, -4]); + assert_eq!(got[3], 4, "3.5 rounds away from zero to 4"); + assert_eq!(got[5], -4, "-3.5 rounds away from zero to -4"); +} + +/// A valid one-unit RGB8 payload to mutate. +fn fixture() -> Vec { + let below = picture(2, 2, 1); + let target = picture(2, 2, 2); + encode(UnitKind::Rgb8, &[Below::U8(&below)], (2, 2, 3), &[Below::U8(&target)], (2, 2, 3), ¶ms(2, Predictor::Identity, 9, false)).unwrap().0 +} + +fn code(p: &[u8], kind: UnitKind) -> &'static str { + match parse(p, kind) { + Ok(_) => "ok", + Err(e) => e.code(), + } +} + +/// The parser refuses in the appendix's order, one fixture per code, and a fixture +/// with two faults reports the earlier one. +#[test] +fn wir1_refusal_order() { + let ok = fixture(); + assert_eq!(code(&ok, UnitKind::Rgb8), "ok"); + let set = |at: usize, v: &[u8]| { + let mut p = ok.clone(); + p[at..at + v.len()].copy_from_slice(v); + p + }; + assert_eq!(code(b"WIR", UnitKind::Rgb8), "bad_magic"); + assert_eq!(code(&set(0, b"WIV1"), UnitKind::Rgb8), "bad_magic"); + assert_eq!(code(&ok[..4], UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(4, &[2]), UnitKind::Rgb8), "version"); + assert_eq!(code(&ok[..43], UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(42, &[1]), UnitKind::Rgb8), "reserved_nonzero"); + assert_eq!(code(&set(5, &[3]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(6, &[3]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(7, &[2]), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(8, &0u32.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(24, &1u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "rate fields on identity"); + assert_eq!(code(&set(32, &0u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "q = 0"); + assert_eq!(code(&set(32, &65536u32.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&set(40, &2u16.to_le_bytes()), UnitKind::Rgb8), "field_range"); + assert_eq!(code(&ok, UnitKind::Pcm), "field_range", "u8 elements for a PCM unit"); + assert_eq!(code(&set(20, &4u32.to_le_bytes()), UnitKind::Rgb8), "field_range", "RGB8 has three channels"); + assert_eq!(code(&set(12, &(1u32 << 25).to_le_bytes()), UnitKind::Rgb8), "too_large"); + assert_eq!(code(&set(8, &((1u32 << 20) + 1).to_le_bytes()), UnitKind::Rgb8), "too_large"); + assert_eq!(code(&set(44, &1u32.to_le_bytes()), UnitKind::Rgb8), "cdf_malformed"); + let cdf_len = u16::from_le_bytes([ok[40], ok[41]]) as usize; + let lens_at = 44 + 4 * cdf_len; + assert_eq!(code(&ok[..lens_at - 1], UnitKind::Rgb8), "stream_length", "a table cut short"); + assert_eq!(code(&set(lens_at, &6u32.to_le_bytes()), UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&set(lens_at, &4u32.to_le_bytes()), UnitKind::Rgb8), "stream_length"); + assert_eq!(code(&ok[..ok.len() - 1], UnitKind::Rgb8), "stream_length"); + let mut trailing = ok.clone(); + trailing.push(0); + assert_eq!(code(&trailing, UnitKind::Rgb8), "trailing"); + // Order: reserved before fields, fields before caps, caps before tables. + let mut two = set(42, &[1]); + two[5] = 9; + assert_eq!(code(&two, UnitKind::Rgb8), "reserved_nonzero"); + let mut two = set(5, &[9]); + two[12..16].copy_from_slice(&(1u32 << 25).to_le_bytes()); + assert_eq!(code(&two, UnitKind::Rgb8), "field_range"); + let mut two = set(12, &(1u32 << 25).to_le_bytes()); + two[44] = 1; + assert_eq!(code(&two, UnitKind::Rgb8), "too_large"); + for r in Wir1Refusal::ALL { + assert_eq!(Wir1Refusal::from_code(r.code()), Some(r)); + } +} + +/// The extension's text lists the codes this module reports, in its order: §4.2's +/// table the reasons of [`super::StageRefusal::ALL`] (two rows at step 2 and one row +/// listing three at step 6 included), and A.6 the codes of [`Wir1Refusal::ALL`]. +#[test] +fn staged_delivery_text_lists_the_codes() { + let text = include_str!("../../../extensions/staged-delivery.md"); + let at = text.find("### 4.2 Reasons").expect("§4.2"); + let table = &text[at..at + text[at..].find("The list is closed").expect("the table's end")]; + let mut reasons: Vec<&str> = Vec::new(); + for row in table.lines().filter(|l| l.starts_with("| ") && !l.starts_with("| step") ) { + let cell = row.split('|').nth(2).unwrap_or(""); + for code in cell.split('`').skip(1).step_by(2) { + if !reasons.contains(&code) { + reasons.push(code); + } + } + } + let want: Vec<&str> = super::StageRefusal::ALL.iter().map(|r| r.code()).collect(); + assert_eq!(reasons, want, "§4.2 lists the reasons StageRefusal reports, in its order"); + let a6 = &text[text.find("### A.6 Refusal codes").expect("A.6")..]; + let list = &a6[..a6.find(". A parser").expect("the list's end")]; + let codes: Vec<&str> = list.split('`').skip(1).step_by(2).filter(|c| !c.contains('.')).collect(); + let want: Vec<&str> = Wir1Refusal::ALL.iter().map(|r| r.code()).collect(); + assert_eq!(codes, want, "A.6 lists the codes Wir1Refusal reports, in its order"); +} + +/// A header whose sizes pass 32 bits when multiplied (one table per unit, 2^20 units, +/// tables of 4097 entries) is refused for the bytes it lacks, on a 32-bit target as on +/// a 64-bit one: no size is formed in a 32-bit `usize`. +#[test] +fn wir1_header_products_fit_any_usize() { + for (n_units, cdf_len) in [(1u32 << 20, 4097u16), (1 << 20, 4096), (1 << 19, 4097)] { + let mut p = Vec::new(); + p.extend_from_slice(b"WIR1"); + p.extend_from_slice(&[1, 1, 0, 1]); + for v in [n_units, 1u32, 1, 1, 0, 0, 1] { + p.extend_from_slice(&v.to_le_bytes()); + } + p.extend_from_slice(&0i32.to_le_bytes()); + p.extend_from_slice(&cdf_len.to_le_bytes()); + p.extend_from_slice(&[0, 0]); + assert_eq!(code(&p, UnitKind::Attr), "stream_length", "{n_units} units, tables of {cdf_len}"); + } +} + +/// A stream that runs out is refused at its first missing symbol, and a word after +/// the last symbol is not canonical; both before any unit is applied, by +/// `check_streams`, and by the per-unit decode. +#[test] +fn wir1_stream_ends() { + let ok = fixture(); + let w = parse(&ok, UnitKind::Rgb8).unwrap(); + assert!(w.check_streams(&ok).is_ok()); + let cdf_len = u16::from_le_bytes([ok[40], ok[41]]) as usize; + let lens_at = 44 + 4 * cdf_len; + let len = u32::from_le_bytes(ok[lens_at..lens_at + 4].try_into().unwrap()); + // One more word after the stream: the stream length grows by 4. + let mut extra = ok.clone(); + extra[lens_at..lens_at + 4].copy_from_slice(&(len + 4).to_le_bytes()); + extra.extend_from_slice(&[0, 0, 0, 0]); + let we = parse(&extra, UnitKind::Rgb8).unwrap(); + assert_eq!(we.check_streams(&extra).unwrap_err().1.code(), "stream_noncanonical"); + let below = picture(2, 2, 1); + assert_eq!(apply_unit(&we, &extra, 0, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "stream_noncanonical"); + // A unit of four times the elements runs the stream out. + let mut big = ok.clone(); + big[12..16].copy_from_slice(&4u32.to_le_bytes()); + big[16..20].copy_from_slice(&4u32.to_le_bytes()); + let wb = parse(&big, UnitKind::Rgb8).unwrap(); + assert_eq!(wb.check_streams(&big).unwrap_err().1.code(), "stream_exhausted"); + let below4 = picture(4, 4, 1); + assert_eq!(apply_unit(&wb, &big, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "stream_exhausted"); + // The wrong shape below is refused before any symbol. + assert_eq!(apply_unit(&w, &ok, 0, Below::U8(&below4), (4, 4, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, &ok, 0, Below::I16(&[0; 12]), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); + assert_eq!(apply_unit(&w, &ok, 1, Below::U8(&below), (2, 2, 3)).unwrap_err().code(), "geometry_mismatch"); +} + +// ───────────────────────── the stage chain (§4) ───────────────────────── + +use super::schedule::{present, presentation_ms, UnitClock}; +use super::tier::compose; +use super::{BaseInfo, ChainConfig, HoldBound, Live, OfferReport, ReceiptEvidence, Samples, StageChain, StageRefusal, StageSink, UnitBuf}; +use crate::codecs::{DeterminismTier, CAP_IMAGE_JPEG, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE}; +use crate::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, RefForm, Refines, Span, Wai}; +use serde_json::json; + +const H: u32 = 4; +const W: u32 = 6; + +fn hexs(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +fn envelope(cap: &str, refines: Option, payload: Vec) -> Vec { + let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x")); + m.refines = refines; + Wai::new(m, payload).to_bytes().unwrap() +} + +fn frame(seed: u32) -> Vec { + picture(H as usize, W as usize, seed) +} + +/// A base of four frames, its envelope's hash, and the frames. +fn base4() -> (Vec, BaseInfo, Vec) { + let env = envelope(CAP_VIDEO_INT_MOTION, None, b"a base payload".to_vec()); + let info = BaseInfo { + content_hash: crate::jwp_hash::content_hash(&env), + capability: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + tier: Some(DeterminismTier::DecodeEquivalence), + attested: false, + first_unit: 0, + }; + let units = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); + (env, info, units) +} + +fn cfg() -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes: 8, max_age_ms: 1000 }, cost_limit: None, budget: u64::MAX, record_budget: u64::MAX, live: None } +} + +/// A residual layer over `belows` (units `from..from+belows.len()`) toward frames of +/// `seed`, with `prev` as given. +fn layer(base: &[u8; 32], prev: Prev, layer: u8, span: Option, belows: &[Vec], seed: u32) -> Vec { + let b: Vec = belows.iter().map(|v| Below::U8(v)).collect(); + let targets: Vec> = (0..belows.len()).map(|i| frame(seed + i as u32)).collect(); + let t: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let (payload, _) = encode(UnitKind::Rgb8, &b, (H, W, 3), &t, (H, W, 3), ¶ms(3, Predictor::Identity, 31, false)).unwrap(); + let r = Refines { base: hexs(base), prev, layer, span }; + envelope(CAP_VIDEO_INT_REFINE, Some(r.to_value()), payload) +} + +fn one(base: &[u8; 32]) -> Prev { + Prev::One(hexs(base)) +} + +fn h(env: &[u8]) -> [u8; 32] { + crate::jwp_hash::content_hash(env) +} + +fn unit_frame(c: &StageChain, u: u32) -> Vec { + match &c.unit(u).unwrap().buf.samples { + Samples::U8(v) => v.clone(), + Samples::I16(_) => panic!(), + } +} + +/// Test 1: each malformation of §3 is `refines-malformed`, and a well-formed member +/// reads back as written. +#[test] +fn refines_check_rejects_each_malformation() { + let a = "a".repeat(64); + let b = "b".repeat(64); + let read = |v: serde_json::Value| Refines::from_value(&v, RefForm::ContentHash); + let ok = json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 1}], "layer": 2, "span": {"from": 1, "count": 3}}); + let r = read(ok.clone()).unwrap(); + assert_eq!(r.to_value(), ok, "a well-formed member reads back as written"); + assert_eq!(r.producer_for(0), Some(a.as_str())); + assert_eq!(r.producer_for(2), Some(b.as_str())); + assert_eq!(r.producer_for(3), None); + assert!(read(json!({"base": a, "prev": a, "layer": 1})).is_ok()); + assert!(read(json!({"base": a, "prev": a, "layer": 1, "unknown": [1, 2]})).is_ok(), "an undefined member is ignored"); + assert_eq!(read(json!({"base": a, "prev": a, "layer": 1, "span": null})).unwrap().span, None, "a null span is absent (SPEC §3.1)"); + let bad = [ + json!({"base": a.to_uppercase(), "prev": a, "layer": 1}), + json!({"base": &a[..63], "prev": &a[..63], "layer": 1}), + json!({"base": a, "prev": a, "layer": 0}), + json!({"base": a, "prev": a, "layer": 256}), + json!({"base": a, "prev": b, "layer": 1}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": 0, "count": 0}}), + json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 2}], "layer": 2, "span": {"from": 0, "count": 3}}), + json!({"base": a, "prev": [{"producer": a, "count": 1}, {"producer": a, "count": 1}], "layer": 2}), + json!({"base": a, "prev": [{"producer": a, "count": 2}], "layer": 2}), + json!({"base": a, "prev": [], "layer": 2}), + json!({"base": a, "prev": a, "layer": "2"}), + json!({"base": a, "prev": a, "layer": 2.0}), + json!({"base": 7, "prev": a, "layer": 2}), + json!({"base": a, "prev": {"producer": a}, "layer": 2}), + json!({"base": a, "prev": [{"producer": a, "count": 1.5}, {"producer": b, "count": 1}], "layer": 2}), + json!({"base": a, "prev": a, "layer": 2, "span": [0, 1]}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": -1, "count": 1}}), + json!({"base": a, "prev": a, "layer": 2, "span": {"from": u32::MAX, "count": 2}}), + json!({"prev": a, "layer": 1}), + json!([a, a, 1]), + ]; + for (i, v) in bad.iter().enumerate() { + assert!(read(v.clone()).is_err(), "case {i} is malformed: {v}"); + } + // An integer member is written as an integer: no fraction, no exponent, no sign on + // zero (SPEC §3). + for (k, v) in [("layer", "-0"), ("layer", "1.0"), ("layer", "1e0"), ("layer", "-0.0"), ("from", "-0"), ("from", "0e0"), ("count", "1E0"), ("count", "2.5")] { + let text = match k { + "layer" => format!(r#"{{"base": "{a}", "prev": "{a}", "layer": {v}}}"#), + "from" => format!(r#"{{"base": "{a}", "prev": "{a}", "layer": 2, "span": {{"from": {v}, "count": 1}}}}"#), + _ => format!(r#"{{"base": "{a}", "prev": [{{"producer": "{a}", "count": {v}}}, {{"producer": "{b}", "count": 1}}], "layer": 2}}"#), + }; + let parsed: serde_json::Value = serde_json::from_str(&text).unwrap(); + assert!(read(parsed).is_err(), "{k}: {v} is not an integer"); + } + let zero: serde_json::Value = serde_json::from_str(&format!(r#"{{"base": "{a}", "prev": "{a}", "layer": 2, "span": {{"from": 0, "count": 1}}}}"#)).unwrap(); + assert!(read(zero).is_ok()); + // The checks that need the base: a span past the base, runs not summing to it. + let r = read(json!({"base": a, "prev": a, "layer": 2, "span": {"from": 3, "count": 2}})).unwrap(); + assert!(r.check_units(4).is_err() && r.check_units(5).is_ok()); + let r = read(json!({"base": a, "prev": [{"producer": a, "count": 2}, {"producer": b, "count": 1}], "layer": 2})).unwrap(); + assert!(r.check_units(4).is_err() && r.check_units(3).is_ok()); + // A component id is a reference in a WAI2 entry, and a hash is not required there. + assert!(Refines::from_value(&json!({"base": "base", "prev": "base", "layer": 1}), RefForm::ComponentId).is_ok()); + assert!(Refines::from_value(&json!({"base": "Base", "prev": "Base", "layer": 1}), RefForm::ComponentId).is_err()); +} + +/// The WAI1 role rules (SPEC §3, §8) and dispatch (SPEC §4): a refinement-only +/// capability carries well-formed `refines` and no fallback, is never a fallback, and +/// is never dispatched; `refines` sits only on a refinement-only capability. +#[test] +fn refinement_roles_v1_and_dispatch() { + use crate::container::RoleError; + let a = "a".repeat(64); + let good = json!({"base": a, "prev": a, "layer": 1}); + let mk = |cap: &str, fb: Option<&str>, refines: Option| { + let mut mr = ModelRequirement::new(cap); + if let Some(f) = fb { + mr = mr.with_fallback(f); + } + let mut m = Manifest::new("video", "replicate", mr, Conditioning::new("x")); + m.refines = refines; + Wai::new(m, vec![1, 2, 3]) + }; + assert!(mk(CAP_VIDEO_INT_REFINE, None, Some(good.clone())).check_component_roles().is_ok()); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, None, None).check_component_roles(), Err(RoleError::RefinementWithoutRefines { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, None, Some(json!({"base": a}))).check_component_roles(), Err(RoleError::RefinesMalformed { index: None, .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_REFINE, Some(CAP_VIDEO_INT_MOTION), Some(good.clone())).check_component_roles(), Err(RoleError::RefinementWithFallback { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_MOTION, Some(CAP_VIDEO_INT_REFINE), None).check_component_roles(), Err(RoleError::RefinementOnlyAsFallback { .. }))); + assert!(matches!(mk(CAP_VIDEO_INT_MOTION, None, Some(good.clone())).check_component_roles(), Err(RoleError::RefinesWithoutRefinement { .. }))); + // Dispatch: never taken, at a sink that advertises everything, and step 5 is not + // taken either. + let w = mk(CAP_VIDEO_INT_REFINE, Some(CAP_VIDEO_INT_MOTION), Some(good)); + assert_eq!(w.dispatch_verified(|_| true, crate::container::no_parameter_sets()).unwrap_err().code(), "missing_capability"); + // A manifest round-trips its refines through the envelope bytes. + let bytes = mk(CAP_VIDEO_INT_REFINE, None, Some(json!({"base": a, "prev": a, "layer": 1}))).to_bytes().unwrap(); + let back = Wai::from_bytes(&bytes).unwrap(); + assert_eq!(back.refines().unwrap().unwrap().layer, 1); + // A malformed refines still parses. + let bytes = mk(CAP_VIDEO_INT_REFINE, None, Some(json!("not an object"))).to_bytes().unwrap(); + assert!(Wai::from_bytes(&bytes).unwrap().refines().unwrap().is_err()); +} + +fn codes(r: &OfferReport) -> Vec<(u32, &'static str)> { + r.refused.iter().map(|(u, w)| (*u, w.code())).collect() +} + +/// Test 2: one fixture per §4.2 reason, each failing exactly its step; and with two +/// faults, the earlier step is the reason. +#[test] +fn offer_order_of_checks() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let env_reason = |c: &mut StageChain, env: &[u8], ev: ReceiptEvidence| c.offer(env, 0, ev).unwrap()[0].envelope.map(|r| r.code()); + let fresh = |info: BaseInfo, cfg: ChainConfig| StageChain::new(info, units.clone(), cfg); + let dv = ReceiptEvidence::DeliveredVerified; + // 1: refines malformed; capability not refinement. + let mut bad_refines = Wai::from_bytes(&l1).unwrap(); + bad_refines.manifest.refines = Some(json!({"base": hexs(&b), "prev": hexs(&b), "layer": 0})); + let bad_refines = bad_refines.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &bad_refines, dv), Some("refines-malformed")); + let mut not_ref = Wai::from_bytes(&l1).unwrap(); + not_ref.manifest.model_requirement.capability = CAP_VIDEO_INT_MOTION.into(); + let not_ref = not_ref.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), ¬_ref, dv), Some("capability-not-refinement")); + // 2: another base. + let other = layer(&[7; 32], Prev::One(hexs(&[7; 32])), 1, None, &below, 50); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &other, dv), Some("base-absent")); + // 2: a span past the base's units. + let past = layer(&b, one(&b), 1, Some(Span { from: 3, count: 2 }), &below[..2], 50); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &past, dv), Some("refines-malformed")); + // 3: the base came through a fallback. + let fb = BaseInfo { via_fallback: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(fb, cfg()), &l1, dv), Some("base-via-fallback")); + // 4: not admitted. + let jpeg = BaseInfo { capability: CAP_IMAGE_JPEG.into(), ..info.clone() }; + assert_eq!(env_reason(&mut fresh(jpeg.clone(), cfg()), &l1, dv), Some("base-not-admitted")); + // 5: attested base, layer verified by inclusion only (or not at all). + let att = BaseInfo { attested: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(att.clone(), cfg()), &l1, ReceiptEvidence::InclusionVerified), Some("refinement-unattested")); + assert_eq!(env_reason(&mut fresh(att.clone(), cfg()), &l1, ReceiptEvidence::None), Some("refinement-unattested")); + assert_eq!(env_reason(&mut fresh(att, cfg()), &l1, dv), None); + // 6: a malformed payload, a cost past the limit. + let mut bad_payload = Wai::from_bytes(&l1).unwrap(); + bad_payload.payload[44] = 1; // the table's first entry + let bad_payload = bad_payload.to_bytes().unwrap(); + let r = &fresh(info.clone(), cfg()).offer(&bad_payload, 0, dv).unwrap()[0]; + assert_eq!((r.envelope.map(|x| x.code()), r.detail), (Some("payload-malformed"), Some("cdf_malformed"))); + let tight = ChainConfig { cost_limit: Some(super::wir1::Wir1Cost { output_bytes: 10, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }; + let r = &fresh(info.clone(), tight).offer(&l1, 0, dv).unwrap()[0]; + assert_eq!((r.envelope.map(|x| x.code()), r.detail), (Some("cost-over-limit"), Some("output bytes"))); + // 8: prev-mismatch: the same L1 again, to units already at stage 1. + let mut c = fresh(info.clone(), cfg()); + assert_eq!(c.offer(&l1, 0, dv).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(codes(&c.offer(&l1, 0, dv).unwrap()[0]), vec![(0, "prev-mismatch"), (1, "prev-mismatch"), (2, "prev-mismatch"), (3, "prev-mismatch")]); + // 8: prev-mismatch: an L2 whose prev names another producer than the units have. + let wrong = layer(&b, Prev::One(hexs(&[9; 32])), 2, None, &below, 61); + assert_eq!(codes(&c.offer(&wrong, 0, dv).unwrap()[0]), vec![(0, "prev-mismatch"), (1, "prev-mismatch"), (2, "prev-mismatch"), (3, "prev-mismatch")]); + // 7: a different L1 for units that passed step 8 under L1: conflict, frozen. + let l1b = layer(&b, one(&b), 1, None, &below, 60); + assert_eq!(codes(&c.offer(&l1b, 0, dv).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")]); + assert!((0..4).all(|u| c.unit(u).unwrap().frozen)); + // 9: geometry: a layer coded for 2×2 frames over 4×6 frames. + let small: Vec> = (0..4).map(|f| picture(2, 2, f)).collect(); + let sb: Vec = small.iter().map(|v| Below::U8(v)).collect(); + let (pl, _) = encode(UnitKind::Rgb8, &sb, (2, 2, 3), &sb, (2, 2, 3), ¶ms(3, Predictor::Identity, 9, false)).unwrap(); + let geo = envelope(CAP_VIDEO_INT_REFINE, Some(Refines { base: hexs(&b), prev: one(&b), layer: 1, span: None }.to_value()), pl); + assert_eq!(codes(&fresh(info.clone(), cfg()).offer(&geo, 0, dv).unwrap()[0]), vec![(0, "geometry-mismatch"), (1, "geometry-mismatch"), (2, "geometry-mismatch"), (3, "geometry-mismatch")]); + // 10: deadline: units presented at 100, 140, 180, 220 with a 50 ms lead; arrival at 100. + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 100, unit_ms: 40, target_latency_ms: 0 }, lead_ms: vec![50] }), ..cfg() }; + let r = &fresh(info.clone(), live).offer(&l1, 100, dv).unwrap()[0]; + assert_eq!(codes(r), vec![(0, "deadline-missed"), (1, "deadline-missed")]); + assert_eq!(r.applied, vec![2, 3]); + // Two faults: the earlier step is the reason. + let mut two = Wai::from_bytes(&bad_payload).unwrap(); + two.manifest.refines = Some(Refines { base: hexs(&[7; 32]), prev: Prev::One(hexs(&[7; 32])), layer: 1, span: None }.to_value()); + let two = two.to_bytes().unwrap(); + assert_eq!(env_reason(&mut fresh(info.clone(), cfg()), &two, dv), Some("base-absent"), "step 2 before step 6"); + assert_eq!(env_reason(&mut fresh(jpeg, cfg()), &bad_payload, dv), Some("base-not-admitted"), "step 4 before step 6"); + let fb_att = BaseInfo { via_fallback: true, attested: true, ..info.clone() }; + assert_eq!(env_reason(&mut fresh(fb_att, cfg()), &l1, ReceiptEvidence::None), Some("base-via-fallback"), "step 3 before step 5"); + for r in StageRefusal::ALL { + assert_eq!(StageRefusal::from_code(r.code()), Some(r)); + } +} + +/// Test 3: per-frame layers 1 (span f,1; prev the base) apply, one unit each. +#[test] +fn per_frame_layers_do_not_conflict() { + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(info, units, cfg()); + for f in 0..4u32 { + let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + let r = &c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.applied.clone(), r.refused.len()), (None, vec![f], 0), "frame {f}"); + assert_eq!(c.unit(f).unwrap().producer, h(&l)); + } + assert!((0..4).all(|f| c.unit(f).unwrap().stage == 1 && !c.unit(f).unwrap().frozen)); +} + +/// Per-frame layers 1 over the base: the four frames after them, and their hashes. +fn per_frame_l1s(b: &[u8; 32], c: &mut StageChain) -> (Vec>, Vec<[u8; 32]>) { + let mut hs = Vec::new(); + for f in 0..4u32 { + let l = layer(b, one(b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + c.offer(&l, 0, ReceiptEvidence::None).unwrap(); + hs.push(h(&l)); + } + ((0..4).map(|f| unit_frame(c, f)).collect(), hs) +} + +fn runs(hs: &[[u8; 32]]) -> Prev { + Prev::Runs(hs.iter().map(|x| PrevRun { producer: hexs(x), count: 1 }).collect()) +} + +/// Test 4: a whole-GOP layer 2 whose prev is the run form over four per-frame +/// producers applies to all four units. +#[test] +fn whole_gop_l2_over_per_frame_l1_runs() { + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(info, units, cfg()); + let (frames, hs) = per_frame_l1s(&b, &mut c); + let l2 = layer(&b, runs(&hs), 2, None, &frames, 90); + let r = &c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.applied.clone()), (None, vec![0, 1, 2, 3])); + assert!((0..4).all(|f| c.unit(f).unwrap().stage == 2 && c.unit(f).unwrap().producer == h(&l2))); +} + +/// Test 5: layer 1 for frame 2 missed its deadline; a run-form layer 2 applies to +/// frames 0, 1 and 3, and frame 2, still at stage 0, holds it until the bound +/// (`layer-gap`). +#[test] +fn mixed_stage_span_applies_ready_units() { + let (_, info, units) = base4(); + let b = info.content_hash; + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 1000 }, lead_ms: vec![10, 10] }), ..cfg() }; + let mut c = StageChain::new(info, units, live); + let mut hs = Vec::new(); + for f in 0..4u32 { + let l = layer(&b, one(&b), 1, Some(Span { from: f, count: 1 }), &[frame(f)], 70 + f); + // Frame 2 is presented at 1200; its layer 1 arrives at 1195, inside the lead. + let at = if f == 2 { 1195 } else { 0 }; + let r = &c.offer(&l, at, ReceiptEvidence::None).unwrap()[0]; + if f == 2 { + assert_eq!(codes(r), vec![(2, "deadline-missed")]); + } + hs.push(h(&l)); + } + let frames: Vec> = (0..4).map(|f| unit_frame(&c, f)).collect(); + let l2 = layer(&b, runs(&hs), 2, None, &frames, 90); + let r = &c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.applied.clone(), r.held.clone()), (vec![0, 1, 3], vec![2])); + assert_eq!(c.unit(2).unwrap().stage, 0); + let e = c.expire(2000); + assert_eq!(e.len(), 1); + assert_eq!(codes(&e[0]), vec![(2, "layer-gap")]); +} + +/// Test 6: layer 2 offered before layer 1 reaches the same stage hashes as in order. +#[test] +fn out_of_order_equals_in_order() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut inorder = StageChain::new(info.clone(), units.clone(), cfg()); + inorder.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&inorder, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + inorder.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + let mut ooo = StageChain::new(info, units, cfg()); + let r = &ooo.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!(r.held, vec![0, 1, 2, 3]); + let rs = ooo.offer(&l1, 1, ReceiptEvidence::None).unwrap(); + assert_eq!(rs[0].applied, vec![0, 1, 2, 3]); + assert_eq!((rs[1].content_hash, rs[1].applied.clone()), (h(&l2), vec![0, 1, 2, 3]), "the release reports layer 2"); + assert_eq!(ooo.clip_sha256(), inorder.clip_sha256()); + assert!(ooo.clip_sha256().is_some()); + for u in 0..4 { + assert_eq!(ooo.unit(u), inorder.unit(u)); + } +} + +/// Test 7: a held layer 2 is discarded per unit at the bound (`layer-gap`), and a +/// layer that would exceed the bound's count is refused on arrival. +#[test] +fn bound_expires_to_layer_gap() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 60); + let mut c = StageChain::new(info.clone(), units.clone(), cfg()); + assert_eq!(c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + assert!(c.expire(1000).is_empty(), "held to the bound, not past it"); + let e = c.expire(1001); + assert_eq!(codes(&e[0]), vec![(0, "layer-gap"), (1, "layer-gap"), (2, "layer-gap"), (3, "layer-gap")]); + assert!(c.held().is_empty()); + // Layer 1 later finds no held layer 2. + assert_eq!(c.offer(&l1, 1002, ReceiptEvidence::None).unwrap().len(), 1); + // A bound of one envelope: a second held layer is refused on arrival. + let one_env = ChainConfig { bound: HoldBound { max_envelopes: 1, max_age_ms: 1000 }, ..cfg() }; + let mut c = StageChain::new(info, units, one_env); + let l2b = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 61); + assert_eq!(c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + assert_eq!(codes(&c.offer(&l2b, 0, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-gap"), (1, "layer-gap"), (2, "layer-gap"), (3, "layer-gap")]); +} + +/// Test 8: two layers 1 for one unit, both held for the base and released together: +/// neither is applied and the unit is frozen. One applied, then the other: the second +/// is refused, the unit frozen at stage 1, and no layer 2 applies to it. +#[test] +fn conflict_both_orders() { + let (_, info, units) = base4(); + let b = info.content_hash; + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + assert_ne!(h(&la), h(&lb)); + // Both held for the base. + let mut sink = StageSink::new(cfg()); + assert!(sink.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert!(sink.offer(&lb, 1, ReceiptEvidence::None).unwrap()[0].held_for_base); + let rs = sink.add_base(info.clone(), units.clone()); + assert_eq!(rs.len(), 2); + assert_eq!((rs[0].content_hash, codes(&rs[0])), (h(&la), vec![(1, "layer-conflict")])); + assert_eq!((rs[1].content_hash, codes(&rs[1])), (h(&lb), vec![(1, "layer-conflict")])); + let c = sink.chain(&b).unwrap(); + assert_eq!((c.unit(1).unwrap().stage, c.unit(1).unwrap().frozen), (0, true)); + // The other order gives the same: the batch does not depend on arrival. + let mut sink2 = StageSink::new(cfg()); + sink2.offer(&lb, 0, ReceiptEvidence::None).unwrap(); + sink2.offer(&la, 1, ReceiptEvidence::None).unwrap(); + sink2.add_base(info.clone(), units.clone()); + assert_eq!(sink2.chain(&b).unwrap().units(), c.units()); + // Applied first, then the other. + let mut c = StageChain::new(info, units, cfg()); + assert_eq!(c.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![1]); + let applied = c.unit(1).unwrap().clone(); + assert_eq!(codes(&c.offer(&lb, 0, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + let u1 = c.unit(1).unwrap(); + assert!(u1.frozen && u1.stage == 1 && u1.producer == h(&la) && u1.buf == applied.buf, "frozen at the applied layer"); + let l2 = layer(&b, Prev::One(hexs(&h(&la))), 2, Some(Span { from: 1, count: 1 }), &[unit_frame(&c, 1)], 85); + assert_eq!(codes(&c.offer(&l2, 0, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + assert_eq!(c.unit(1).unwrap().stage, 1); +} + +/// Test 9: a base decoded through a fallback refuses every layer. +#[test] +fn fallback_base_refuses_layers() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { via_fallback: true, ..info }, units, cfg()); + let r = &c.offer(&l1, 0, ReceiptEvidence::DeliveredVerified).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::BaseViaFallback)); + assert!((0..4).all(|u| c.unit(u).unwrap().stage == 0)); +} + +/// Test 10: with an attested base, a layer whose receipt verified by inclusion only +/// is never applied (`refinement-unattested`). +#[test] +fn inclusion_evidence_never_applies() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { attested: true, ..info }, units, cfg()); + let r = &c.offer(&l1, 0, ReceiptEvidence::InclusionVerified).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::RefinementUnattested)); + assert!(r.applied.is_empty()); + assert_eq!(c.offer(&l1, 0, ReceiptEvidence::DeliveredVerified).unwrap()[0].applied, vec![0, 1, 2, 3]); +} + +/// Test 11: the tier table, and a stage hash only at decode-equivalence. +#[test] +fn compose_table() { + use DeterminismTier::*; + let de = Some(DecodeEquivalence); + let ec = Some(EntropyConsistency); + assert_eq!(compose(de, &[de, de], &[true, true]), de); + assert_eq!(compose(de, &[], &[]), de); + assert_eq!(compose(ec, &[], &[]), ec, "a base alone keeps its tier"); + assert_eq!(compose(ec, &[de], &[true]), None, "entropy-consistency does not pass through a residual step"); + assert_eq!(compose(ec, &[de], &[false]), ec, "a step on symbols keeps the weakest"); + assert_eq!(compose(None, &[de], &[true]), None, "an unpinned integer base"); + assert_eq!(compose(de, &[None], &[true]), None); + assert_eq!(compose(de, &[ec], &[false]), ec); + // Base tiers by class and pin. + use super::tier::base_tier; + use crate::codecs::{CAP_IMAGE_JPEGAI, CAP_NEURAL_INT_HYPER}; + use crate::container::Prior; + assert_eq!(base_tier(CAP_VIDEO_INT_MOTION, None), de); + assert_eq!(base_tier(CAP_NEURAL_INT_HYPER, None), None, "unpinned"); + let pin = Prior::new("x", "a".repeat(64), "int-rans/p16+int-hyper/i64").with_tier(crate::codecs::DeclaredTier::DecodeEquivalence); + assert_eq!(base_tier(CAP_NEURAL_INT_HYPER, Some(&pin)), de); + assert_eq!(base_tier(CAP_IMAGE_JPEGAI, None), ec); + assert_eq!(base_tier(CAP_IMAGE_JPEG, None), None); + // A chain over an entropy-consistency base: no stage hash, before or after a layer. + let (_, info, units) = base4(); + let b = info.content_hash; + let mut c = StageChain::new(BaseInfo { tier: ec, ..info }, units, cfg()); + assert!(c.unit_sha256(0).is_none() && c.clip_sha256().is_none()); + assert_eq!(c.tier(0), ec); + let below: Vec> = (0..4).map(frame).collect(); + c.offer(&layer(&b, one(&b), 1, None, &below, 50), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(c.tier(0), None); + assert!(c.unit_sha256(0).is_none()); +} + +/// Test 22: for random arrival traces, played through a live [`StageSink`], every +/// unit's stage at its presentation time is the run of layers 1, 2, … that arrived in +/// time, a layer that arrives after that is never applied, and the base is presented +/// whatever its layers do: the sink's stage for each unit is what [`present`] states +/// for the trace. +#[test] +fn base_never_waits() { + let clock = UnitClock { t0_ms: 40, unit_ms: 33, target_latency_ms: 100 }; + let mut x: u64 = 0x1234_5678_9abc_def1; + let mut next = |m: u64| { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + x % m + }; + let (_, info, units) = base4(); + let b = info.content_hash; + // Per unit, a chain of three single-unit layers (built in order on a chain with + // no schedule). + let mut scratch = StageChain::new(info.clone(), units.clone(), cfg()); + let mut envs: Vec>> = vec![vec![]; 4]; + for u in 0..4u32 { + let mut prev = b; + for l in 1..=3u8 { + let e = layer(&b, Prev::One(hexs(&prev)), l, Some(Span { from: u, count: 1 }), &[unit_frame(&scratch, u)], 100 * l as u32 + u); + assert_eq!(scratch.offer(&e, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![u]); + prev = h(&e); + envs[u as usize].push(e); + } + } + let mut checked = 0; + let (mut stages, mut missed) = ([0u32; 4], 0u32); + for _ in 0..150 { + let arrivals: Vec>> = + (0..3).map(|_| (0..4).map(|_| if next(4) == 0 { None } else { Some(next(400)) }).collect()).collect(); + let lead = vec![next(30), next(30), next(30)]; + let cfg_live = ChainConfig { live: Some(Live { clock, lead_ms: lead.clone() }), bound: HoldBound { max_envelopes: 64, max_age_ms: 10_000 }, ..cfg() }; + let mut sink = StageSink::new(cfg_live); + sink.add_base(info.clone(), units.clone()); + let mut events: Vec<(u64, usize, usize)> = Vec::new(); + for (l, per_unit) in arrivals.iter().enumerate() { + for (u, t) in per_unit.iter().enumerate() { + if let Some(t) = t { + events.push((*t, l, u)); + } + } + } + events.sort_unstable(); + let p = present(&clock, &[0; 4], &arrivals, &lead); + let mut at_due = [None; 4]; + for (t, l, u) in events { + // What the sink presents for each unit due by now, before this arrival. + for v in 0..4 { + if at_due[v].is_none() && presentation_ms(&clock, v as u64) < t { + at_due[v] = Some(sink.chain(&b).unwrap().unit(v as u32).unwrap().stage); + } + } + for r in sink.offer(&envs[u][l], t, ReceiptEvidence::None).unwrap() { + for (v, why) in &r.refused { + assert!(matches!(why, StageRefusal::DeadlineMissed | StageRefusal::PrevMismatch), "unit {v}: {why:?}"); + missed += (*why == StageRefusal::DeadlineMissed) as u32; + } + } + } + let c = sink.chain(&b).unwrap(); + for u in 0..4 { + let now = c.unit(u as u32).unwrap().stage; + assert_eq!(now, p[u].stage, "unit {u}: the sink's stage is the run of layers in time"); + if let Some(s) = at_due[u] { + assert_eq!(s, now, "unit {u}: nothing arriving after its presentation applied"); + } + assert!(p[u].stage <= p[u].expected); + stages[now as usize] += 1; + checked += 1; + } + } + assert_eq!(checked, 600); + assert!(missed > 0 && stages.iter().all(|&n| n > 0), "the traces reach every stage and miss deadlines: {stages:?}, {missed} missed"); +} + +/// A live schedule over a set split into bases: each base's units sit on the one clock +/// from its `first_unit`, so a layer for the second base's unit 0 is due when the +/// set's unit 4 is (§8). +#[test] +fn live_clock_per_base() { + let (_, info, units) = base4(); + let env2 = envelope(CAP_VIDEO_INT_MOTION, None, b"the second group of frames".to_vec()); + let info2 = BaseInfo { content_hash: h(&env2), first_unit: 4, ..info.clone() }; + let b2 = info2.content_hash; + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 0 }, lead_ms: vec![50] }), ..cfg() }; + let l = layer(&b2, one(&b2), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 70); + let mut sink = StageSink::new(live.clone()); + sink.add_base(info.clone(), units.clone()); + sink.add_base(info2.clone(), units.clone()); + // Unit 0 of the second base is presented at 400: in time at 150 and at 350. + assert_eq!(sink.offer(&l, 150, ReceiptEvidence::None).unwrap()[0].applied, vec![0]); + let mut late = StageSink::new(live.clone()); + late.add_base(info2.clone(), units.clone()); + assert_eq!(codes(&late.offer(&l, 351, ReceiptEvidence::None).unwrap()[0]), vec![(0, "deadline-missed")]); + // At position 0 the same layer at 150 would be late. + let mut first = StageSink::new(live); + first.add_base(BaseInfo { first_unit: 0, ..info2 }, units); + assert_eq!(codes(&first.offer(&l, 150, ReceiptEvidence::None).unwrap()[0]), vec![(0, "deadline-missed")]); +} + +/// Decoding a base the sink holds again keeps its units: stages, producers, frozen +/// units and what passed step 8 stay, and nothing is reported. +#[test] +fn readding_a_base_keeps_its_units() { + let (_, info, units) = base4(); + let b = info.content_hash; + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + assert_eq!(sink.offer(&la, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![1]); + assert_eq!(codes(&sink.offer(&lb, 1, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + let before = sink.chain(&b).unwrap().units().to_vec(); + assert!(sink.add_base(info.clone(), units.clone()).is_empty(), "no report"); + assert_eq!(sink.chain(&b).unwrap().units(), &before[..], "the units are kept"); + assert!(sink.chain(&b).unwrap().unit(1).unwrap().frozen); + assert_eq!(codes(&sink.offer(&lb, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + assert_eq!(sink.chain(&b).unwrap().unit(1).unwrap().producer, h(&la)); + // Dropped and decoded again: the record keeps unit 1 frozen, so the layer that + // conflicted, held for the base meanwhile, is refused again on release. + assert_eq!(sink.remove_base(&b), Some(vec![])); + assert!(sink.chain(&b).is_none()); + assert!(sink.offer(&lb, 3, ReceiptEvidence::None).unwrap()[0].held_for_base); + let r = sink.add_base(info.clone(), units.clone()); + assert_eq!(codes(&r[0]), vec![(1, "layer-conflict")]); + let u1 = sink.chain(&b).unwrap().unit(1).unwrap().clone(); + assert!(u1.frozen && u1.stage == 0 && u1.producers() == [h(&la)]); + // Forgotten as well: a base never decoded. + sink.remove_base(&b); + assert!(sink.forget_base(&b)); + assert!(sink.offer(&lb, 4, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert_eq!(sink.add_base(info, units)[0].applied, vec![1]); +} + +/// A base whose chain was dropped and that is decoded again replays its record (§4.4): +/// each unit starts at stage 0 and advances only along the producers it had, a layer +/// whose key a recorded stage holds with other content conflicts, and a layer held for +/// the dropped chain is reported `base-absent`. +#[test] +fn removed_base_keeps_its_record() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut probe = StageChain::new(info.clone(), units.clone(), cfg()); + probe.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&probe, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + let l1x = layer(&b, one(&b), 1, None, &below, 51); + let l3 = edit_refines(&l2, Refines { base: hexs(&b), prev: Prev::One(hexs(&[7; 32])), layer: 4, span: None }); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&l2, 1, ReceiptEvidence::None).unwrap(); + assert_eq!(sink.offer(&l3, 2, ReceiptEvidence::None).unwrap()[0].held, vec![0, 1, 2, 3]); + let before = sink.chain(&b).unwrap().units().to_vec(); + let state_before = sink.chain(&b).unwrap().state_bytes(); + // The held layer 4 is discarded with the chain. + let r = sink.remove_base(&b).unwrap(); + assert_eq!(r.len(), 1); + assert_eq!((r[0].content_hash, r[0].envelope), (h(&l3), Some(StageRefusal::BaseAbsent))); + assert_eq!(sink.remove_base(&b), None); + // Decoded again: stage 0, the record's producers counted in the state. + assert!(sink.add_base(info.clone(), units.clone()).is_empty()); + let c = sink.chain(&b).unwrap(); + assert!(c.units().iter().all(|u| u.stage == 0 && u.producers() == [h(&l1), h(&l2)])); + let unit_bytes = (H * W * 3) as u64; + assert_eq!(c.state_bytes(), 4 * unit_bytes + 4 * 2 * 32); + // Another layer 1 conflicts with the record and freezes the units. + let mut other = sink.clone(); + assert_eq!(codes(&other.offer(&l1x, 3, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")]); + assert!(other.chain(&b).unwrap().units().iter().all(|u| u.frozen && u.stage == 0)); + // The recorded layers replay to the same stage, and the state does not grow. + assert_eq!(sink.offer(&l1, 4, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.offer(&l2, 5, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.chain(&b).unwrap().units(), &before[..]); + assert_eq!(sink.chain(&b).unwrap().state_bytes(), 4 * unit_bytes + 4 * 2 * 32, "replayed stages add nothing"); + assert!(state_before > 4 * unit_bytes + 4 * 2 * 32, "layer 4 was retained before"); + // A unit frozen with nothing recorded (two layers 1 conflicting in one batch) stays + // frozen when the base is decoded again: its flag is part of its record. + let la = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 80); + let lb = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 81); + let mut sink = StageSink::new(cfg()); + sink.offer(&la, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&lb, 1, ReceiptEvidence::None).unwrap(); + sink.add_base(info.clone(), units.clone()); + let u1 = sink.chain(&b).unwrap().unit(1).unwrap().clone(); + assert!(u1.frozen && u1.producers().is_empty()); + sink.remove_base(&b); + sink.add_base(info.clone(), units.clone()); + assert_eq!(codes(&sink.offer(&la, 2, ReceiptEvidence::None).unwrap()[0]), vec![(1, "layer-conflict")]); + // A frozen unit with a record replays it, in order and no further, and stays frozen: + // it presents its base or a recorded producer's output, nothing else. + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let frozen_by = layer(&b, one(&b), 1, None, &below, 52); + assert_eq!(codes(&sink.offer(&frozen_by, 1, ReceiptEvidence::None).unwrap()[0]).len(), 4); + let at_stage_1 = sink.chain(&b).unwrap().units().to_vec(); + sink.remove_base(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().units().iter().all(|u| u.frozen && u.stage == 0)); + assert_eq!(codes(&sink.offer(&l2, 2, ReceiptEvidence::None).unwrap()[0]).len(), 4, "not in order"); + assert_eq!(sink.offer(&l1, 3, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3], "its recorded producer"); + assert_eq!(sink.chain(&b).unwrap().units(), &at_stage_1[..], "the frozen stage, presented again"); + assert_eq!(codes(&sink.offer(&l2, 4, ReceiptEvidence::None).unwrap()[0]), vec![(0, "layer-conflict"), (1, "layer-conflict"), (2, "layer-conflict"), (3, "layer-conflict")], "never past the record"); + assert_eq!(codes(&sink.offer(&frozen_by, 5, ReceiptEvidence::None).unwrap()[0]).len(), 4); + assert_eq!(codes(&sink.offer(&l1, 6, ReceiptEvidence::None).unwrap()[0]).len(), 4, "a frozen unit takes no layer it already holds"); +} + +/// What a sink keeps of the bases it does not hold stays within its record budget +/// (§4.4): a base with nothing recorded keeps nothing; past the budget the base dropped +/// longest ago is forgotten whole, and decoded again it starts with no history and +/// unattested, as after forget_base; an attestation mark is counted too. +#[test] +fn records_within_their_budget() { + let (_, info, units) = base4(); + let below: Vec> = (0..4).map(frame).collect(); + let base_i = |i: u8| { + let mut b = info.clone(); + b.content_hash[0] ^= i; + b + }; + // Nothing recorded: nothing kept. + let mut sink = StageSink::new(cfg()); + for i in 1..=20u8 { + let b = base_i(i); + sink.add_base(b.clone(), units.clone()); + sink.remove_base(&b.content_hash); + } + assert_eq!(sink.kept_bytes(), 0); + // One layer applied to every unit: 32 + 4·(8 + 32) bytes a base; room for two. + let one_base = 32 + 4 * (8 + 32); + let mut sink = StageSink::new(ChainConfig { record_budget: 2 * one_base, ..cfg() }); + for i in 1..=4u8 { + let b = base_i(i); + let h = b.content_hash; + sink.add_base(b, units.clone()); + assert_eq!(sink.offer(&layer(&h, one(&h), 1, None, &below, 50), 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + sink.remove_base(&h); + assert!(sink.kept_bytes() <= 2 * one_base, "{}", sink.kept_bytes()); + } + assert_eq!(sink.kept_bytes(), 2 * one_base, "bases 3 and 4 kept"); + // Base 1 was forgotten: a layer other than its old producer applies. + let b1 = base_i(1); + let h1 = b1.content_hash; + sink.add_base(b1, units.clone()); + assert!(sink.chain(&h1).unwrap().units().iter().all(|u| u.producers().is_empty())); + assert_eq!(sink.offer(&layer(&h1, one(&h1), 1, None, &below, 51), 1, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + // Base 4 kept its record: the same other layer conflicts. + let b4 = base_i(4); + let h4 = b4.content_hash; + sink.add_base(b4, units.clone()); + assert_eq!(codes(&sink.offer(&layer(&h4, one(&h4), 1, None, &below, 51), 2, ReceiptEvidence::None).unwrap()[0]).len(), 4); + // Attestation marks are kept within the budget too, and a forgotten one is gone. + let mut sink = StageSink::new(ChainConfig { record_budget: 3 * 32, ..cfg() }); + for i in 1..=5u8 { + sink.mark_attested(&base_i(i).content_hash); + assert!(sink.kept_bytes() <= 3 * 32); + } + sink.add_base(base_i(1), units.clone()); + assert!(!sink.chain(&base_i(1).content_hash).unwrap().base().attested, "marked longest ago: forgotten"); + sink.add_base(base_i(5), units.clone()); + assert!(sink.chain(&base_i(5).content_hash).unwrap().base().attested); +} + +fn edit_refines(env: &[u8], r: Refines) -> Vec { + let mut w = Wai::from_bytes(env).unwrap(); + w.manifest.refines = Some(r.to_value()); + w.to_bytes().unwrap() +} + +/// A base decoded before its delivery receipt is verified becomes attested when the +/// receipt is: decoded again with it, or marked. Step 5 then applies to every layer +/// that arrives after, and nothing makes an attested base unattested again (§10). +#[test] +fn late_attestation_applies_step_5() { + let (_, info, units) = base4(); + let b = info.content_hash; + let l0 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let l1 = layer(&b, one(&b), 1, Some(Span { from: 1, count: 1 }), &[frame(1)], 51); + let l2 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 1 }), &[frame(2)], 52); + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + assert_eq!(sink.offer(&l0, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0], "unattested: no receipt needed"); + assert!(sink.add_base(BaseInfo { attested: true, ..info.clone() }, units.clone()).is_empty()); + assert!(sink.chain(&b).unwrap().base().attested); + assert_eq!(sink.offer(&l1, 1, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + assert!(sink.add_base(info.clone(), units.clone()).is_empty()); + assert!(sink.chain(&b).unwrap().base().attested, "never back to unattested"); + assert_eq!(sink.offer(&l1, 2, ReceiptEvidence::DeliveredVerified).unwrap()[0].applied, vec![1]); + // Marked, without decoding again. + let mut sink = StageSink::new(cfg()); + sink.add_base(info.clone(), units.clone()); + sink.mark_attested(&b); + assert_eq!(sink.offer(&l2, 0, ReceiptEvidence::InclusionVerified).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + // Attestation belongs to the base hash: it survives a dropped chain and a decode + // that does not carry it, and a mark before the first decode holds. + let mut sink = StageSink::new(cfg()); + sink.add_base(BaseInfo { attested: true, ..info.clone() }, units.clone()); + sink.remove_base(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().base().attested, "dropped and decoded again"); + assert_eq!(sink.offer(&l0, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::RefinementUnattested)); + let mut sink = StageSink::new(cfg()); + sink.mark_attested(&b); + sink.add_base(info.clone(), units.clone()); + assert!(sink.chain(&b).unwrap().base().attested, "marked before it was decoded"); + // Forgotten: as never decoded, so unattested. + sink.remove_base(&b); + assert!(sink.forget_base(&b)); + sink.add_base(info, units); + assert!(!sink.chain(&b).unwrap().base().attested); +} + +/// A unit or a stage below whose stated shape's element count passes u64 is refused +/// (`geometry_mismatch`, `geometry-mismatch`), not wrapped. +#[test] +fn shape_products_are_checked() { + let below = picture(2, 2, 1); + let ok = fixture(); + let w = parse(&ok, UnitKind::Rgb8).unwrap(); + let huge = (u32::MAX, u32::MAX, u32::MAX); + assert_eq!(apply_unit(&w, &ok, 0, Below::U8(&below), huge).unwrap_err().code(), "geometry_mismatch"); + let (_, info, _) = base4(); + let b = info.content_hash; + let l1 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let units = vec![ + UnitBuf { shape: huge, samples: Samples::U8(vec![0; 3]) }, + UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(1)) }, + ]; + let mut c = StageChain::new(info, units, cfg()); + assert_eq!(codes(&c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0]), vec![(0, "geometry-mismatch")]); +} + +/// A layer whose manifest names a fallback is refused at step 1 +/// (`capability-not-refinement`): §3 requires `fallback` to be null. +#[test] +fn fallback_refinement_refused_at_step_1() { + let (_, info, units) = base4(); + let b = info.content_hash; + let ok = layer(&b, one(&b), 1, None, &(0..4).map(frame).collect::>(), 50); + let mut w = Wai::from_bytes(&ok).unwrap(); + w.manifest.model_requirement = ModelRequirement::new(CAP_VIDEO_INT_REFINE).with_fallback(CAP_VIDEO_INT_MOTION); + let fb = w.to_bytes().unwrap(); + let mut c = StageChain::new(info.clone(), units.clone(), cfg()); + let r = &c.offer(&fb, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!(r.envelope, Some(StageRefusal::CapabilityNotRefinement)); + assert!((0..4).all(|u| c.unit(u).unwrap().stage == 0)); + // Held for its base, it is refused at step 1 on arrival, not on release. + let mut sink = StageSink::new(cfg()); + assert_eq!(sink.offer(&fb, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::CapabilityNotRefinement)); + assert!(sink.add_base(info, units).is_empty()); +} + +/// A layer held for its base and refused when the base arrives has one report, its +/// refusal; the reports of a release are in the order the layers arrived. +#[test] +fn refused_on_release_reports_once() { + let (_, info, units) = base4(); + let b = info.content_hash; + let ok = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let mut w = Wai::from_bytes(&ok).unwrap(); + w.payload.truncate(w.payload.len() - 4); + let bad = w.to_bytes().unwrap(); + let ok2 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 1 }), &[frame(2)], 52); + let mut sink = StageSink::new(cfg()); + for (t, e) in [&bad, &ok2].iter().enumerate() { + assert!(sink.offer(e, t as u64, ReceiptEvidence::None).unwrap()[0].held_for_base); + } + let r = sink.add_base(info, units); + assert_eq!(r.len(), 2, "one report per layer: {r:?}"); + assert_eq!((r[0].content_hash, r[0].envelope, r[0].detail), (h(&bad), Some(StageRefusal::PayloadMalformed), Some("stream_length"))); + assert_eq!((r[1].content_hash, r[1].applied.clone()), (h(&ok2), vec![2])); +} + +/// A layer held for its base and offered again with stronger evidence keeps the +/// stronger evidence, and counts as arrived for a live deadline when the sink verified +/// it (§8, §10). +#[test] +fn stronger_evidence_upgrades_a_held_layer() { + let (_, info, units) = base4(); + let b = info.content_hash; + let att = BaseInfo { attested: true, ..info }; + let l1 = layer(&b, one(&b), 1, Some(Span { from: 0, count: 1 }), &[frame(0)], 50); + let mut sink = StageSink::new(cfg()); + assert!(sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].held_for_base); + assert!(sink.offer(&l1, 3, ReceiptEvidence::DeliveredVerified).unwrap()[0].held_for_base); + assert!(sink.offer(&l1, 4, ReceiptEvidence::InclusionVerified).unwrap()[0].held_for_base, "weaker evidence keeps the stronger"); + assert_eq!(sink.add_base(att.clone(), units.clone())[0].applied, vec![0]); + // Without the upgrade: refinement-unattested. + let mut sink = StageSink::new(cfg()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(sink.add_base(att.clone(), units.clone())[0].envelope, Some(StageRefusal::RefinementUnattested)); + // Unit 0 is presented at 100 with lead 96: arrived at 0 it was in time, but it was + // verified at 5, which is late for an attested base. + let live = ChainConfig { live: Some(Live { clock: UnitClock { t0_ms: 0, unit_ms: 100, target_latency_ms: 100 }, lead_ms: vec![96] }), ..cfg() }; + let mut sink = StageSink::new(live.clone()); + sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + sink.offer(&l1, 5, ReceiptEvidence::DeliveredVerified).unwrap(); + assert_eq!(codes(&sink.add_base(att.clone(), units.clone())[0]), vec![(0, "deadline-missed")]); + let mut sink = StageSink::new(live); + sink.offer(&l1, 4, ReceiptEvidence::DeliveredVerified).unwrap(); + assert_eq!(sink.add_base(att, units)[0].applied, vec![0]); +} + +/// The chain budget (§4.5): a layer that would take the chain's state past it is +/// refused at step 6 (`chain-over-limit`), after the cost limit and before the +/// streams; what a chain retains is counted until no unit is held for it. +#[test] +fn chain_budget() { + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let p = Wai::from_bytes(&l1).unwrap().payload; + let cost = parse(&p, UnitKind::Rgb8).unwrap().declared_cost(); + let units_bytes = 4 * (H * W * 3) as u64; + let need = units_bytes + p.len() as u64 + cost.output_bytes + cost.working_bytes + 4 * 32; + let mut c = StageChain::new(info.clone(), units.clone(), ChainConfig { budget: need - 1, ..cfg() }); + assert_eq!(c.state_bytes(), units_bytes); + let r = &c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.envelope, r.detail), (Some(StageRefusal::ChainOverLimit), None)); + assert!(StageRefusal::ChainOverLimit.is_envelope_scoped()); + assert_eq!(StageRefusal::from_code("chain-over-limit"), Some(StageRefusal::ChainOverLimit)); + // The cost limit is checked first; a malformed stream after the budget. + let tight = ChainConfig { budget: need - 1, cost_limit: Some(super::wir1::Wir1Cost { output_bytes: 1, symbols: u64::MAX, working_bytes: u64::MAX }), ..cfg() }; + assert_eq!(StageChain::new(info.clone(), units.clone(), tight).offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::CostOverLimit)); + let mut w = Wai::from_bytes(&l1).unwrap(); + let n = w.payload.len(); + w.payload[n - 4..].copy_from_slice(&[0xff; 4]); + let damaged = w.to_bytes().unwrap(); + assert_eq!(StageChain::new(info.clone(), units.clone(), ChainConfig { budget: need - 1, ..cfg() }).offer(&damaged, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::ChainOverLimit)); + // At the budget it applies, and the state is the new output and 32 bytes a stage. + let mut c = StageChain::new(info, units, ChainConfig { budget: need, ..cfg() }); + assert_eq!(c.offer(&l1, 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); + assert_eq!(c.state_bytes(), units_bytes + 4 * 32); + assert_eq!(c.unit(0).unwrap().producers(), &[h(&l1)]); + // Tiny units: a 1×1 frame holds 3 bytes and each stage 32. Admitted at the budget, + // the layer leaves the chain within it once applied. + let (_, info, _) = base4(); + let b = info.content_hash; + let tiny: Vec = (0..64u32).map(|f| UnitBuf { shape: (1, 1, 3), samples: Samples::U8(vec![f as u8; 3]) }).collect(); + let belows: Vec> = (0..64u32).map(|f| vec![f as u8; 3]).collect(); + let targets: Vec> = (0..64u32).map(|f| vec![(f * 3) as u8, 7, 200]).collect(); + let bb: Vec = belows.iter().map(|v| Below::U8(v)).collect(); + let tt: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let (payload, _) = encode(UnitKind::Rgb8, &bb, (1, 1, 3), &tt, (1, 1, 3), ¶ms(1, Predictor::Identity, 31, false)).unwrap(); + let cost = parse(&payload, UnitKind::Rgb8).unwrap().declared_cost(); + let need = 64 * 3 + payload.len() as u64 + cost.output_bytes + cost.working_bytes + 64 * 32; + let l = envelope(CAP_VIDEO_INT_REFINE, Some(Refines { base: hexs(&b), prev: one(&b), layer: 1, span: None }.to_value()), payload); + let mut c = StageChain::new(info.clone(), tiny.clone(), ChainConfig { budget: need - 1, ..cfg() }); + assert_eq!(c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0].envelope, Some(StageRefusal::ChainOverLimit)); + let mut c = StageChain::new(info, tiny, ChainConfig { budget: need, ..cfg() }); + assert_eq!(c.offer(&l, 0, ReceiptEvidence::None).unwrap()[0].applied.len(), 64); + assert!(c.state_bytes() <= need, "the state {} passed the budget {need}", c.state_bytes()); + // A layer applied to some of its units and held for the others stays retained: the + // units it was applied to take their share out of its reserve, so the state stays + // within the budget it was admitted at, to the byte. + let (_, info, units) = base4(); + let b = info.content_hash; + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, Some(Span { from: 2, count: 2 }), &below[2..], 50); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &below, 60); + let mut probe = StageChain::new(info.clone(), units.clone(), cfg()); + probe.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let p2 = Wai::from_bytes(&l2).unwrap().payload; + let c2 = parse(&p2, UnitKind::Rgb8).unwrap().declared_cost(); + let budget = probe.state_bytes() + p2.len() as u64 + c2.output_bytes + c2.working_bytes + 4 * 32; + let mut c = StageChain::new(info, units, ChainConfig { budget, ..cfg() }); + c.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let r = &c.offer(&l2, 1, ReceiptEvidence::None).unwrap()[0]; + assert_eq!((r.applied.clone(), r.held.clone()), (vec![2, 3], vec![0, 1])); + assert!(c.state_bytes() <= budget, "half applied: the state {} passed the budget {budget}", c.state_bytes()); + let unit = (H * W * 3) as u64; + assert_eq!(c.state_bytes(), budget - 2 * unit, "each applied unit's old output left the state"); +} + +// ───────────────────────── WAI2 identity (SPEC §7.1) ───────────────────────── + +use crate::container::{Rendition, RenditionMeta, RoleError, WaiMulti}; + +/// A small chain: a base envelope, its frames, layer 1 and layer 2. +fn chain3() -> (Vec, Vec, Vec, Vec) { + let base = envelope(CAP_VIDEO_INT_MOTION, None, b"base payload".to_vec()); + let b = h(&base); + let units: Vec = (0..4).map(|f| UnitBuf { shape: (H, W, 3), samples: Samples::U8(frame(f)) }).collect(); + let below: Vec> = (0..4).map(frame).collect(); + let l1 = layer(&b, one(&b), 1, None, &below, 50); + let mut c = StageChain::new(BaseInfo { content_hash: b, capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false, first_unit: 0 }, units.clone(), cfg()); + c.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + let mid: Vec> = (0..4).map(|f| unit_frame(&c, f)).collect(); + let l2 = layer(&b, Prev::One(hexs(&h(&l1))), 2, None, &mid, 60); + (base, units, l1, l2) +} + +/// Test 19: a WAI1 chain packed into a WAI2 explodes back to the same bytes and +/// hashes; the packed WAI2's roles and envelope manifests check; and an explode +/// without `envelope_manifest` writes the manifest's members exactly as they are in +/// the WAI2 (a prior, and a target holding `1.0e0`), with ids turned into the content +/// hashes of the exploded envelopes, which a stage chain then applies. +#[test] +fn explode_pack_roundtrip() { + let (base, units, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let back = WaiMulti::explode_all(&packed).unwrap(); + assert_eq!(back, vec![base.clone(), l1.clone(), l2.clone()], "WAI1 → WAI2 → WAI1 keeps every byte"); + let w = WaiMulti::from_bytes(&packed).unwrap(); + assert_eq!(w.check_component_roles(), Ok(())); + assert_eq!(WaiMulti::check_envelope_manifests(&packed), Ok(())); + assert_eq!(w.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), vec![1, 2]); + assert_eq!(w.manifest.renditions[2].refines.as_ref().unwrap()["base"], "c0"); + assert_eq!(w.manifest.renditions[2].refines.as_ref().unwrap()["prev"], "c1"); + // An envelope_manifest that does not match its entry. + let mut w2 = WaiMulti::from_bytes(&packed).unwrap(); + w2.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into(); + w2.manifest.renditions[1].role = None; + w2.manifest.renditions[1].refines = None; + let mismatched = w2.to_bytes().unwrap(); + assert_eq!(WaiMulti::check_envelope_manifests(&mismatched), Err((1, "component-manifest-mismatch"))); + + // The synthesised form, with raw member text kept. + let pin = crate::container::Prior::new("p", "a".repeat(64), "int-rans/p16+int-motion/i64"); + let wm = WaiMulti::new("video", "replicate", json!({"dur": 1.0}), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base").with_prior(pin), b"base payload".to_vec()).with_payload_digest(), + ]); + let bytes = wm.to_bytes().unwrap(); + let text = String::from_utf8_lossy(&bytes[8..8 + u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize]).to_string(); + let edited = text.replace("\"dur\":1.0", "\"dur\":1.0e0"); + assert_ne!(edited, text); + let mut e = b"WAI2".to_vec(); + e.extend_from_slice(&(edited.len() as u32).to_le_bytes()); + e.extend_from_slice(edited.as_bytes()); + e.extend_from_slice(&bytes[8 + text.len()..]); + let ex = WaiMulti::explode(&e, 0).unwrap(); + let exm = String::from_utf8_lossy(&ex[8..8 + u32::from_le_bytes(ex[4..8].try_into().unwrap()) as usize]).to_string(); + assert!(exm.contains("\"target\":{\"dur\":1.0e0}"), "{exm}"); + assert!(exm.starts_with("{\"wai\":\"1.1\",\"media\":\"video\",\"intent\":\"replicate\",\"model_requirement\":{\"capability\":\"wai.video.int_motion\",\"fallback\":null},\"conditioning\":{\"kind\":\"int_motion\",\"prior\":{"), "{exm}"); + // A refinement synthesised from ids explodes to hashes a chain applies. + let base_ex = WaiMulti::explode(&e, 0).unwrap(); + let bh = h(&base_ex); + let below: Vec> = (0..4).map(frame).collect(); + let layer_env = layer(&bh, one(&bh), 1, None, &below, 50); + let payload = Wai::from_bytes(&layer_env).unwrap().payload; + let mut meta = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id("l1").with_role("refinement"); + meta.refines = Some(json!({"base": "base", "prev": "base", "layer": 1})); + let wm = WaiMulti::new("video", "replicate", json!(null), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), b"base payload".to_vec()).with_payload_digest(), + Rendition::from_meta(meta, payload).with_payload_digest(), + ]); + assert_eq!(wm.check_component_roles(), Ok(())); + let bytes = wm.to_bytes().unwrap(); + let all = WaiMulti::explode_all(&bytes).unwrap(); + let r = Wai::from_bytes(&all[1]).unwrap().refines().unwrap().unwrap(); + assert_eq!(r.base, hexs(&h(&all[0]))); + let mut c = StageChain::new(BaseInfo { content_hash: h(&all[0]), capability: CAP_VIDEO_INT_MOTION.into(), via_fallback: false, tier: Some(DeterminismTier::DecodeEquivalence), attested: false, first_unit: 0 }, units, cfg()); + assert_eq!(c.offer(&all[1], 0, ReceiptEvidence::None).unwrap()[0].applied, vec![0, 1, 2, 3]); +} + +/// Entry `i`'s exploded envelope is its own result: a refinement whose `prev` names +/// no entry fails alone, and the base and the other refinement explode as they would +/// without it. +#[test] +fn explode_each_entry_alone() { + let (base, _, l1, _) = chain3(); + let payload = |e: &[u8]| Wai::from_bytes(e).unwrap().payload; + let refinement = |id: &str, refines: serde_json::Value, p: Vec| { + let mut m = RenditionMeta::new(CAP_VIDEO_INT_REFINE, "wir1").with_id(id).with_role("refinement"); + m.refines = Some(refines); + Rendition::from_meta(m, p).with_payload_digest() + }; + let wm = WaiMulti::new("video", "replicate", json!(null), vec![ + Rendition::from_meta(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion").with_id("base"), payload(&base)).with_payload_digest(), + refinement("l2", json!({"base": "base", "prev": "zz", "layer": 2}), payload(&l1)), + refinement("l1", json!({"base": "base", "prev": "base", "layer": 1}), payload(&l1)), + ]); + let bytes = wm.to_bytes().unwrap(); + assert!(WaiMulti::explode_all(&bytes).is_err()); + let each = WaiMulti::explode_each(&bytes).unwrap(); + assert!(each[0].is_ok() && each[1].is_err() && each[2].is_ok()); + assert_eq!(WaiMulti::explode(&bytes, 0).unwrap(), *each[0].as_ref().unwrap()); + assert!(WaiMulti::explode(&bytes, 1).is_err()); + assert_eq!(WaiMulti::explode(&bytes, 2).unwrap(), *each[2].as_ref().unwrap()); + let r = Wai::from_bytes(each[2].as_ref().unwrap()).unwrap().refines().unwrap().unwrap(); + assert_eq!(r.base, hexs(&h(each[0].as_ref().unwrap()))); +} + +/// Pack refuses what it cannot give back: a manifest with bytes around its JSON value, +/// bytes after a payload, and a manifest nested so deep that, as a member of the WAI2 +/// manifest, it passes the reader's depth limit; what it returns explodes back to its +/// sources and passes the manifest check. +#[test] +fn pack_refuses_what_it_cannot_give_back() { + let (base, _, l1, l2) = chain3(); + let parts = |e: &[u8]| { + let ml = u32::from_le_bytes(e[4..8].try_into().unwrap()) as usize; + (e[8..8 + ml].to_vec(), e[8 + ml..].to_vec()) + }; + let rebuild = |m: &[u8], rest: &[u8]| { + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(m.len() as u32).to_le_bytes()); + out.extend_from_slice(m); + out.extend_from_slice(rest); + out + }; + let (m, rest) = parts(&base); + assert!(WaiMulti::pack(&[&base, &l1, &l2]).is_ok()); + for (what, env) in [ + ("a trailing newline", rebuild(&[&m[..], b"\n"].concat(), &rest)), + ("a leading space", rebuild(&[b" ", &m[..]].concat(), &rest)), + ("bytes after the payload", [&base[..], b"xyz"].concat()), + ] { + assert!(Wai::from_bytes(&env).is_ok(), "{what}: a WAI1 reader takes it"); + let e = WaiMulti::pack(&[&env]).unwrap_err().to_string(); + assert!(e.contains("bytes around") || e.contains("bytes after"), "{what}: {e}"); + } + // Depth: the WAI1 reader takes a manifest of depth 127; inside a WAI2 entry's + // envelope_manifest it sits three levels deeper. + for (depth, packs) in [(120usize, true), (124, false), (126, false)] { + let text = String::from_utf8(m.clone()).unwrap(); + let deep = format!("{},\"zz_deep\":{}{}}}", &text[..text.len() - 1], "[".repeat(depth), "]".repeat(depth)); + let env = rebuild(deep.as_bytes(), &rest); + assert!(Wai::from_bytes(&env).is_ok(), "depth {depth} is a WAI1"); + let got = WaiMulti::pack(&[&env]); + assert_eq!(got.is_ok(), packs, "depth {depth}: {:?}", got.as_ref().err()); + if let Ok(p) = got { + assert_eq!(WaiMulti::explode_all(&p).unwrap(), vec![env]); + } + } +} + +/// A refinement's envelope_manifest names no fallback (SPEC §7.1, staged-delivery §3). +#[test] +fn refinement_envelope_manifest_has_no_fallback() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let mut w = WaiMulti::from_bytes(&packed).unwrap(); + w.manifest.renditions[1].envelope_manifest.as_mut().unwrap()["model_requirement"]["fallback"] = json!(CAP_VIDEO_INT_MOTION); + assert_eq!(WaiMulti::check_envelope_manifests(&w.to_bytes().unwrap()), Err((1, "component-manifest-mismatch"))); + let mut w = WaiMulti::from_bytes(&WaiMulti::pack(&[&base]).unwrap()).unwrap(); + w.manifest.renditions[0].envelope_manifest.as_mut().unwrap()["model_requirement"]["fallback"] = json!(crate::codecs::CAP_IMAGE_PNG); + assert_eq!(WaiMulti::check_envelope_manifests(&w.to_bytes().unwrap()), Ok(()), "a base may name one"); +} + +/// A manifest that names a member twice in one object, at any depth, is malformed +/// (SPEC §2), in a WAI1 and in a WAI2. +#[test] +fn repeated_member_names_are_malformed() { + let (base, _, l1, l2) = chain3(); + let (ml, text) = { + let ml = u32::from_le_bytes(base[4..8].try_into().unwrap()) as usize; + (ml, String::from_utf8(base[8..8 + ml].to_vec()).unwrap()) + }; + let with = |t: &str| { + let mut out = b"WAI1".to_vec(); + out.extend_from_slice(&(t.len() as u32).to_le_bytes()); + out.extend_from_slice(t.as_bytes()); + out.extend_from_slice(&base[8 + ml..]); + out + }; + assert!(Wai::from_bytes(&with(&text)).is_ok()); + let inner = text.replacen("\"capability\":", "\"q\":1,\"q\":2,\"capability\":", 1); + assert_ne!(inner, text); + let deep = format!("{},\"zz\":[{{\"a\":1,\"b\":{{\"c\":1,\"\\u0063\":2}}}}]}}", &text[..text.len() - 1]); + let top = format!("{},\"zz\":1,\"zz\":2}}", &text[..text.len() - 1]); + for (what, t) in [("in model_requirement", inner), ("nested, one name escaped", deep), ("an undefined member", top)] { + assert!(Wai::from_bytes(&with(&t)).is_err(), "{what}"); + } + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let pml = u32::from_le_bytes(packed[4..8].try_into().unwrap()) as usize; + let ptext = String::from_utf8(packed[8..8 + pml].to_vec()).unwrap(); + let dup = ptext.replacen("\"kind\":", "\"zz\":1,\"zz\":1,\"kind\":", 1); + let mut p2 = b"WAI2".to_vec(); + p2.extend_from_slice(&(dup.len() as u32).to_le_bytes()); + p2.extend_from_slice(dup.as_bytes()); + p2.extend_from_slice(&packed[8 + pml..]); + assert!(WaiMulti::from_bytes(&p2).is_err()); +} + +/// The selection of the revision before refinement components: the first entry that +/// is not a companion, whose capability the sink advertises (that revision did not +/// register the refinement strings, so they were dispatchable), whose payload matches +/// its `sha256` when it states one, and whose pin, when it has one, the sink accepts. +fn primary_v1_1(w: &WaiMulti, advertises: impl Fn(&str) -> bool, pin_ok: impl Fn(&crate::container::Prior) -> bool) -> Option { + w.manifest.renditions.iter().enumerate().position(|(i, m)| { + m.role.as_deref() != Some("companion") + && advertises(&m.capability) + && m.sha256.as_deref().is_none_or(|d| d == crate::pin::sha256_hex(&w.renditions[i])) + && (!crate::container::pin_applies_to(&m.capability) || m.prior.as_ref().is_none_or(&pin_ok)) + }) +} + +/// Test 20: an earlier sink, picking by its own selection, picks the base of a +/// conforming WAI2 whose refinements follow it, whether it advertises only what it +/// knows or every capability; a WAI2 with a refinement first is non-conforming, and +/// there the earlier rule would pick the refinement. +#[test] +fn older_primary_returns_base() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let w = WaiMulti::from_bytes(&packed).unwrap(); + let knows = |c: &str| c != CAP_VIDEO_INT_REFINE; + assert_eq!(primary_v1_1(&w, knows, |_| true), Some(0)); + assert_eq!(primary_v1_1(&w, |_| true, |_| true), Some(0)); + assert_eq!(w.primary(crate::container::no_parameter_sets()).map(|p| p.index).ok(), Some(0)); + // The base skipped for its digest, or for its pin: an earlier sink that advertises + // what it knows picks nothing, as the current selection does; one that advertised + // every string would reach a refinement, find it undecodable, and leave it inert. + let mut bad_digest = WaiMulti::from_bytes(&packed).unwrap(); + bad_digest.renditions[0][0] ^= 1; + assert_eq!(primary_v1_1(&bad_digest, knows, |_| true), None); + assert!(bad_digest.primary(crate::container::no_parameter_sets()).is_err()); + assert_eq!(primary_v1_1(&bad_digest, |_| true, |_| true), Some(1)); + // A pin applies to a base decoded against a parameter set. + let mut pinned = WaiMulti::from_bytes(&packed).unwrap(); + pinned.manifest.renditions[0].capability = crate::codecs::CAP_VIDEO_INT_HYPER.into(); + pinned.manifest.renditions[0].prior = Some(crate::container::Prior::new("p", "a".repeat(64), "int-rans/p16+int-hyper/i64")); + assert_eq!(primary_v1_1(&pinned, knows, |_| true), Some(0)); + assert_eq!(primary_v1_1(&pinned, knows, |_| false), None, "a refused pin skips the base"); + let refused = pinned.pick(|c| knows(c), |_, _| Err::<(), _>(crate::container::PinRefusal::Malformed { why: "refused".into() })).map(|p| p.index); + assert!(refused.is_err(), "the current selection skips it too, and no refinement is selectable"); + let mut swapped = WaiMulti::from_bytes(&packed).unwrap(); + swapped.manifest.renditions.swap(0, 1); + swapped.renditions.swap(0, 1); + assert!(matches!(swapped.check_component_roles(), Err(RoleError::RefinementBaseMissing { .. }) | Err(RoleError::RefinementBeforeSelectable { .. }))); + assert_eq!(primary_v1_1(&swapped, |_| true, |_| true), Some(0), "the earlier rule picks the misplaced refinement"); + assert_eq!(swapped.manifest.renditions[0].capability, CAP_VIDEO_INT_REFINE); +} + +/// Test 21: every refinement role error, each from a conforming WAI2 changed in one +/// place. +#[test] +fn refinement_roles() { + let (base, _, l1, l2) = chain3(); + let packed = WaiMulti::pack(&[&base, &l1, &l2]).unwrap(); + let ok = || WaiMulti::from_bytes(&packed).unwrap(); + let err = |f: &dyn Fn(&mut WaiMulti)| { + let mut w = ok(); + f(&mut w); + w.check_component_roles().unwrap_err() + }; + assert!(matches!(err(&|w| w.manifest.renditions[0].refines = Some(json!({}))), RoleError::RefinesOnNonRefinement { index: 0 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].capability = CAP_VIDEO_INT_MOTION.into()), RoleError::RefinementNotRefinementOnly { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].id = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].sha256 = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[0].sha256 = None), RoleError::RefinementIdentityMissing { index: 1 })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = Some(json!({"base": "c0", "prev": "c0", "layer": 0}))), RoleError::RefinesMalformed { index: Some(1), .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = None), RoleError::RefinesMalformed { index: Some(1), .. })); + assert!(matches!(err(&|w| w.manifest.renditions[1].refines = Some(json!({"base": "nope", "prev": "nope", "layer": 1}))), RoleError::RefinementBaseMissing { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[0].capability = crate::codecs::CAP_IMAGE_PNG.into()), RoleError::RefinementBaseNotAdmitted { index: 1, .. })); + assert!(matches!(err(&|w| w.manifest.renditions[2].refines = Some(json!({"base": "c0", "prev": "c9", "layer": 2}))), RoleError::RefinementPrevMissing { index: 2, .. })); + assert!(matches!(err(&|w| { + w.manifest.renditions.push(RenditionMeta::new(CAP_VIDEO_INT_MOTION, "int_motion")); + w.renditions.push(vec![1]); + }), RoleError::RefinementBeforeSelectable { index: 1, selectable: 3 })); + assert!(matches!(err(&|w| w.manifest.renditions[2].role = None), RoleError::RefinesOnNonRefinement { index: 2 })); + assert!(matches!(err(&|w| { + w.manifest.renditions[2].role = None; + w.manifest.renditions[2].refines = None; + }), RoleError::RefinementOnlyAsComponent { index: 2, .. })); + // A refinement is never selected, and a refinement whose payload does not match + // its sha256 is not offered. + let w = ok(); + assert!(!w.manifest.renditions[1].is_selectable()); + let mut tampered = ok(); + tampered.renditions[2][50] ^= 1; + assert_eq!(tampered.refinements_of(0).iter().map(|(i, ..)| *i).collect::>(), vec![1]); +} diff --git a/wai/wai-rs/src/staged/tier.rs b/wai/wai-rs/src/staged/tier.rs new file mode 100644 index 000000000..e5ff9b264 --- /dev/null +++ b/wai/wai-rs/src/staged/tier.rs @@ -0,0 +1,64 @@ +//! The determinism tier of a stage (`extensions/staged-delivery.md` §5). +//! +//! A unit's tier is the weakest step of its chain: the base's decode, then each layer +//! applied to it. A layer acts on reconstructed output, so `entropy-consistency`, which +//! fixes only the entropy-decoded symbols, does not pass through it +//! (`extensions/derived-tracks.md` §3 rules 1 and 2). [`compose`] is a pure function, +//! so the rule is tested over every combination, whether or not any admitted base +//! holds a given tier. + +use crate::codecs::{capability_replicate_class, DeterminismTier, ReplicateClass}; +use crate::container::Prior; + +/// The tier of a chain: `base`, then each of `steps` in order, where +/// `on_reconstruction[i]` says whether step `i` acts on reconstructed output. The +/// weakest tier wins, and a step on reconstructed output over `entropy-consistency` +/// gives no tier. `None` is no tier. A step whose `on_reconstruction` flag is missing +/// counts as acting on reconstructed output. +pub fn compose(base: Option, steps: &[Option], on_reconstruction: &[bool]) -> Option { + let mut t = base; + for (i, step) in steps.iter().enumerate() { + let recon = on_reconstruction.get(i).copied().unwrap_or(true); + t = match (t, *step) { + (None, _) | (_, None) => None, + (Some(DeterminismTier::EntropyConsistency), Some(_)) if recon => None, + (Some(a), Some(b)) => Some(a.min(b)), + }; + } + t +} + +/// The tier of a base's step (§5): `decode-equivalence` when `cap`'s registry class is +/// `StandardDefined`, or `NeuralIntegerExact` with a pin that resolved (`pin` is the +/// pin a sink verified, `None` when the decode was unpinned) and declares +/// `decode-equivalence`; `entropy-consistency` when the class is `NeuralEntropyExact`; +/// otherwise none. +pub fn base_tier(cap: &str, pin: Option<&Prior>) -> Option { + match capability_replicate_class(cap)? { + ReplicateClass::StandardDefined => Some(DeterminismTier::DecodeEquivalence), + ReplicateClass::NeuralIntegerExact => { + pin.and_then(Prior::declared_tier).filter(|t| *t == DeterminismTier::DecodeEquivalence) + } + ReplicateClass::NeuralEntropyExact => Some(DeterminismTier::EntropyConsistency), + _ => None, + } +} + +/// The token a tier is written as: `decode-equivalence`, `entropy-consistency`, or +/// `none` for no tier. +pub fn token(t: Option) -> &'static str { + match t { + Some(DeterminismTier::DecodeEquivalence) => "decode-equivalence", + Some(DeterminismTier::EntropyConsistency) => "entropy-consistency", + None => "none", + } +} + +/// The tier a token names; `none` and anything else is no tier. +pub fn from_token(s: &str) -> Option { + match s { + "decode-equivalence" => Some(DeterminismTier::DecodeEquivalence), + "entropy-consistency" => Some(DeterminismTier::EntropyConsistency), + _ => None, + } +} diff --git a/wai/wai-rs/src/staged/wir1.rs b/wai/wai-rs/src/staged/wir1.rs new file mode 100644 index 000000000..4b3a04aa1 --- /dev/null +++ b/wai/wai-rs/src/staged/wir1.rs @@ -0,0 +1,729 @@ +//! `WIR1`, the residual refinement payload (`extensions/staged-delivery.md` §6.1, +//! Appendix A.1): a parser with a closed list of refusal codes in a stated order, the +//! cost a header declares before anything is decoded, the per-unit decode, and the +//! encoder the corpus generator and `wai stage refine` use. +//! +//! One rANS stream per unit, so units decode independently. For each element of a +//! unit, in raster order, the next stage is `clamp(pred + r·q)`: `pred` is the +//! predictor ([`super::predict`]) on the unit's current output, `r` the decoded +//! residual, `q` the step, and the clamp the element type's range +//! ([`super::wir1_arith::step`]). +//! +//! The parser and the decode follow `extensions/integer-payloads.md`'s discipline: +//! every size formed in `u64` with checked arithmetic and capped alike on every +//! target; nothing allocated beyond what the payload's bytes justify; a stream refused +//! at the first symbol during whose decode it runs out (`stream_exhausted`) and at an +//! end that is not canonical (`stream_noncanonical`). A refinement reads no parameter +//! set, so there is nothing for SPEC §3.1's check 5 to run over here; the base it +//! refines was checked when it was decoded. +//! +//! This file names only the rANS coder (`crate::rans`), the histogram table +//! (`crate::int_entropy::hist_to_cdf`) and its two siblings (`super::predict`, +//! `super::wir1_arith`), so the byte-equality crate includes it beside them. + +use super::predict::{rate_at, rate_len, rdiv, up2_at}; +use super::wir1_arith::{step, I16_RANGE, U8_RANGE}; +use crate::rans::{RansDecoder, RansEncoder}; + +/// cbindgen:ignore +pub const MAGIC: [u8; 4] = *b"WIR1"; +/// The one version this revision defines. +/// +/// cbindgen:ignore +pub const VERSION: u8 = 1; +/// The bytes before the tables. +/// +/// cbindgen:ignore +pub const HEADER: usize = 44; +/// The most elements one unit may hold (`d0·d1·d2`). +/// +/// cbindgen:ignore +pub const MAX_UNIT: u64 = 1 << 26; +/// The most units one payload may hold. +/// +/// cbindgen:ignore +pub const MAX_UNITS: u64 = 1 << 20; +/// The most elements one payload may hold (`n_units·d0·d1·d2`). +/// +/// cbindgen:ignore +pub const MAX_ELEMENTS: u64 = 1 << 30; +/// The largest step. +/// +/// cbindgen:ignore +pub const MAX_Q: u32 = 65_535; +/// The bounds on a table's length. +/// +/// cbindgen:ignore +pub const CDF_LEN_MIN: usize = 3; +/// cbindgen:ignore +pub const CDF_LEN_MAX: usize = 4097; + +/// Why a `WIR1` payload was refused: the closed list of staged-delivery Appendix A.1, +/// whose codes are those of `extensions/integer-payloads.md` §8 for the same faults. +/// The first rule that fails, in the order the appendix gives, is the reason. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +/// cbindgen:ignore +pub enum Wir1Refusal { + /// The payload does not start with `WIR1`. + BadMagic, + /// A version this revision does not define. + Version, + /// A reserved field that is not zero. + ReservedNonzero, + /// A header field outside its range, or not the one the unit's kind allows. + FieldRange, + /// A size past the caps. + TooLarge, + /// A table that breaks the table rules. + CdfMalformed, + /// The payload ends before a field or section it declares; or a stream length that + /// is not a multiple of 4, or is under 8. + StreamLength, + /// Bytes after the last stream. + Trailing, + /// A stream that ran out before its last symbol. + StreamExhausted, + /// A stream that does not end canonically after its last symbol. + StreamNoncanonical, + /// The unit's current output is not the shape the payload's predictor reads. + GeometryMismatch, +} + +impl Wir1Refusal { + /// Every refusal, in the order of the appendix. + /// + /// cbindgen:ignore + pub const ALL: [Wir1Refusal; 11] = [ + Self::BadMagic, Self::Version, Self::ReservedNonzero, Self::FieldRange, Self::TooLarge, Self::CdfMalformed, + Self::StreamLength, Self::Trailing, Self::StreamExhausted, Self::StreamNoncanonical, Self::GeometryMismatch, + ]; + + /// The code. + pub fn code(self) -> &'static str { + match self { + Self::BadMagic => "bad_magic", + Self::Version => "version", + Self::ReservedNonzero => "reserved_nonzero", + Self::FieldRange => "field_range", + Self::TooLarge => "too_large", + Self::CdfMalformed => "cdf_malformed", + Self::StreamLength => "stream_length", + Self::Trailing => "trailing", + Self::StreamExhausted => "stream_exhausted", + Self::StreamNoncanonical => "stream_noncanonical", + Self::GeometryMismatch => "geometry_mismatch", + } + } + + /// The refusal whose code is `code`. + pub fn from_code(code: &str) -> Option { + Self::ALL.into_iter().find(|r| r.code() == code) + } +} + +/// A refusal and what failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Wir1Error { + pub refusal: Wir1Refusal, + pub detail: &'static str, +} + +impl Wir1Error { + pub fn code(&self) -> &'static str { + self.refusal.code() + } +} + +impl std::fmt::Display for Wir1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.refusal.code(), self.detail) + } +} + +impl std::error::Error for Wir1Error {} + +const fn err(refusal: Wir1Refusal, detail: &'static str) -> Wir1Error { + Wir1Error { refusal, detail } +} + +/// What a unit's output is, which fixes the element type and the predictors a +/// payload may use: the unit kind of the refinement capability. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnitKind { + /// A picture or a frame, RGB8 (`wai.image.int_refine`, `wai.video.int_refine`): + /// `d0` rows, `d1` columns, `d2 = 3`; elements u8; identity or up2. + Rgb8, + /// PCM (`wai.audio.int_refine`): `d0` samples per channel, `d1 = 1`, `d2` + /// channels interleaved; elements i16; identity or rate. + Pcm, + /// A splat attribute tensor (`wai.splat.int_refine`): `d0` planes of `d1 × d2`; + /// elements u8; identity. + Attr, +} + +/// A payload's predictor. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Predictor { + /// The element at the same position. + Identity, + /// Twice the resolution (`h' = ⌈d0/2⌉`, `w' = ⌈d1/2⌉`). + Up2, + /// Another sample rate: `rate_num` output samples per `rate_den` source samples. + Rate, +} + +impl Predictor { + fn from_byte(b: u8) -> Option { + match b { + 0 => Some(Self::Identity), + 1 => Some(Self::Up2), + 2 => Some(Self::Rate), + _ => None, + } + } + /// The header byte. + pub fn byte(self) -> u8 { + match self { + Self::Identity => 0, + Self::Up2 => 1, + Self::Rate => 2, + } + } + /// The token `wai stage` and the corpus use. + pub fn token(self) -> &'static str { + match self { + Self::Identity => "identity", + Self::Up2 => "up2", + Self::Rate => "rate", + } + } +} + +/// A parsed `WIR1` payload: its header, its tables (flat, one after another) and +/// where each unit's stream lies in the payload. It holds no reference to the payload, +/// so a sink keeps it beside the payload's bytes; the streams are read from them +/// ([`Wir1::stream`]). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Wir1 { + pub kind: UnitKind, + pub predictor: Predictor, + /// One table for every unit (`false`), or one per unit (`true`). + pub table_per_unit: bool, + pub n_units: u32, + pub d0: u32, + pub d1: u32, + pub d2: u32, + pub rate_num: u32, + pub rate_den: u32, + pub q: u32, + pub offset: i32, + /// Each table's length. + pub cdf_len: usize, + /// One table, or one per unit, `cdf_len` entries each, one after another. + tables: Vec, + /// Where the streams start in the payload. + streams_at: usize, + /// Where each unit's stream ends in the payload; unit `u`'s starts where unit + /// `u − 1`'s ends. + ends: Vec, +} + +/// The cost a `WIR1` header declares, read before any symbol is decoded: the layer's +/// output over its units, its symbols, and the most bytes its decode holds at once +/// beyond the payload and the stage below (the parsed tables, 4 bytes per entry; the +/// stream index, 8 bytes per unit; and one unit's output while it is built, since a +/// unit is applied whole). A residual runs no network, so it has no +/// multiply-accumulates. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Wir1Cost { + pub output_bytes: u64, + pub symbols: u64, + pub working_bytes: u64, +} + +struct Cur<'a> { + b: &'a [u8], + p: usize, +} + +impl<'a> Cur<'a> { + fn take(&mut self, n: u64, what: &'static str) -> Result<&'a [u8], Wir1Error> { + let n = usize::try_from(n).ok().filter(|&n| n <= self.b.len() - self.p).ok_or(err(Wir1Refusal::StreamLength, what))?; + let s = &self.b[self.p..self.p + n]; + self.p += n; + Ok(s) + } +} + +fn le32(b: &[u8], at: usize) -> u32 { + u32::from_le_bytes([b[at], b[at + 1], b[at + 2], b[at + 3]]) +} + +/// The table rules: `3 ≤ len ≤ 4097`, `cdf[0] = 0`, `cdf[len − 1] = 2^16`, strictly +/// increasing. +fn check_cdf(cdf: &[i32]) -> Result<(), Wir1Error> { + if cdf.len() < CDF_LEN_MIN || cdf.len() > CDF_LEN_MAX { + return Err(err(Wir1Refusal::CdfMalformed, "a table's length is outside 3..=4097")); + } + if cdf[0] != 0 || cdf[cdf.len() - 1] != 1 << 16 { + return Err(err(Wir1Refusal::CdfMalformed, "a table does not run from 0 to 65536")); + } + if cdf.windows(2).any(|w| w[0] >= w[1]) { + return Err(err(Wir1Refusal::CdfMalformed, "a table is not strictly increasing")); + } + Ok(()) +} + +/// Parse a `WIR1` payload whose units are of `kind`, checking in the appendix's order +/// and refusing with the first rule that fails: +/// +/// 1. the magic (`bad_magic`); +/// 2. the version byte present (`stream_length`) and 1 (`version`); +/// 3. the 44-byte header present (`stream_length`); +/// 4. the reserved field at 42 (`reserved_nonzero`); +/// 5. the header's fields (`field_range`), in order: `elem` (1 or 2), `predictor` +/// (0–2), `table_mode` (0 or 1), `n_units ≥ 1`, `d0`, `d1`, `d2 ≥ 1`, the rate +/// fields (both ≥ 1 for the rate predictor, both 0 otherwise), `q` in 1..=65535, +/// `cdf_len` in 3..=4097; then the unit kind's rules: `elem`, the predictors the kind +/// allows, and its fixed dimension (`d2 = 3` for RGB8, `d1 = 1` for PCM); +/// 6. the caps (`too_large`): a unit's `d0·d1·d2 ≤ 2^26`, `n_units ≤ 2^20`, the +/// payload's `n_units·d0·d1·d2 ≤ 2^30`; +/// 7. the tables, one at a time, present (`stream_length`) then checked +/// (`cdf_malformed`); the stream lengths present (`stream_length`), each a multiple +/// of 4 and at least 8 (`stream_length`); the streams present (`stream_length`); +/// 8. nothing after the last stream (`trailing`). +pub fn parse(p: &[u8], kind: UnitKind) -> Result { + use Wir1Refusal::*; + if p.get(0..4) != Some(&MAGIC[..]) { + return Err(err(BadMagic, "the payload does not start with WIR1")); + } + let version = *p.get(4).ok_or(err(StreamLength, "the version byte"))?; + if version != VERSION { + return Err(err(Version, "a version this revision does not define")); + } + if p.len() < HEADER { + return Err(err(StreamLength, "the header")); + } + if p[42] != 0 || p[43] != 0 { + return Err(err(ReservedNonzero, "the reserved field at 42")); + } + let (elem, pred_b, table_mode) = (p[5], p[6], p[7]); + let (n_units, d0, d1, d2) = (le32(p, 8), le32(p, 12), le32(p, 16), le32(p, 20)); + let (rate_num, rate_den, q) = (le32(p, 24), le32(p, 28), le32(p, 32)); + let offset = le32(p, 36) as i32; + let cdf_len = u16::from_le_bytes([p[40], p[41]]) as usize; + if elem != 1 && elem != 2 { + return Err(err(FieldRange, "elem is not 1 (u8) or 2 (i16)")); + } + let predictor = Predictor::from_byte(pred_b).ok_or(err(FieldRange, "predictor is not 0, 1 or 2"))?; + if table_mode > 1 { + return Err(err(FieldRange, "table_mode is not 0 or 1")); + } + if n_units == 0 { + return Err(err(FieldRange, "n_units is 0")); + } + if d0 == 0 || d1 == 0 || d2 == 0 { + return Err(err(FieldRange, "a dimension is 0")); + } + let rate_ok = match predictor { + Predictor::Rate => rate_num >= 1 && rate_den >= 1, + _ => rate_num == 0 && rate_den == 0, + }; + if !rate_ok { + return Err(err(FieldRange, "the rate fields are not both ≥ 1 for the rate predictor, and both 0 otherwise")); + } + if q == 0 || q > MAX_Q { + return Err(err(FieldRange, "q is outside 1..=65535")); + } + if !(CDF_LEN_MIN..=CDF_LEN_MAX).contains(&cdf_len) { + return Err(err(FieldRange, "cdf_len is outside 3..=4097")); + } + let kind_ok = match kind { + UnitKind::Rgb8 => elem == 1 && matches!(predictor, Predictor::Identity | Predictor::Up2) && d2 == 3, + UnitKind::Pcm => elem == 2 && matches!(predictor, Predictor::Identity | Predictor::Rate) && d1 == 1, + UnitKind::Attr => elem == 1 && predictor == Predictor::Identity, + }; + if !kind_ok { + return Err(err(FieldRange, "the element type, predictor or fixed dimension is not one the unit kind allows")); + } + let unit = (d0 as u64).checked_mul(d1 as u64).and_then(|v| v.checked_mul(d2 as u64)).unwrap_or(u64::MAX); + if unit > MAX_UNIT { + return Err(err(TooLarge, "a unit holds more than 2^26 elements")); + } + if n_units as u64 > MAX_UNITS { + return Err(err(TooLarge, "more than 2^20 units")); + } + if unit.saturating_mul(n_units as u64) > MAX_ELEMENTS { + return Err(err(TooLarge, "the payload holds more than 2^30 elements")); + } + let mut c = Cur { b: p, p: HEADER }; + let n_tables = if table_mode == 1 { n_units as usize } else { 1 }; + // Sized by what the payload holds, so the parse allocates nothing its bytes do not + // justify; the tables are taken and checked one at a time (A.3 step 7). The product + // is formed in u64: n_tables·cdf_len reaches 2^32 + 2^20, past a 32-bit usize. + let room = ((p.len() - c.p) / 4) as u64; + let mut tables: Vec = Vec::with_capacity((n_tables as u64).saturating_mul(cdf_len as u64).min(room) as usize); + for _ in 0..n_tables { + let t = c.take(cdf_len as u64 * 4, "a table")?; + let at = tables.len(); + tables.extend(t.chunks_exact(4).map(|w| i32::try_from(u32::from_le_bytes([w[0], w[1], w[2], w[3]])).unwrap_or(i32::MAX))); + check_cdf(&tables[at..])?; + } + let lens = c.take(n_units as u64 * 4, "a stream length")?; + let streams_at = c.p; + let mut ends: Vec = Vec::with_capacity(n_units as usize); + let mut end = streams_at as u64; + for w in lens.chunks_exact(4) { + let l = u32::from_le_bytes([w[0], w[1], w[2], w[3]]); + if l % 4 != 0 || l < 8 { + return Err(err(StreamLength, "a stream length is not a multiple of 4, or is under 8")); + } + end += l as u64; + // A stream past the payload's end is refused at the streams' presence below. + ends.push(usize::try_from(end).unwrap_or(usize::MAX)); + } + for (u, &e) in ends.iter().enumerate() { + let start = if u == 0 { streams_at } else { ends[u - 1] }; + c.take(e.saturating_sub(start) as u64, "a stream")?; + } + if c.p != p.len() { + return Err(err(Trailing, "bytes follow the last stream")); + } + Ok(Wir1 { + kind, + predictor, + table_per_unit: table_mode == 1, + n_units, + d0, + d1, + d2, + rate_num, + rate_den, + q, + offset, + cdf_len, + tables, + streams_at, + ends, + }) +} + +impl Wir1 { + /// Elements per unit. + pub fn unit_elements(&self) -> u64 { + self.d0 as u64 * self.d1 as u64 * self.d2 as u64 + } + + /// Bytes per element of the output. + pub fn elem_bytes(&self) -> u64 { + match self.kind { + UnitKind::Pcm => 2, + _ => 1, + } + } + + /// The cost the header declares ([`Wir1Cost`]). + pub fn declared_cost(&self) -> Wir1Cost { + let unit_bytes = self.unit_elements() * self.elem_bytes(); + let tables = 4 * self.tables.len() as u64; + Wir1Cost { + output_bytes: unit_bytes * self.n_units as u64, + symbols: self.unit_elements() * self.n_units as u64, + working_bytes: tables + 8 * self.n_units as u64 + unit_bytes, + } + } + + /// The number of tables: 1, or one per unit. + pub fn n_tables(&self) -> usize { + self.tables.len() / self.cdf_len.max(1) + } + + /// The table unit `unit` is decoded against. + pub fn table(&self, unit: usize) -> &[i32] { + let t = if self.table_per_unit { unit } else { 0 }; + &self.tables[t * self.cdf_len..(t + 1) * self.cdf_len] + } + + /// Unit `unit`'s stream in `payload`, the bytes this was parsed from; empty for a + /// unit outside the payload or a payload that is not the one parsed. + pub fn stream<'p>(&self, payload: &'p [u8], unit: usize) -> &'p [u8] { + let Some(&end) = self.ends.get(unit) else { return &[] }; + let start = if unit == 0 { self.streams_at } else { self.ends[unit - 1] }; + payload.get(start..end).unwrap_or(&[]) + } + + /// The shape the stage below a unit must have for this payload's predictor: + /// `(d0, d1, d2)` for identity, `(⌈d0/2⌉, ⌈d1/2⌉, d2)` for up2; for rate, any + /// `(n, 1, d2)` with `⌊n·rate_num/rate_den⌋ = d0`. + pub fn accepts_below(&self, below: (u32, u32, u32)) -> bool { + let (b0, b1, b2) = below; + match self.predictor { + Predictor::Identity => below == (self.d0, self.d1, self.d2), + Predictor::Up2 => below == (self.d0.div_ceil(2), self.d1.div_ceil(2), self.d2), + Predictor::Rate => { + b1 == 1 && b2 == self.d2 && b0 >= 1 && rate_len(b0 as u64, self.rate_num, self.rate_den) == Some(self.d0 as u64) + } + } + } + + /// Decode every unit's symbols, without the stage below, and check each stream + /// runs to its last symbol and ends canonically: whether the payload is well + /// formed (a payload malformed for one unit is malformed for all). Units in order; + /// the first refusal. + pub fn check_streams(&self, payload: &[u8]) -> Result<(), (u32, Wir1Error)> { + let n = self.unit_elements(); + let cdf_len = self.cdf_len; + for u in 0..self.ends.len() { + let fail = |e| (u as u32, e); + let mut dec = RansDecoder::new(self.stream(payload, u)).map_err(|_| fail(err(Wir1Refusal::StreamLength, "a stream")))?; + let t = self.table(u); + for _ in 0..n { + dec.decode(t, cdf_len, self.offset); + if dec.is_exhausted() { + return Err(fail(err(Wir1Refusal::StreamExhausted, "a stream ran out before its last symbol"))); + } + } + if !dec.is_canonical_end() { + return Err(fail(err(Wir1Refusal::StreamNoncanonical, "a stream does not end canonically"))); + } + } + Ok(()) + } +} + +/// The stage below a unit, as the decode reads it. +#[derive(Debug, Clone, Copy)] +pub enum Below<'a> { + U8(&'a [u8]), + I16(&'a [i16]), +} + +/// A unit's next stage. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Next { + U8(Vec), + I16(Vec), +} + +/// The prediction of element `i` of a unit (raster order, `i = (a·d1 + b)·d2 + c`). +fn pred_at(w: &Wir1, below: Below<'_>, below_shape: (u32, u32, u32), i: u64) -> i64 { + let (d1, d2) = (w.d1 as u64, w.d2 as u64); + let (a, b, c) = (i / (d1 * d2), (i / d2) % d1, i % d2); + match (w.predictor, below) { + (Predictor::Identity, Below::U8(p)) => p[i as usize] as i64, + (Predictor::Identity, Below::I16(p)) => p[i as usize] as i64, + (Predictor::Up2, Below::U8(p)) => { + up2_at(p, below_shape.0 as usize, below_shape.1 as usize, below_shape.2 as usize, a as usize, b as usize, c as usize) + } + (Predictor::Rate, Below::I16(p)) => rate_at(p, w.d2 as usize, w.rate_num, w.rate_den, a as usize, c as usize), + _ => 0, + } +} + +/// Decode unit `unit` of `w` over `below`, the unit's current output of shape +/// `below_shape`: `clamp(pred + r·q)` for every element in raster order. Refuses +/// `geometry_mismatch` before any symbol when the shape is not one the predictor +/// reads ([`Wir1::accepts_below`]) or the buffer's type or length disagrees with it; +/// then `stream_exhausted` at the first symbol whose decode runs the stream out, and +/// `stream_noncanonical` at an end that is not canonical. Nothing is returned for a +/// refused unit, so a caller never holds a partly decoded one. +pub fn apply_unit(w: &Wir1, payload: &[u8], unit: u32, below: Below<'_>, below_shape: (u32, u32, u32)) -> Result { + use Wir1Refusal::*; + let u = unit as usize; + if u >= w.ends.len() { + return Err(err(GeometryMismatch, "the unit is outside the payload")); + } + // Checked: a shape whose element count passes u64 is no shape a payload reads. + let below_len = (below_shape.0 as u64) + .checked_mul(below_shape.1 as u64) + .and_then(|v| v.checked_mul(below_shape.2 as u64)) + .ok_or(err(GeometryMismatch, "the unit's current output is not the shape the predictor reads"))?; + let type_ok = matches!((w.kind, below), (UnitKind::Pcm, Below::I16(_)) | (UnitKind::Rgb8 | UnitKind::Attr, Below::U8(_))); + let len = match below { + Below::U8(p) => p.len() as u64, + Below::I16(p) => p.len() as u64, + }; + if !type_ok || len != below_len || !w.accepts_below(below_shape) { + return Err(err(GeometryMismatch, "the unit's current output is not the shape the predictor reads")); + } + let n = w.unit_elements(); + let t = w.table(u); + let mut dec = RansDecoder::new(w.stream(payload, u)).map_err(|_| err(StreamLength, "a stream"))?; + // Built in the element type (the clamp puts every value in its range), sized + // exactly: a unit holds the bytes its declared cost counts, and no more. + let mut out = match w.kind { + UnitKind::Pcm => Next::I16(Vec::with_capacity(n as usize)), + _ => Next::U8(Vec::with_capacity(n as usize)), + }; + for i in 0..n { + let r = dec.decode(t, t.len(), w.offset); + if dec.is_exhausted() { + return Err(err(StreamExhausted, "a stream ran out before its last symbol")); + } + let pred = pred_at(w, below, below_shape, i); + match &mut out { + Next::I16(v) => v.push(step(pred, r as i64, w.q as i64, I16_RANGE.0, I16_RANGE.1) as i16), + Next::U8(v) => v.push(step(pred, r as i64, w.q as i64, U8_RANGE.0, U8_RANGE.1) as u8), + } + } + if !dec.is_canonical_end() { + return Err(err(StreamNoncanonical, "a stream does not end canonically")); + } + Ok(out) +} + +/// What [`encode`] writes. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RefineParams { + pub q: u32, + pub predictor: Predictor, + /// For the rate predictor: output samples per `rate.1` source samples. + pub rate: (u32, u32), + pub table_per_unit: bool, + /// The most real symbols a table holds (its length less 2); residuals outside the + /// table's span are coded through the bypass escape. At most 4095. + pub max_symbols: u32, +} + +/// Why [`encode`] could not write a payload. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncodeError(pub String); + +impl std::fmt::Display for EncodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for EncodeError {} + +fn enc_err(s: impl Into) -> EncodeError { + EncodeError(s.into()) +} + +/// Encode a layer that brings each unit from `belows[u]` (of shape `below_shape`) +/// toward `targets[u]` (of shape `dims`), and return the payload and each unit's +/// exact reconstruction, the next stage a decoder computes. The residual is +/// `rdiv(target − pred, q)`. The table spans the residuals' range, or `max_symbols` +/// of it around the median, from [`crate::int_entropy::hist_to_cdf`]; residuals +/// outside it take the bypass escape. The payload is parsed and every unit decoded +/// as a sink does before it is returned, and each decode must equal the +/// reconstruction. +pub fn encode(kind: UnitKind, belows: &[Below<'_>], below_shape: (u32, u32, u32), targets: &[Below<'_>], dims: (u32, u32, u32), p: &RefineParams) -> Result<(Vec, Vec), EncodeError> { + let n_units = belows.len(); + if n_units == 0 || targets.len() != n_units { + return Err(enc_err("one target per unit, at least one unit")); + } + if p.max_symbols == 0 || p.max_symbols > (CDF_LEN_MAX - 2) as u32 { + return Err(enc_err("max_symbols is outside 1..=4095")); + } + let (rate_num, rate_den) = if p.predictor == Predictor::Rate { p.rate } else { (0, 0) }; + let elem: u8 = if kind == UnitKind::Pcm { 2 } else { 1 }; + // A header with a placeholder table, to plan predictions through the parser's + // own `Wir1` (one source for every predictor's geometry). + let plan = Wir1 { + kind, predictor: p.predictor, table_per_unit: p.table_per_unit, n_units: n_units as u32, + d0: dims.0, d1: dims.1, d2: dims.2, rate_num, rate_den, q: p.q, offset: 0, + cdf_len: 3, tables: vec![0, 1, 1 << 16], streams_at: 0, ends: vec![0; n_units], + }; + if !plan.accepts_below(below_shape) { + return Err(enc_err("the stage below is not the shape the predictor reads")); + } + let n = plan.unit_elements(); + let (lo, hi) = if kind == UnitKind::Pcm { I16_RANGE } else { U8_RANGE }; + let target_at = |t: &Below<'_>, i: u64| -> Option { + match t { + Below::U8(v) => v.get(i as usize).map(|&x| x as i64), + Below::I16(v) => v.get(i as usize).map(|&x| x as i64), + } + }; + let mut residuals: Vec> = Vec::with_capacity(n_units); + for u in 0..n_units { + let len = match targets[u] { + Below::U8(v) => v.len() as u64, + Below::I16(v) => v.len() as u64, + }; + if len != n { + return Err(enc_err("a target is not the payload's unit shape")); + } + let rs: Vec = (0..n) + .map(|i| rdiv(target_at(&targets[u], i).unwrap_or(0) - pred_at(&plan, belows[u], below_shape, i), p.q as i64)) + .collect(); + residuals.push(rs); + } + // The table's window: every residual when they fit, else max_symbols around the median. + let mut all: Vec = residuals.iter().flatten().copied().collect(); + all.sort_unstable(); + let (min_r, max_r) = (all[0], all[all.len() - 1]); + let span = (max_r - min_r + 1) as u64; + let width = span.min(p.max_symbols as u64) as i64; + let start = if span as i64 == width { min_r } else { (all[all.len() / 2] - width / 2).clamp(min_r, max_r - width + 1) }; + let offset = i32::try_from(start).map_err(|_| enc_err("the residuals' range is outside i32"))?; + let hist = |rs: &[i64]| -> Vec { + let mut h = vec![0u64; width as usize]; + for &r in rs { + if r >= start && r < start + width { + h[(r - start) as usize] += 1; + } + } + h + }; + let tables: Vec> = if p.table_per_unit { + residuals.iter().map(|rs| crate::int_entropy::hist_to_cdf(&hist(rs), 16)).collect::>() + } else { + crate::int_entropy::hist_to_cdf(&hist(&all), 16).map(|t| vec![t]) + } + .ok_or_else(|| enc_err("no table for these residuals"))?; + let cdf_len = tables[0].len(); + let mut streams = Vec::with_capacity(n_units); + for (u, rs) in residuals.iter().enumerate() { + let t = if p.table_per_unit { &tables[u] } else { &tables[0] }; + let mut e = RansEncoder::new(); + for &r in rs { + let r = i32::try_from(r).map_err(|_| enc_err("a residual is outside i32"))?; + if !e.encode(r, t, cdf_len, offset) { + return Err(enc_err("a residual the coder cannot write")); + } + } + streams.push(e.flush()); + } + let mut out = Vec::new(); + out.extend_from_slice(&MAGIC); + out.extend_from_slice(&[VERSION, elem, p.predictor.byte(), p.table_per_unit as u8]); + for v in [n_units as u32, dims.0, dims.1, dims.2, rate_num, rate_den, p.q] { + out.extend_from_slice(&v.to_le_bytes()); + } + out.extend_from_slice(&offset.to_le_bytes()); + out.extend_from_slice(&(cdf_len as u16).to_le_bytes()); + out.extend_from_slice(&[0, 0]); + for t in &tables { + for &v in t { + out.extend_from_slice(&(v as u32).to_le_bytes()); + } + } + for s in &streams { + out.extend_from_slice(&(s.len() as u32).to_le_bytes()); + } + for s in &streams { + out.extend_from_slice(s); + } + // Parse and decode as a sink does; the decode is the reconstruction. + let w = parse(&out, kind).map_err(|e| enc_err(format!("the encoder wrote a payload the parser refuses: {e}")))?; + let mut recon = Vec::with_capacity(n_units); + for (u, b) in belows.iter().enumerate() { + let next = apply_unit(&w, &out, u as u32, *b, below_shape).map_err(|e| enc_err(format!("unit {u}: {e}")))?; + // The reconstruction the encoder intended, element by element. + for (i, &r) in residuals[u].iter().enumerate() { + let want = step(pred_at(&w, *b, below_shape, i as u64), r, p.q as i64, lo, hi); + let got = match &next { + Next::U8(v) => v[i] as i64, + Next::I16(v) => v[i] as i64, + }; + if want != got { + return Err(enc_err(format!("unit {u}: the decode differs from the reconstruction at {i}"))); + } + } + recon.push(next); + } + Ok((out, recon)) +} diff --git a/wai/wai-rs/src/staged/wir1_arith.rs b/wai/wai-rs/src/staged/wir1_arith.rs new file mode 100644 index 000000000..3d5712440 --- /dev/null +++ b/wai/wai-rs/src/staged/wir1_arith.rs @@ -0,0 +1,44 @@ +//! The one arithmetic step of a residual refinement (`WIR1`, +//! `extensions/staged-delivery.md` Appendix A.1): `clamp(pred + r·q, lo, hi)`. +//! +//! Self-contained (`core` only), so the model checker (`wai-verify`, harness +//! `step_in_range`) proves it as the decoder runs it: for every `r` the rANS decoder +//! can return (`[−2^31, 2^31 − 1]`, the bypass saturates there), every step +//! `q ∈ [1, 65535]` and every prediction in the element's range, no operation +//! overflows and the result is in the range. + +/// The u8 element range. +pub const U8_RANGE: (i64, i64) = (0, 255); +/// The i16 element range. +pub const I16_RANGE: (i64, i64) = (-32768, 32767); + +/// `clamp(pred + r·q, lo, hi)`. With `|r| ≤ 2^31`, `1 ≤ q < 2^16` and `|pred| < 2^16`, +/// `|r·q| < 2^47`, so every step fits `i64`; outside those bounds the sum saturates +/// before the clamp, so the function is total for every input. +pub fn step(pred: i64, r: i64, q: i64, lo: i64, hi: i64) -> i64 { + pred.saturating_add(r.saturating_mul(q)).clamp(lo, hi.max(lo)) +} + +/// `step_in_range`: for every `r` the rANS decoder returns, every step `q` in +/// `1..=65535`, both element ranges and every prediction in the range, `pred + r·q` +/// fits `i64` without saturating, and the step is that sum clamped to the range. So a +/// build with overflow checks and one without compute the same element. +#[cfg(kani)] +#[kani::proof] +fn step_in_range() { + let r: i64 = kani::any(); + kani::assume(r >= -(1i64 << 31) && r < (1i64 << 31)); + let q: i64 = kani::any(); + kani::assume((1..=65_535).contains(&q)); + let wide: bool = kani::any(); + let (lo, hi) = if wide { I16_RANGE } else { U8_RANGE }; + let pred: i64 = kani::any(); + kani::assume(pred >= lo && pred <= hi); + let exact = r.checked_mul(q).and_then(|m| pred.checked_add(m)); + kani::assert(exact.is_some(), "pred + r·q fits i64"); + let v = step(pred, r, q, lo, hi); + kani::assert(v >= lo && v <= hi, "the element is in its range"); + kani::assert(Some(v) == exact.map(|e| e.clamp(lo, hi)), "the step is the exact sum, clamped"); + kani::cover!(v == lo, "the clamp's floor is reached"); + kani::cover!(v == hi, "the clamp's ceiling is reached"); +} diff --git a/wai/wai-rs/src/ternary.rs b/wai/wai-rs/src/ternary.rs index ab459892b..0a37b2f4d 100644 --- a/wai/wai-rs/src/ternary.rs +++ b/wai/wai-rs/src/ternary.rs @@ -573,7 +573,8 @@ pub fn rans_encode_trits(trits: &[i8]) -> Vec { let mut enc = crate::rans::RansEncoder::new(); for &t in trits { let v = (t.clamp(-1, 1) + 1) as i32; - debug_assert!(enc.encode(v, &cdf, 5, 0)); + let coded = enc.encode(v, &cdf, 5, 0); + debug_assert!(coded, "a trit is always in the table's span"); } let body = enc.flush(); let mut out = Vec::with_capacity(10 + body.len()); diff --git a/wai/wai-rs/tests/carriage.rs b/wai/wai-rs/tests/carriage.rs index bedfd13db..1a026d36a 100644 --- a/wai/wai-rs/tests/carriage.rs +++ b/wai/wai-rs/tests/carriage.rs @@ -25,7 +25,7 @@ use wai::codecs::{ capability_license, capability_media, capability_tier, is_candidate, is_companion_only, - is_known_capability, known_capabilities, Media, + is_known_capability, is_refinement_only, known_capabilities, Media, }; use wai::container::{Rendition, RoleError, WaiMulti}; use wai::{Conditioning, Manifest, ModelRequirement, Wai}; @@ -62,6 +62,15 @@ fn wrap(cap: &str, payload: Vec) -> Wai { ) } +/// A refinement-only capability in the form SPEC §5 allows: a `WAI1` with `refines` +/// and no fallback (`extensions/staged-delivery.md` §3). +fn wrap_refinement(cap: &str, payload: Vec) -> Wai { + let h = "a".repeat(64); + let mut m = Manifest::new(media_for(cap), "replicate", ModelRequirement::new(cap), Conditioning::new("wir1")); + m.refines = Some(serde_json::json!({"base": h, "prev": h, "layer": 1})); + Wai::new(m, payload) +} + /// A companion-only capability in the form SPEC §5 allows: a `WAI2` companion /// bound to a base video rendition. Returns the parsed envelope. fn wrap_companion(cap: &str, payload: Vec) -> WaiMulti { @@ -93,6 +102,14 @@ fn every_registered_capability_carries_bytes_byte_exact() { assert!(capability_license(cap).is_some(), "{cap}: no license class"); continue; } + if is_refinement_only(cap) { + let back = Wai::from_bytes(&wrap_refinement(cap, payload.clone()).to_bytes().expect("serialize")).expect("parse"); + assert_eq!(back.check_component_roles(), Ok(()), "{cap}: refinement carriage must conform"); + assert_eq!(back.payload, payload, "{cap}: payload not carried byte-identically at {len} B"); + assert_eq!(back.refines().map(|r| r.map(|r| r.layer)), Some(Ok(1)), "{cap}: refines not preserved"); + assert!(capability_tier(cap).is_some() && capability_license(cap).is_some(), "{cap}: unclassified"); + continue; + } let env = wrap(cap, payload.clone()); let bytes = env.to_bytes().expect("serialize envelope"); let back = Wai::from_bytes(&bytes).expect("parse envelope"); @@ -151,6 +168,18 @@ fn no_standalone_capability_is_carried_as_a_companion() { continue; } let payload = payload_for(cap, 257); + if is_refinement_only(cap) { + // A refinement is not standalone media either: carried as a companion, the + // role check reports it as a refinement-only capability in another role. + let env = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ + Rendition::media("wai.video.hevc", "hevc", payload_for("wai.video.hevc", 64)), + Rendition::companion(cap, payload.clone(), "wai.video.hevc"), + ]); + let back = WaiMulti::from_bytes(&env.to_bytes().expect("serialize envelope")).expect("parse envelope"); + assert_eq!(back.check_component_roles(), + Err(RoleError::RefinementOnlyAsComponent { index: 1, capability: cap.into() }), "{cap}"); + continue; + } let env = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![ Rendition::media("wai.video.hevc", "hevc", payload_for("wai.video.hevc", 64)), Rendition::companion(cap, payload.clone(), "wai.video.hevc"), diff --git a/wai/wai-rs/tests/cli_stage.rs b/wai/wai-rs/tests/cli_stage.rs new file mode 100644 index 000000000..a2a2dcc49 --- /dev/null +++ b/wai/wai-rs/tests/cli_stage.rs @@ -0,0 +1,139 @@ +//! `wai stage`: the tool writes, applies, inspects, packs and explodes +//! staged-delivery layers (`extensions/staged-delivery.md`; SPEC §7.1), and `wai wrap` +//! refuses a refinement capability. These run the binary on the committed corpus +//! (`staged-conformance/`), whose expectations the corpus verifiers hold. + +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +fn wai_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("..") +} + +fn corpus(rel: &str) -> PathBuf { + wai_dir().join("staged-conformance").join(rel) +} + +fn scratch(name: &str) -> PathBuf { + let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_stage"); + std::fs::create_dir_all(&dir).unwrap(); + let p = dir.join(name); + let _ = std::fs::remove_file(&p); + p +} + +fn wai(args: &[&std::ffi::OsStr]) -> Output { + Command::new(env!("CARGO_BIN_EXE_wai")).args(args).output().expect("run wai") +} + +fn os(s: &str) -> &std::ffi::OsStr { + std::ffi::OsStr::new(s) +} + +fn text(o: &Output) -> String { + format!("{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr)) +} + +fn case(name: &str) -> serde_json::Value { + serde_json::from_slice(&std::fs::read(corpus(&format!("cases/{name}/case.json"))).unwrap()).unwrap() +} + +/// The tiny conditional codec's parameter set, by the names SPEC §5 gives its files. +fn tiny_set() -> PathBuf { + let dir = Path::new(env!("CARGO_TARGET_TMPDIR")).join("cli_stage/tiny_set"); + std::fs::create_dir_all(&dir).unwrap(); + let v = wai_dir().join("wai-rs/tests/vectors/int_hyper"); + for (from, to) in [("tiny_entropy.json", "entropy.json"), ("tiny_g_s.bin", "g_s.whm"), ("tiny_h_s.bin", "h_s.whm")] { + std::fs::copy(v.join(from), dir.join(to)).unwrap(); + } + dir +} + +/// `wai stage apply` over a pinned integer base and two layers reaches the clip hash +/// the corpus states. +#[test] +fn apply_reaches_the_corpus_clip_hash() { + let c = case("image_int_hyper_2l"); + let d = corpus("cases/image_int_hyper_2l"); + let set = tiny_set(); + let o = wai(&[os("stage"), os("apply"), d.join("base.wai").as_os_str(), d.join("L1.wai").as_os_str(), d.join("L2.wai").as_os_str(), os("--set"), set.as_os_str()]); + let t = text(&o); + assert!(o.status.success(), "{t}"); + let want = c["expect"]["bases"][0]["clip_sha256"].as_str().unwrap(); + assert!(t.contains(&format!("clip sha256 {want}")), "{t}"); + assert!(t.contains("unit 0: stage 2"), "{t}"); +} + +/// `wai stage refine` at q = 1 with the identity predictor writes a layer that +/// `wai stage apply` brings exactly to the target, whose frames' SHA-256 it prints; +/// `wai stage inspect` reads the layer back. +#[test] +fn refine_then_apply_reaches_the_target() { + let d = corpus("cases/out_of_order"); + let target: Vec = (0..2 * 8 * 8 * 3).map(|i| ((i * 37) % 251) as u8).collect(); + let tpath = scratch("target.rgb"); + std::fs::write(&tpath, &target).unwrap(); + let lpath = scratch("L1.wai"); + let o = wai(&[os("stage"), os("refine"), os("--base"), d.join("base.wai").as_os_str(), os("--target"), tpath.as_os_str(), os("--q"), os("1"), os("-o"), lpath.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + let o = wai(&[os("stage"), os("apply"), d.join("base.wai").as_os_str(), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success(), "{t}"); + for f in 0..2 { + let sha = wai::pin::sha256_hex(&target[f * 192..(f + 1) * 192]); + assert!(t.contains(&format!("unit {f}: stage 1")) && t.contains(&sha), "frame {f}: {t}"); + } + let o = wai(&[os("stage"), os("inspect"), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success() && t.contains("roles: ✓") && t.contains("layer 1") && t.contains("streams: ✓"), "{t}"); +} + +/// `wai stage refine` over a base read from `--stage0` at `--dims` writes a layer at +/// `--out-dims`: up2 from 16×16 to an odd 31×31 (the predictor's default would be +/// 32×32), which `wai stage inspect` reads back. +#[test] +fn refine_honours_out_dims_over_stage0() { + let d = corpus("cases/image_up2_odd"); + let target: Vec = (0..31 * 31 * 3).map(|i| ((i * 29) % 253) as u8).collect(); + let tpath = scratch("target31.rgb"); + std::fs::write(&tpath, &target).unwrap(); + let lpath = scratch("L1_31.wai"); + let o = wai(&[os("stage"), os("refine"), os("--base"), d.join("base.wai").as_os_str(), os("--stage0"), d.join("stage0.bin").as_os_str(), + os("--units"), os("1"), os("--dims"), os("16,16,3"), os("--predictor"), os("up2"), os("--out-dims"), os("31,31,3"), + os("--target"), tpath.as_os_str(), os("--q"), os("1"), os("-o"), lpath.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + let o = wai(&[os("stage"), os("inspect"), lpath.as_os_str()]); + let t = text(&o); + assert!(o.status.success() && t.contains("1 units of 31x31x3") && t.contains("predictor up2"), "{t}"); +} + +/// `wai stage pack` packs the corpus's live-style chain into a WAI2 that `wai stage +/// explode` turns back into each source, byte for byte. +#[test] +fn pack_then_explode_is_the_identity() { + let srcs: Vec = (0..4).map(|i| corpus(&format!("wai2/pack_live_chain.src{i}.wai"))).collect(); + let packed = scratch("packed.wai"); + let mut args: Vec<&std::ffi::OsStr> = vec![os("stage"), os("pack")]; + args.extend(srcs.iter().map(|p| p.as_os_str())); + args.extend([os("-o"), packed.as_os_str()]); + let o = wai(&args); + assert!(o.status.success(), "{}", text(&o)); + assert_eq!(std::fs::read(&packed).unwrap(), std::fs::read(corpus("wai2/pack_live_chain.wai")).unwrap(), "the corpus's packing"); + for (i, s) in srcs.iter().enumerate() { + let out = scratch(&format!("exploded{i}.wai")); + let o = wai(&[os("stage"), os("explode"), packed.as_os_str(), os(&i.to_string()), os("-o"), out.as_os_str()]); + assert!(o.status.success(), "{}", text(&o)); + assert_eq!(std::fs::read(out).unwrap(), std::fs::read(s).unwrap(), "component {i}"); + } +} + +/// `wai wrap` writes no `refines`, so it refuses a refinement capability and writes +/// nothing. +#[test] +fn wrap_refuses_a_refinement_capability() { + let out = scratch("wrapped.wai"); + let o = wai(&[os("wrap"), os("wai.video.int_refine"), corpus("cases/out_of_order/L1.wai").as_os_str(), os("-o"), out.as_os_str()]); + assert!(!o.status.success()); + assert!(text(&o).contains("refinement-only"), "{}", text(&o)); + assert!(!out.exists()); +} diff --git a/wai/wai-rs/tests/int_kernels_vectors.rs b/wai/wai-rs/tests/int_kernels_vectors.rs index 586c4ba4a..fcccbec42 100644 --- a/wai/wai-rs/tests/int_kernels_vectors.rs +++ b/wai/wai-rs/tests/int_kernels_vectors.rs @@ -223,6 +223,7 @@ fn every_decoded_output_states_a_sha256() { ("signer-conformance", "key logs, grants and statuses: signed documents, no decoded output"), ("byte-exact-conformance", "goldens over tests/vectors, whose sidecars state the SHA-256"), ("int-payload-conformance", "refusal fixtures and the index of the payloads under tests/vectors, whose sidecars state the SHA-256"), + ("staged-conformance", "each case.json states the SHA-256 of every stage-0 buffer and of every unit's stage (SPEC §7 Digests)"), ("vectors/int_mlicpp", "expected.json states rgb_sha256 (RGB8)"), ("vectors/jpegai", "inputs only; the decoded pixels' SHA-256 is pinned by the JPEG AI e2e job"), ]; diff --git a/wai/wai-rs/tests/staged_resources.rs b/wai/wai-rs/tests/staged_resources.rs new file mode 100644 index 000000000..b89c75913 --- /dev/null +++ b/wai/wai-rs/tests/staged_resources.rs @@ -0,0 +1,395 @@ +//! The staged sink's own work and memory (`extensions/staged-delivery.md` §4.5 and +//! Appendix A.5), measured with a global allocator that counts the bytes allocated, +//! the allocations, the bytes live and their peak. +//! +//! - Work per layer is linear in its units: an offer of a layer over 4096, 16384 and +//! 65536 units (one layer alone; a layer whose `prev` is a run per unit, held; a +//! layer held and then released by the layer below it) allocates, holds and takes +//! time in proportion to the units, within a margin a quadratic sink is far outside. +//! - The chain budget: a layer that would take the chain's state past it is refused +//! (`chain-over-limit`) before any stream is decoded, and the state the chain +//! reports is the sum §4.5 defines. +//! - Retention: a layer no unit is held for is dropped with its payload, so offering +//! many layers that conflict, or that expire, leaves the sink holding what it held. +//! - The parse holds no more than the declared working bytes beyond the payload. +//! +//! Its own test binary with a single test, so that no other test's allocations land in +//! the windows it measures. +#![cfg(all(feature = "staged", feature = "video_int"))] + +use std::alloc::{GlobalAlloc, Layout, System}; +use std::sync::atomic::{AtomicUsize, Ordering::Relaxed}; +use std::time::{Duration, Instant}; + +use wai::codecs::{DeterminismTier, CAP_VIDEO_INT_MOTION, CAP_VIDEO_INT_REFINE}; +use wai::container::{Conditioning, Manifest, ModelRequirement, Prev, PrevRun, Refines, Wai}; +use wai::staged::wir1::{self, Below, Predictor, RefineParams, UnitKind}; +use wai::staged::{BaseInfo, ChainConfig, HoldBound, ReceiptEvidence, Samples, StageRefusal, StageSink, UnitBuf}; + +/// The system allocator, counting bytes allocated, allocations, bytes live and peak. +struct Count; +static ALLOCATED: AtomicUsize = AtomicUsize::new(0); +static CALLS: AtomicUsize = AtomicUsize::new(0); +static CUR: AtomicUsize = AtomicUsize::new(0); +static PEAK: AtomicUsize = AtomicUsize::new(0); + +fn grow(n: usize) { + ALLOCATED.fetch_add(n, Relaxed); + CALLS.fetch_add(1, Relaxed); + PEAK.fetch_max(CUR.fetch_add(n, Relaxed) + n, Relaxed); +} + +unsafe impl GlobalAlloc for Count { + unsafe fn alloc(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc(l) } + } + unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 { + grow(l.size()); + unsafe { System.alloc_zeroed(l) } + } + unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 { + if new >= l.size() { + grow(new - l.size()); + } else { + CUR.fetch_sub(l.size() - new, Relaxed); + } + unsafe { System.realloc(p, l, new) } + } + unsafe fn dealloc(&self, p: *mut u8, l: Layout) { + CUR.fetch_sub(l.size(), Relaxed); + unsafe { System.dealloc(p, l) } + } +} + +#[global_allocator] +static ALLOC: Count = Count; + +/// What a window measured: bytes allocated, allocations, the most bytes live at once +/// beyond the start, and the time. +#[derive(Debug, Clone, Copy)] +struct Window { + allocated: usize, + calls: usize, + peak: usize, + time: Duration, +} + +fn measure(f: impl FnOnce() -> T) -> (T, Window) { + let start = CUR.load(Relaxed); + PEAK.store(start, Relaxed); + let (a0, c0) = (ALLOCATED.load(Relaxed), CALLS.load(Relaxed)); + let t = Instant::now(); + let out = f(); + let time = t.elapsed(); + let w = Window { allocated: ALLOCATED.load(Relaxed) - a0, calls: CALLS.load(Relaxed) - c0, peak: PEAK.load(Relaxed) - start, time }; + (out, w) +} + +fn hex(h: &[u8; 32]) -> String { + h.iter().map(|b| format!("{b:02x}")).collect() +} + +fn cfg(max_envelopes: usize, budget: u64) -> ChainConfig { + ChainConfig { bound: HoldBound { max_envelopes, max_age_ms: 1000 }, cost_limit: None, budget, record_budget: u64::MAX, live: None } +} + +/// A base of `n` frames of `side × side` RGB8, named by `hash`. +fn base(n: u32, side: u32, hash: [u8; 32]) -> (BaseInfo, Vec) { + let info = BaseInfo { + content_hash: hash, + capability: CAP_VIDEO_INT_MOTION.into(), + via_fallback: false, + tier: Some(DeterminismTier::DecodeEquivalence), + attested: false, + first_unit: 0, + }; + let units = (0..n) + .map(|f| UnitBuf { shape: (side, side, 3), samples: Samples::U8((0..side * side * 3).map(|i| ((i * 7 + f * 3) % 251) as u8).collect()) }) + .collect(); + (info, units) +} + +/// A `wai.video.int_refine` envelope: an identity layer bringing each of `units` to +/// itself plus `delta`. +fn layer(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], delta: u8) -> Vec { + toward(base, prev, layer, units, |_, x| x.wrapping_add(delta)) +} + +/// A layer toward a target of noise seeded by `seed`, so its payload is about the +/// size of its output. +fn noisy(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], seed: u32) -> Vec { + toward(base, prev, layer, units, move |i, x| x ^ ((i as u32).wrapping_mul(2654435761).wrapping_add(seed.wrapping_mul(40503)) >> 13) as u8) +} + +fn toward(base: &[u8; 32], prev: Prev, layer: u8, units: &[UnitBuf], f: impl Fn(usize, u8) -> u8) -> Vec { + let shape = units[0].shape; + let b: Vec = units.iter().map(|u| match &u.samples { + Samples::U8(v) => Below::U8(v), + Samples::I16(v) => Below::I16(v), + }).collect(); + let targets: Vec> = units.iter().map(|u| match &u.samples { + Samples::U8(v) => v.iter().enumerate().map(|(i, x)| f(i, *x)).collect(), + Samples::I16(_) => unreachable!(), + }).collect(); + let t: Vec = targets.iter().map(|v| Below::U8(v)).collect(); + let p = RefineParams { q: 1, predictor: Predictor::Identity, rate: (0, 0), table_per_unit: false, max_symbols: 4095 }; + let (payload, _) = wir1::encode(UnitKind::Rgb8, &b, shape, &t, shape, &p).expect("encode"); + let r = Refines { base: hex(base), prev, layer, span: None }; + let m = Manifest::new("video", "replicate", ModelRequirement::new(CAP_VIDEO_INT_REFINE), Conditioning::new("wir1")).with_refines(&r); + Wai::new(m, payload).to_bytes().expect("serialise") +} + +/// The shapes of offer, at `n` units: a layer 1 alone; a layer 2 whose `prev` is one +/// run per unit, held; a layer 2 held, then the layer 1 that releases it; and a layer 2 +/// whose report mixes outcomes, with layer 1 applied to half the units: half held and +/// half applied, then half held and half prev-mismatch. +fn offers(n: u32) -> [Window; 5] { + let bh = [0x5a; 32]; + let (info, units) = base(n, 1, bh); + let l1 = layer(&bh, Prev::One(hex(&bh)), 1, &units, 1); + let h1 = wai::jwp_hash::content_hash(&l1); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let (r, single) = measure(|| sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].applied.len(), n as usize); + + let (pa, pb) = (hex(&[0xaa; 32]), hex(&[0xbb; 32])); + let runs: Vec = (0..n).map(|i| PrevRun { producer: if i % 2 == 0 { pa.clone() } else { pb.clone() }, count: 1 }).collect(); + let l2r = layer(&bh, Prev::Runs(runs), 2, &units, 2); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let (r, runs) = measure(|| sink.offer(&l2r, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].held.len(), n as usize); + + let l1_out: Vec = units + .iter() + .map(|u| match &u.samples { + Samples::U8(v) => UnitBuf { shape: u.shape, samples: Samples::U8(v.iter().map(|x| x.wrapping_add(1)).collect()) }, + Samples::I16(_) => unreachable!(), + }) + .collect(); + let l2 = layer(&bh, Prev::One(hex(&h1)), 2, &l1_out, 1); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info, units); + let (r, held) = measure(|| { + let a = sink.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + let b = sink.offer(&l1, 1, ReceiptEvidence::None).unwrap(); + (a, b) + }); + assert_eq!(r.0[0].held.len(), n as usize); + assert_eq!((r.1[0].applied.len(), r.1[1].applied.len()), (n as usize, n as usize)); + + let half = n / 2; + let upper: Vec = l1_out[half as usize..].to_vec(); + let base_upper: Vec = { + let (_, all) = base(n, 1, bh); + all[half as usize..].to_vec() + }; + let l1_upper = { + let e = layer(&bh, Prev::One(hex(&bh)), 1, &base_upper, 1); + let mut w = Wai::from_bytes(&e).unwrap(); + w.manifest = w.manifest.clone().with_refines(&Refines { base: hex(&bh), prev: Prev::One(hex(&bh)), layer: 1, span: Some(wai::container::Span { from: half, count: n - half }) }); + w.to_bytes().unwrap() + }; + let h1u = wai::jwp_hash::content_hash(&l1_upper); + let all_mid: Vec = (0..n as usize).map(|i| if i < half as usize { base_upper[0].clone() } else { upper[i - half as usize].clone() }).collect(); + let mut mixed = Vec::new(); + for prev in [h1u, [0x11; 32]] { + let l2m = layer(&bh, Prev::One(hex(&prev)), 2, &all_mid, 3); + let (info, units) = base(n, 1, bh); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info, units); + assert_eq!(sink.offer(&l1_upper, 0, ReceiptEvidence::None).unwrap()[0].applied.len(), (n - half) as usize); + let (r, w) = measure(|| sink.offer(&l2m, 1, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].held.len(), half as usize); + if prev == h1u { + assert_eq!(r[0].applied.len(), (n - half) as usize); + } else { + assert_eq!(r[0].refused.len(), (n - half) as usize); + } + mixed.push(w); + } + [single, runs, held, mixed[0], mixed[1]] +} + +#[test] +fn staged_sink_resources() { + // ── Work per layer, linear in its units ── + let sizes = [4096u32, 16384, 65536]; + // The fastest of three runs at each size, so a busy machine does not decide. + let mut at: Vec<[Window; 5]> = Vec::new(); + for &n in &sizes { + let mut best = offers(n); + for _ in 0..2 { + let w = offers(n); + for k in 0..5 { + if w[k].time < best[k].time { + best[k].time = w[k].time; + } + } + } + at.push(best); + } + for (k, name) in ["layer 1 alone", "layer 2, a run per unit, held", "layer 2 held, then layer 1", "layer 2: half held, half applied", "layer 2: half held, half prev-mismatch"] + .iter() + .enumerate() + { + for (i, &n) in sizes.iter().enumerate() { + let w = at[i][k]; + println!("{name:32} {n:6} units: {:9.3} ms {:10} bytes allocated {:7} allocations peak +{} bytes", + w.time.as_secs_f64() * 1e3, w.allocated, w.calls, w.peak); + } + // 16× the units: a linear sink stays near 16×, a quadratic one near 256×. + let (lo, hi) = (at[0][k], at[2][k]); + let ratio = |a: usize, b: usize| a as f64 / b.max(1) as f64; + assert!(ratio(hi.allocated, lo.allocated) <= 24.0, "{name}: bytes allocated grew {:.1}× for 16× the units", ratio(hi.allocated, lo.allocated)); + assert!(ratio(hi.calls, lo.calls) <= 24.0, "{name}: allocations grew {:.1}× for 16× the units", ratio(hi.calls, lo.calls)); + assert!(ratio(hi.peak, lo.peak) <= 24.0, "{name}: the peak grew {:.1}× for 16× the units", ratio(hi.peak, lo.peak)); + let t = hi.time.as_secs_f64() / lo.time.as_secs_f64().max(1e-4); + assert!(t <= 64.0 || hi.time < Duration::from_millis(50), "{name}: the offer's time grew {t:.1}× for 16× the units"); + } + + // ── The chain budget ── + let bh = [0x6b; 32]; + let (info, units) = base(4, 8, bh); + let l1 = layer(&bh, Prev::One(hex(&bh)), 1, &units, 1); + let payload_len = Wai::from_bytes(&l1).unwrap().payload.len() as u64; + let w = wir1::parse(&Wai::from_bytes(&l1).unwrap().payload, UnitKind::Rgb8).unwrap(); + let c = w.declared_cost(); + let unit_bytes: u64 = units.iter().map(|u| u.samples.byte_len()).sum(); + let reserve = payload_len + c.output_bytes + c.working_bytes + 4 * 32; + // One byte short: refused before any stream is decoded, nothing changes. + let mut sink = StageSink::new(cfg(16, unit_bytes + reserve - 1)); + sink.add_base(info.clone(), units.clone()); + let (r, refused) = measure(|| sink.offer(&l1, 0, ReceiptEvidence::None).unwrap()); + assert_eq!(r[0].envelope, Some(StageRefusal::ChainOverLimit)); + assert!(r[0].applied.is_empty() && r[0].refused.is_empty()); + let chain = sink.chain(&bh).unwrap(); + assert!(chain.units().iter().all(|s| s.stage == 0)); + assert_eq!(chain.state_bytes(), unit_bytes); + // The refusal held the envelope as parsed and the payload's parse, and no unit's output. + assert!((refused.peak as u64) < 2 * l1.len() as u64 + c.working_bytes - c.output_bytes / 4, + "a refused layer held {} bytes", refused.peak); + // At the budget: applied; the state is the units' new output and 32 bytes a stage. + let mut sink = StageSink::new(cfg(16, unit_bytes + reserve)); + sink.add_base(info.clone(), units.clone()); + let r = sink.offer(&l1, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].applied, vec![0, 1, 2, 3]); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + 32 * 4); + + // A held layer is counted while it is retained, and released when it is not. + let h1 = wai::jwp_hash::content_hash(&l1); + let l2 = layer(&bh, Prev::One(hex(&h1)), 2, &units, 3); + let mut sink = StageSink::new(cfg(16, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let r = sink.offer(&l2, 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].held, vec![0, 1, 2, 3]); + let p2 = Wai::from_bytes(&l2).unwrap().payload; + let c2 = wir1::parse(&p2, UnitKind::Rgb8).unwrap().declared_cost(); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes + p2.len() as u64 + c2.output_bytes + c2.working_bytes + 4 * 32); + let r = sink.expire(10_000); + assert_eq!(r[0].refused.len(), 4); + assert_eq!(sink.chain(&bh).unwrap().state_bytes(), unit_bytes); + + // ── Retention: what no unit holds is dropped ── + // Distinct layer-1 envelopes over one unit: the first applies, the second conflicts + // and freezes the unit, every later one is refused for it. None is held, so the sink + // keeps none of their payloads. + let bh = [0x7c; 32]; + let (info, units) = base(1, 64, bh); + let mut sink = StageSink::new(cfg(1, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let layers: Vec> = (0..48).map(|i| noisy(&bh, Prev::One(hex(&bh)), 1, &units, i)).collect(); + let after_first = { + sink.offer(&layers[0], 0, ReceiptEvidence::None).unwrap(); + CUR.load(Relaxed) + }; + for (i, l) in layers.iter().enumerate().skip(1) { + let r = sink.offer(l, i as u64, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].refused, vec![(0, StageRefusal::LayerConflict)]); + } + let grew = CUR.load(Relaxed).saturating_sub(after_first); + println!("retention: 47 refused layers of {} bytes each; the sink grew {grew} bytes", layers[1].len()); + assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes of layers no unit holds"); + // Held layers that expire are dropped too. + let mut sink = StageSink::new(cfg(64, u64::MAX)); + sink.add_base(info.clone(), units.clone()); + let before = CUR.load(Relaxed); + for (i, l) in layers.iter().enumerate().take(16) { + let mut l2 = Wai::from_bytes(l).unwrap(); + let r = Refines { base: hex(&bh), prev: Prev::One(hex(&[i as u8 + 1; 32])), layer: 2, span: None }; + l2.manifest = l2.manifest.clone().with_refines(&r); + let r = sink.offer(&l2.to_bytes().unwrap(), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].held, vec![0]); + } + assert!(CUR.load(Relaxed) > before + 16 * layers[1].len() / 2, "held layers are kept"); + let r = sink.expire(10_000); + assert_eq!(r.len(), 16); + drop(r); + let grew = CUR.load(Relaxed).saturating_sub(before); + assert!(grew < layers[1].len() / 2, "the sink kept {grew} bytes after every held layer expired"); + + // ── Records of dropped chains (§4.4) ── + // Dropped bases with nothing recorded leave nothing behind; with a layer applied to + // every unit, what the sink keeps stays within its record budget, and the heap it + // holds for them within a small multiple of it. + let units: Vec = base(4096, 1, [0; 32]).1; + let mut sink = StageSink::new(cfg(16, u64::MAX)); + let before = CUR.load(Relaxed); + for i in 0..20u8 { + let mut info = base(1, 1, [0x40 + i; 32]).0; + info.content_hash = [0x40 + i; 32]; + sink.add_base(info, units.clone()); + sink.remove_base(&[0x40 + i; 32]); + } + let grew = CUR.load(Relaxed).saturating_sub(before); + println!("records: 20 dropped bases of 4096 units with nothing recorded keep {} bytes; the sink grew {grew} bytes", sink.kept_bytes()); + assert_eq!(sink.kept_bytes(), 0); + assert!(grew < 16 << 10, "the sink kept {grew} bytes for bases with nothing recorded"); + let record_budget = 1 << 20; + let mut sink = StageSink::new(ChainConfig { record_budget, ..cfg(16, u64::MAX) }); + let before = CUR.load(Relaxed); + for i in 0..20u8 { + let h = [0x60 + i; 32]; + let mut info = base(1, 1, h).0; + info.content_hash = h; + sink.add_base(info, units.clone()); + let r = sink.offer(&layer(&h, Prev::One(hex(&h)), 1, &units, 1), 0, ReceiptEvidence::None).unwrap(); + assert_eq!(r[0].applied.len(), 4096); + sink.remove_base(&h); + assert!(sink.kept_bytes() <= record_budget); + } + let grew = CUR.load(Relaxed).saturating_sub(before); + println!("records: 20 dropped bases of 4096 refined units keep {} counted bytes within {record_budget}; the sink grew {grew} bytes", sink.kept_bytes()); + assert!(grew as u64 <= 4 * record_budget, "the sink holds {grew} bytes for a record budget of {record_budget}"); + + // ── The parse holds no more than the declared working bytes beyond the payload ── + // One table per unit, 65536 units of one attribute each. + let n = 65536u32; + let mut p = Vec::new(); + p.extend_from_slice(b"WIR1"); + p.extend_from_slice(&[1, 1, 0, 1]); + for v in [n, 1u32, 1, 1, 0, 0, 1] { + p.extend_from_slice(&v.to_le_bytes()); + } + p.extend_from_slice(&0i32.to_le_bytes()); + p.extend_from_slice(&3u16.to_le_bytes()); + p.extend_from_slice(&[0, 0]); + for _ in 0..n { + for v in [0u32, 65535, 65536] { + p.extend_from_slice(&v.to_le_bytes()); + } + } + for _ in 0..n { + p.extend_from_slice(&8u32.to_le_bytes()); + } + for _ in 0..n { + p.extend_from_slice(&(1u32 << 31).to_le_bytes()); + p.extend_from_slice(&0u32.to_le_bytes()); + } + let (w, parse) = measure(|| wir1::parse(&p, UnitKind::Attr).unwrap()); + let c = w.declared_cost(); + println!("parse: {} units, one table each: holds {} bytes, declares {} working bytes", n, parse.peak, c.working_bytes); + assert!(parse.peak as u64 <= c.working_bytes, "the parse held {} bytes, past the declared {}", parse.peak, c.working_bytes); +} diff --git a/wai/wai-verify/src/lib.rs b/wai/wai-verify/src/lib.rs index 9085b73f5..a31cdbb8c 100644 --- a/wai/wai-verify/src/lib.rs +++ b/wai/wai-verify/src/lib.rs @@ -42,3 +42,11 @@ pub mod int_kernels; /// crypto layer — a stand-in would close the goal without deciding it. #[path = "../../wai-rs/src/wai_video_policy.rs"] pub mod wai_video_policy; + +/// The staged-delivery residual (`extensions/staged-delivery.md` Appendix A): its +/// predictors (`up2_bounded`, `rdiv_matches_spec`, `rate_index_in_bounds`) and its one +/// arithmetic step (`step_in_range`). Self-contained, `core` only. +#[path = "../../wai-rs/src/staged/predict.rs"] +pub mod staged_predict; +#[path = "../../wai-rs/src/staged/wir1_arith.rs"] +pub mod staged_wir1_arith; diff --git a/wai/wai-web/demo/verify_pack.mjs b/wai/wai-web/demo/verify_pack.mjs index e7cfce242..1b0524844 100644 --- a/wai/wai-web/demo/verify_pack.mjs +++ b/wai/wai-web/demo/verify_pack.mjs @@ -3,7 +3,8 @@ // envelope can name one (the WAI1 capability or fallback; a WAI2 component in // either role), and still write an envelope that names none. The candidates are // read from the committed export wai/registry/capabilities.json, so a newly -// registered candidate is checked without editing this script. +// registered candidate is checked without editing this script. They also refuse a +// manifest that names a member twice in one object (SPEC §2), which no reader reads. // // node verify_pack.mjs [path/to/wai_web.js] [path/to/capabilities.json] // The package path is relative to this directory (as in the other verify scripts); @@ -55,6 +56,23 @@ for (const cand of candidates) { refuses(`packMulti, ${cand} as a companion`, () => wai.packMulti(v2([['wai.video.av1'], [cand, 'wai.video.av1']]), [bytes, bytes])); } +// A manifest that names a member twice in one object is refused (SPEC §2): every +// reader refuses it, so a writer never emits it. +function refusesTwice(what, write) { + try { + write(); + fail(`${what}: written, but no reader reads it`); + } catch (e) { + if (!String(e.message ?? e).includes('twice')) fail(`${what}: refused for another reason: ${e.message ?? e}`); + } +} +const v1Text = v1('wai.image.png'); +refusesTwice('pack, a member twice at the top', () => wai.pack(v1Text.slice(0, -1) + ',"zz":1,"zz":2}', bytes)); +refusesTwice('pack, a member twice in target', () => wai.pack(v1Text.replace('"target":null', '"target":{"w":1,"w":2}'), bytes)); +refusesTwice('pack, a member twice in model_requirement', + () => wai.pack(v1Text.replace('"capability":"wai.image.png"', '"capability":"wai.image.png","q":1,"q":2'), bytes)); +refusesTwice('packMulti, a member twice in an entry', + () => wai.packMulti(v2([['wai.video.av1']]).replace('"kind":"bytes"', '"kind":"bytes","zz":1,"zz":2'), [bytes])); // An envelope that names no candidate is still written, and parses back. try { const one = wai.pack(v1('wai.image.png', 'wai.image.jpeg'), bytes); @@ -64,6 +82,6 @@ try { } catch (e) { fail(`an envelope with no candidate was refused: ${e.message ?? e}`); } -console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')})` - : '\n✗ a browser writer emits a candidate'); +console.log(pass ? `PASS pack and packMulti refuse every candidate (${candidates.join(', ')}) and a member named twice` + : '\n✗ a browser writer emits what no reader may take'); process.exit(pass ? 0 : 1); diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs index c7c5f5a5e..11819025a 100644 --- a/wai/wai-web/src/lib.rs +++ b/wai/wai-web/src/lib.rs @@ -479,6 +479,14 @@ fn pack_refusal(m: &wai::container::Manifest, payload: &[u8]) -> Result<(), Stri Ok(()) } +/// A manifest a writer emits as given, read as every reader reads it (SPEC §2): a +/// member named twice in one object, at any depth, is refused, so a writer never emits +/// an envelope the reader refuses. +fn read_manifest(json: &str) -> Result { + wai::container::check_unique_members(json.as_bytes()).map_err(|e| format!("manifest parse: {e}"))?; + serde_json::from_str(json).map_err(|e| format!("manifest parse: {e}")) +} + /// What [`pack_multi`] refuses before it writes, once it holds the payloads: a /// `security` member that is not a security object (SPEC §6.1), and a component whose /// payload is not the format SPEC §5 pins its integer capability to. @@ -499,11 +507,11 @@ fn pack_multi_refusal(manifest: &wai::container::ManifestV2, payloads: &[Vec /// that names a candidate capability as its capability or fallback, which an /// encoder MUST NOT emit (SPEC §5, *Candidate capabilities*), and a payload that is /// not the format SPEC §5 pins its integer capability or fallback to. The manifest is -/// written as given. +/// written as given, so a manifest that names a member twice in one object is refused +/// (SPEC §2), as every reader refuses it. #[wasm_bindgen(js_name = pack)] pub fn pack(manifest_json: &str, payload: &[u8]) -> Result, JsError> { - let m: wai::container::Manifest = serde_json::from_str(manifest_json) - .map_err(|e| JsError::new(&format!("manifest parse: {e}")))?; + let m: wai::container::Manifest = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?; pack_refusal(&m, payload).map_err(|e| JsError::new(&e))?; let mb = manifest_json.as_bytes(); let mut out = Vec::with_capacity(12 + mb.len() + payload.len()); @@ -653,8 +661,7 @@ pub fn parse_multi(bytes: &[u8]) -> Result { /// to (SPEC §5). #[wasm_bindgen(js_name = packMulti)] pub fn pack_multi(manifest_json: &str, payloads: js_sys::Array) -> Result, JsError> { - let manifest: wai::container::ManifestV2 = serde_json::from_str(manifest_json) - .map_err(|e| JsError::new(&format!("manifest parse: {e}")))?; + let manifest: wai::container::ManifestV2 = read_manifest(manifest_json).map_err(|e| JsError::new(&e))?; wai::container::check_component_candidates(manifest.renditions.iter().map(|r| r.capability.as_str())) .map_err(|e| JsError::new(&e.to_string()))?; let n = payloads.length() as usize; @@ -714,6 +721,29 @@ mod tests { /// `WIV1` cut short, and a keyframe-0 `WIV1` under `wai.video.int_hyper`, as the /// capability or the fallback of a `WAI1` and as a `WAI2` component. The committed /// payloads pass. + /// The writers read a manifest as every reader does (SPEC §2): a member named twice + /// in one object, at the top, in `target`, in `model_requirement`, nested, or in a + /// `WAI2` entry, is refused; the same manifest without the repeat is read. + #[test] + fn the_writers_refuse_a_member_named_twice() { + use wai::container::{Manifest, ManifestV2}; + let ok = r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":null}"#; + assert!(super::read_manifest::(ok).is_ok()); + for twice in [ + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":null,"zz":1,"zz":2}"#, + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":{"w":1,"w":2}}"#, + r#"{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{"capability":"wai.image.png","q":1,"q":2},"conditioning":{"kind":"png"},"target":null}"#, + r#"{"wai":"1.0","media":"image","intent":"replicate","model_requirement":{"capability":"wai.image.png"},"conditioning":{"kind":"png"},"target":{"a":[{"b":1,"\u0062":1}]}}"#, + ] { + let e = super::read_manifest::(twice).unwrap_err(); + assert!(e.contains("twice"), "{twice}: {e}"); + } + let v2 = r#"{"wai":"1.1","media":"video","intent":"improve","target":null,"renditions":[{"capability":"wai.image.png","kind":"png"}]}"#; + assert!(super::read_manifest::(v2).is_ok()); + let e = super::read_manifest::(&v2.replace(r#""kind":"png""#, r#""kind":"png","zz":1,"zz":2"#)).unwrap_err(); + assert!(e.contains("twice"), "{e}"); + } + #[test] fn the_writers_refuse_a_payload_that_is_not_the_pinned_format() { use wai::container::{Conditioning, Manifest, ManifestV2, ModelRequirement};