diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 6fd8ec5b4..2ce932959 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -1,13 +1,17 @@ name: wai-pack # The reference packager of WAI's packaging extension (wai/extensions/packaging.md) and -# its conformance corpus (wai/pack-conformance). Three jobs: +# its conformance corpus (wai/pack-conformance). Four jobs: # # - legs: on each architecture of the fleet, clippy with float arithmetic denied (no # float decides anything in a package), the tests in release with a floor on how many -# each target lists, and the corpus recomputed by the crate. +# each target lists, and the corpus recomputed by the crate: the golden packages +# packed again byte for byte and verified, every mutation's reason. # - independent: the corpus recomputed by its second implementation, written in Python -# from the extension text and sharing no code with wai-pack. +# from the extension text and sharing no code with wai-pack, the golden packages +# verified and re-derived from their sources among it. +# - media: the golden's floor, its CMAF initialization segment and fragments, read and +# decoded by a media prober (test 14). # - licences: every crate in wai-pack's dependency graph is under a licence deny.toml # allows; a source-available licence fails it. # @@ -90,8 +94,11 @@ jobs: echo "$target: $n tests (floor $floor)" [ "$n" -ge "$floor" ] || { echo "::error::$target lists $n tests, under its floor of $floor"; fail=1; } done <<'EOF' - --lib 17 + --lib 28 --test=conformance 1 + --test=packs 7 + --test=review 9 + --test=refusals 5 --test=scenecut 3 --test=source_alloc 1 EOF @@ -109,8 +116,14 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.11' - # Standard library only (tomllib, hashlib); BLAKE3 comes from the shared pure-Python - # module beside the claim corpus. + # The standard library (tomllib, hashlib); BLAKE3 from the shared pure-Python module + # beside the claim corpus; the package receipts' Ed25519 equation from the + # `cryptography` package, pinned with its dependencies by SHA-256 so pip installs only + # the files the requirements name. + - name: Install the Ed25519 library (hashes required) + shell: bash + working-directory: wai + run: python -m pip install --require-hashes -r pack-conformance/requirements-verify.txt - name: pack_verify.py working-directory: wai shell: bash @@ -150,6 +163,37 @@ jobs: working-directory: wai/wai-pack run: python3 ../pack-conformance/segment_diff.py target/release/wai-pack 1 2000 + media: + name: wai-pack · the floor through a media prober + runs-on: linux + steps: + - uses: actions/checkout@v5 + - name: Install a media prober + shell: bash + run: | + SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq ffmpeg + # Test 14: the golden's init.mp4 and its fragments, in its floor playlist's order, + # read as one AV1 track of every frame of the source, with a key frame at each piece + # start of the plan and nowhere else, and decode without an error. + - name: The golden's floor reads and decodes + shell: bash + working-directory: wai/pack-conformance/packs/video-base-01/package + run: | + set -euo pipefail + tmp=$(mktemp -d) + cat floor/v/init.mp4 $(grep -v '^#' hls/classic/floor-v.m3u8 | sed 's#^\.\./\.\./##') > "$tmp/floor.mp4" + ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=codec_name,width,height,nb_read_frames -of default=nw=1 "$tmp/floor.mp4" | tee "$tmp/probe.txt" + grep -qx 'codec_name=av1' "$tmp/probe.txt" + grep -qx 'nb_read_frames=200' "$tmp/probe.txt" + keys=$(ffprobe -v error -select_streams v:0 -show_entries packet=pts,flags -of csv=p=0 "$tmp/floor.mp4" | awk -F, '$2 ~ /K/ {print $1}' | tr '\n' ' ') + want=$(tr '\n' ' ' < plan-v.txt) + echo "key frames: $keys" + [ "$keys" = "$want" ] || { echo "::error::key frames at $keys; the plan says $want"; exit 1; } + ffmpeg -v error -xerror -i "$tmp/floor.mp4" -f null - + echo "the floor reads as one AV1 track of 200 frames, a key frame at each piece start" + licences: name: wai-pack · dependency licences runs-on: linux diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index b38b52746..ec3201af2 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -205,6 +205,40 @@ wire format may change and such changes are recorded here. code and each family reports how many cases it generated and how many were refused. - No envelope byte or receipt byte changes for existing material. +### Packaging: objects, the stage index, the floor, catalogs and the package receipt + +- **Objects** ([packaging](extensions/packaging.md) §5): each one envelope, named by its + receipt content hash, with no manifest member naming a position, so a repeated piece + is one object. The packer's base encode calls `choose_q` per piece. +- **The stage index** `WSIX` (§6, `wai-rs` `stage_index`, feature `video_package`): per + component, every object of the package by content hash, its lengths, role, layer, + the tier and decoded-output digest of the prefix it completes, its SSE, and the class + plan; its digest and a Merkle stage root; a named reason for every rule a verifier + applies to an index and to the objects it lists. +- **The floor** (§7): a supplied AV1 floor, checked against the keyframe plan + (`wai-pack plan --out`) and the listing the policy pins, written as floor objects + that carry `derived_from` and as CMAF files (`init.mp4` with `av01`/`av1C`, one + fragment per piece). +- **SPEC §7.1, *Derived renditions***: `derived_from` on a manifest or a rendition + entry states that a payload presents another object and was made from it; an + OPTIONAL member under the MINOR rule that never breaks a parse (§8). +- **Catalogs** (§8): on http-delivery's Mode N, a classical master and floor playlist + that name nothing of WAI; per class, its master (with the one SESSION-DATA line + http-delivery allows), its WAI playlist with the codecs union and floor, and a WAI + media playlist adjacent to the floor's, with `#EXT-X-GAP` for a piece the class + takes as floor only and a `#WAI-LAYER` line for each refinement layer it lists (no + package of this revision carries one: the packer refuses refinement rungs, §11.4); + the classical and per-class DASH MPDs; the MoQ catalog; and a stage view per class. +- **The version-2 package receipt** (§9, `wai-rs` `package`): binds the policy digest + and each component's index by digest and stage root, with entry, object and byte + counts every verifier recomputes; RFC 8785 JSON both ways; a measurement claim beside + a metered figure. The version-1 receipt gains the same JSON form and keeps its + bytes. Both share the figure profile `wai:video-package-receipt-id`. +- **`wai-pack pack`, `verify` and `listing`**, and the corpus's golden package + `video-base-01`, its index, object and receipt mutations with their reasons, and + version-1 receipts as JSON. The registry lists packaging's wire identifiers. +- No envelope byte or receipt byte changes for existing material. + ### Packaging: a declared policy, its digest, and pieces cut at scene changes - **The extension** [packaging](extensions/packaging.md), first sections: a package is diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index b6561bcd9..094584e24 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -56,8 +56,9 @@ Two further tables hold strings that look like capabilities and are not: A third holds the **wire identifiers**: the names the container, its delivery bindings ([http-delivery](extensions/http-delivery.md), -[moq-streaming-format](extensions/moq-streaming-format.md)) and the key discovery -they serve ([jwp-receipts](extensions/jwp-receipts.md) §7) put on the wire outside a +[moq-streaming-format](extensions/moq-streaming-format.md)), the key discovery +they serve ([jwp-receipts](extensions/jwp-receipts.md) §7) and +[packaging](extensions/packaging.md) put on the wire outside a manifest — magic numbers, a media type, `codecs` prefixes, paths, playlist tags and attributes, document kinds and catalog fields — each with its kind, the section that defines it, and whether it is defined, provisional (test material only until @@ -274,13 +275,18 @@ and a role are fixed once registered. | `WAI2` | magic | defined | SPEC §7.1 | | `WAI_RECEIPT` | catalog | defined | moq-streaming-format §5 | | `WHP1` | magic | defined | http-delivery §6.2 | +| `WSIX` | magic | defined | packaging §6 | | `X-WAI-FLOOR` | playlist-attribute | defined | http-delivery §11.1 | | `application/wai` | media-type | proposed | http-delivery §3 | | `mr/` | path | defined | http-delivery §8.6 | | `rcpt/` | path | defined | http-delivery §8.1 | +| `science.transaction.wai.class` | data-id | defined | packaging §8 | | `science.transaction.wai.multivariant` | data-id | defined | http-delivery §11.1 | +| `science.transaction.wai.stages` | data-id | defined | packaging §8 | +| `tag:transaction.science,2026:wai/class` | scheme | defined | packaging §8 | | `tag:transaction.science,2026:wai/floor` | scheme | defined | http-delivery §11.1 | | `tag:transaction.science,2026:wai/http-delivery` | scheme | defined | http-delivery §11.1 | +| `tag:transaction.science,2026:wai/stages` | scheme | defined | packaging §8 | | `wai` | catalog | defined | moq-streaming-format §3 | | `wai-key-status` | document-kind | defined | jwp-receipts §7.9 | | `wai-module-grant` | document-kind | defined | jwp-receipts §7.9 | @@ -292,6 +298,8 @@ and a role are fixed once registered. | `wai2` | fourcc | provisional | http-delivery Appendix B | | `waiC` | fourcc | provisional | http-delivery Appendix B | | `wai_capabilities` | catalog | defined | moq-streaming-format §4 | +| `wai_delivery_timeout_ms` | catalog | defined | packaging §8 | | `wai_determinism` | catalog | defined | moq-streaming-format §4 | +| `wai_layer` | catalog | defined | packaging §8 | | `wai_receipts` | catalog | defined | moq-streaming-format §4 | diff --git a/wai/SPEC.md b/wai/SPEC.md index d9ce33945..fe2abf1a8 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -622,7 +622,7 @@ registration and makes no conforming envelope non-conforming (§8). | `wai.video.manifest` | ATTESTED PER-SESSION MANIFEST — the OTT delivery layer, not a codec: a personalised HLS manifest (ads stitched at `EXT-X-DISCONTINUITY`, blackouts + geo enforced) rendered as a **pure function of the session *class*** `(content, entitlement, geo, blackout set, ad pod, window)` rather than the viewer, and sealed in an Ed25519 receipt that binds the JCP grant (capability, joule ceiling, `funds_ceiling` = ad budget), the composition, the stitched ads and the policy applied. A receipt over a WAI playlist MAY carry `render` and `extras`, the rendering parameters and per-segment lines that playlist adds, both bound through `manifest_hash` ([http-delivery](extensions/http-delivery.md) §8.6) | **manifest-equivalence**: `manifest_hash == hash(render(segments))` and `session_class_hash` are recomputable by anyone holding the bytes, so the ads, blackouts and compose-work stop being dashboard claims; class-determinism means one generation serves every viewer of the class, and `uj_per_viewer` is the amortised energy that a 1:1-unique generator cannot report below its per-viewer cost. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_manifest.rs` | | `wai.video.ssai` | ATTESTED SERVER-SIDE AD INSERTION — a **deterministic** ad decision (greedy by price within the avail duration and the funds remaining) plus a per-impression Ed25519 receipt binding the candidate pod, the manifest it belongs to, the served `creative_hash`es and the VAST quartile each reached | **spend-equivalence**: `budget_spent_micros` is recomputed from the *billable* impressions and rejected if it exceeds the grant's `funds_ceiling` — an overspent receipt fails verification; every creative and quartile is checkable, so a buyer and seller reconcile impressions from the same bytes instead of two logs that only a dashboard joins. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_ssai.rs` | | `wai.video.keys` | ATTESTED CONTENT-KEY RELEASE — the DRM licensing decision as a record: every key release, and every refusal, is an Ed25519 receipt binding the KID, the DRM system, the entitlement demanded and the window enforced, with a BLAKE3 `key_commit` proving *which* key was released without carrying it | **decision-equivalence**: `verify()` re-runs the release policy over the receipt's own `(grant capability, entitlement, issued, expiry)` and rejects any misreported outcome, and enforces granted-iff-committed — so "keys went only to entitled, unexpired sessions" is checkable against the signed log, not asserted. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_keys.rs` | -| `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_package.rs` | +| `wai.video.package` | DETERMINISTIC PACKAGING + ATTESTED SEGMENTMAP — a source sliced into content-hashed segments on GOP boundaries; the ordered `(index → content_hash)` map is a signed Merkle commitment, and identical content (ads, bumpers, replays, simulcast overlap) dedups to one stored/encoded copy | **map-equivalence + verifiable dedup**: `segment_map_root` lets a `wai.video.manifest` reference be checked against the package that produced it, and `verify()` recomputes `unique_segments`/`stored_bytes` from the segments so the storage/encode saving cannot be overstated; `uj_per_unique_segment` is the encode-once energy. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable. Version 2 ([packaging](extensions/packaging.md) §9) binds the policy digest and one stage index per component, names each object by its jwp-receipts §2.1 content hash, and covers every media class a packager packs; the string keeps `video` for history | `wai_video_package.rs`, `package.rs` | | `wai.video.channel` | VIRTUAL LINEAR / FAST ASSEMBLY — a continuous schedule of assets over wall-clock time, sliced into attested playback windows; windows chain via `parent_receipt_hash`, so a channel's history is a hash chain of program-as-run records | **schedule-continuity + recomputable program-as-run**: `verify()` rejects a schedule with a gap or overlap (no dead air, no double-booking) and recomputes `aired` from the schedule and the window, so what actually aired is checkable by a licensor, advertiser or regulator against the signed chain rather than an operator-held play-out log. Only the measurement — `joules_micro`, its acquisition class and the class's evidence — is measured-attested: signed, and not re-measurable | `wai_video_channel.rs` | **Sink-architecture note:** the inter-frame neural video codecs (DCVC-RT, @@ -1876,6 +1876,35 @@ never picks a refinement by capability; and since refinements follow every selec 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. +### Derived renditions + +A `WAI1` manifest (§3), or a rendition entry, MAY carry `derived_from`. It states that +its payload presents another object, the **motif**, and was made from it by a named +renderer or encoder: + +```json +"derived_from": { "motif": "<64 hex>", "motif_capability": "", + "renderer": "@", "settings": "<64 hex>" } +``` + +- `motif` is the motif's content hash ([jwp-receipts](extensions/jwp-receipts.md) + §2.1), 64 lowercase hexadecimal digits. +- `motif_capability` is the motif's capability, and `renderer` the renderer or encoder + that made the payload; each a non-empty string. +- `settings` is `BLAKE3("wai:render-settings\x01" ‖ JCS(settings))`, over the settings + the renderer was run with as RFC 8785 canonical JSON, 64 lowercase hexadecimal + digits. + +A sink MUST NOT present such a payload's output as meeting the motif's equivalence +criterion, its tier or a `replicate` intent: the payload presents the motif; it does not +reconstruct it. A verifier checks `motif` against a motif it holds and reports every +other member as the producer's statement, except where a profile fixes them: +[packaging](extensions/packaging.md) §7 fixes a supplied floor's `renderer` and +`settings`, and a package's verifier holds them to it. A value of another form makes the member +malformed; the envelope still parses (§8). A reader of an earlier revision ignores the +member and loses only that statement. [packaging](extensions/packaging.md) §7 writes it +on every floor object. + ### v1.0 compatibility v1.0 envelopes (`WAI1`) remain valid and their format is unchanged, except that @@ -1972,7 +2001,9 @@ envelope that uses any string a later revision registers. 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`). + parses, and §7.1 reports it (`component-manifest-mismatch`). `derived_from`, on a + manifest and on a rendition entry (§7.1, *Derived renditions*), is defined the same + way: an envelope whose `derived_from` is malformed still parses. - 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 diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index ec09f87fe..4abc02d86 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -3,9 +3,12 @@ > Status: Draft. A packager turns sources into WAI objects under a declared policy; > this extension fixes what a verifier recomputes. This revision defines the policy, > its digest and what it may ask a packager to emit (§3), how a source is cut into -> pieces (§4), and the integer video payloads a packager writes (§5.5). Reference -> impl: `wai-pack` — `policy`, `capcheck`, `source`, `scenecut`, `classify`; the -> encoder `wai-rs` `int_video_enc`; the corpus +> pieces (§4), the objects a packager writes (§5), the stage index (§6), the floor +> (§7), what each class is offered over HTTP and MoQ (§8), the package receipt (§9), +> and the package and its verification (§11). Reference impl: `wai-pack` — `policy`, +> `capcheck`, `source`, `scenecut`, `classify`, `encode`, `floor`, `cmaf`, `catalog`, +> `pack`, `verify`; in `wai-rs`, the encoder `int_video_enc`, the stage index +> `stage_index` and the receipt `package`; the corpus > [`pack-conformance/`](../pack-conformance/). Keywords **MUST**, **MUST NOT**, > **SHOULD**, **MAY** are RFC 2119/8174. @@ -37,7 +40,8 @@ where it names them. **3.2** The **effective policy** is the policy with every default written in (§3.3) and every override the packager applies (such as a command line forcing -`royalty_free_only`). Its JSON form is §3.3's members, with an optional member that +`royalty_free_only`). An override only ever forces `royalty_free_only` on; it never +turns off what the policy asks for. Its JSON form is §3.3's members, with an optional member that has no default omitted when absent, and arrays of tables in document order. Its **policy digest** is @@ -278,15 +282,35 @@ The kind is the first that holds of: **static** (`mad_milli ≤ static_mad_milli ## 5. Objects -**5.1–5.4** Reserved: an object's identity, its independence of its position in a -package, its name, and `refines` on a refinement object are specified with the -packager stage that writes objects. +**5.1** *One envelope.* Each object is exactly one `WAI1` or `WAI2` envelope and +nothing after it ([http-delivery](http-delivery.md) §5.1). + +**5.2** *Position independence.* An object's bytes **MUST NOT** depend on its position +in a package or on the package it belongs to: no member of its manifest names a piece, +a time, an asset, a class or a component, so two pieces with identical content are one +object. The reference packager writes a base's manifest as SPEC §3 orders it, with +`media` the component's media, `intent` `replicate`, `model_requirement` the rung's +capability with no `fallback` member, `conditioning.kind` the capability's last +dot-separated segment (`int_motion`, `av1`), and `target` `{"frames": n, "h": h, "w": w}` +(the piece's frame count and dimensions; for a floor, its sequence header's maximum +frame height and width); a floor's `derived_from` (§7.2) comes after `conditioning`. + +**5.3** *Name.* An object is named by its content hash ([jwp-receipts](jwp-receipts.md) +§2.1), `BLAKE3("moq-jwp:object\x01" ‖ envelope)`, as `objects/<64 lowercase hex>.wai`: +the same value is its stage-index entry (§6), its receipt leaf, its MoQ content hash and +its HTTP strong `ETag` ([http-delivery](http-delivery.md) §5.2). + +**5.4** *Refinements.* A refinement object carries `refines` +([staged-delivery](staged-delivery.md) §3): `base` the content hash of its piece's base, +`layer` its layer, and as producers in `prev` only objects of the layers below it, the +object its entry refines (§6.2) among them. The reference packager does not write +refinements yet. **5.5** *Integer video.* An object of `wai.video.int_motion` carries a `WIV1` payload with keyframe 0, as SPEC §5 pins it ([integer-payloads](integer-payloads.md) §3); this extension defines no other layout for it, and its decode is that section's. The -reference packager does not write objects yet; when it does, it will write these with -one encoder (`wai-rs` `int_video_enc`) at the step §5.6 chooses. The encoder's choices +reference packager writes these with one encoder (`wai-rs` `int_video_enc`) at the step +§5.6 chooses. The encoder's choices make a payload a function of the source frames, the rung's `block`, `search` and `mv_precision` (§3.3), and the step `q`: @@ -332,11 +356,619 @@ candidates that meet it alike are decided by their step. Where no candidate meet it chooses the smallest. The function the reference packager's base encode calls is `wai_pack::encode::choose_q`. -## 6–11. Stage index, floor, classes and delivery, receipt +## 6. Stage index -Reserved. This revision does not yet define a package's stage index, its floor, its -class plans and catalogs, its receipt, royalty-free packaging of a whole package or -re-pack equivalence; they are specified with the packager stages that write them. +**6.1** A package carries, per component, one stage index, the file `index-.wsix` +(`WSIX`), per window: one window for on-demand content, a chain of windows for live +(§9.2). A window holds whole pieces. Integers are little-endian. + +**6.2** *Layout.* A 160-byte header, then `n_pieces` piece records of 32 bytes, +`n_entries` entry records of 128 bytes, `n_classes` class records of 16 bytes, the class +plan, and the string table. Nothing follows the string table. Reserved and padding +bytes are zero. + +| Offset | Size | Header field | +|---|---|---| +| 0 | 4 | `"WSIX"` | +| 4 | 2 | version, 1 | +| 6 | 2 | `media_class`: 0 video, 1 image, 2 audio, 3 world, 4 feed, 5 avatar, 6 film | +| 8 | 4 | `tb_num` | +| 12 | 4 | `tb_den` | +| 16 | 4 | `n_pieces` | +| 20 | 4 | `n_entries` | +| 24 | 4 | `n_strings` | +| 28 | 4 | `n_classes` | +| 32 | 32 | `policy_digest` (§3.2) | +| 64 | 32 | `prev_index_digest`: the previous window's index digest; zero for on-demand content and a first window | +| 96 | 8 | `first_piece`: the absolute number of the window's first piece | +| 104 | 8 | `first_entry`: the absolute ordinal of the window's first entry across the chain | +| 112 | 32 | `plan_digest` (§4.2) of the pieces' starts | +| 144 | 2 | `component`: the component's id, as a string index | +| 146 | 14 | reserved | + +| Offset | Size | Piece record field | +|---|---|---| +| 0 | 8 | `start`, in timebase units | +| 8 | 4 | `duration`, in timebase units | +| 12 | 4 | `first_entry`, window-relative | +| 16 | 2 | `n_entries` | +| 18 | 1 | `kind`: 0 natural, 1 static, 2 synthetic, 3 motion_high (§4.6), 4 world segment, 5 audio | +| 19 | 1 | flags: bit 0 scene cut, bit 1 forced at `max` (§4.3), bit 2 started by a cue, bit 3 aligned to the lead (§4.5) | +| 20 | 4 | `rules`: bit *i* set when rule *i* matched the piece (§8.1) | +| 24 | 4 | `moments`: bit *i* set when the piece lies in moment *i* (§8.1) | +| 28 | 4 | reserved | + +| Offset | Size | Entry record field | +|---|---|---| +| 0 | 32 | `content_hash` (§5.3) | +| 32 | 8 | `byte_len`: the whole envelope's length | +| 40 | 8 | `piece`: the absolute piece number | +| 48 | 4 | `refines`: the window-relative index of the entry this one refines; `0xFFFFFFFF` for none | +| 52 | 2 | `capability`, as a string index | +| 54 | 1 | `role`: 0 base, 1 refinement, 2 floor, 3 snapshot, 4 ops, 5 receipt, 6 asset | +| 55 | 1 | `layer` | +| 56 | 1 | `prefix_tier`: 0 none, 1 entropy-consistency, 2 decode-equivalence (§6.4) | +| 57 | 1 | flags: bit 0 deferrable, bit 1 duplicate (an earlier entry of the index has the content hash), bit 2 the manifest carries `derived_from` | +| 58 | 2 | reserved | +| 60 | 4 | `deadline_ms`: 0 for none (§8.6) | +| 64 | 32 | `prefix_digest` (§6.5) | +| 96 | 8 | `sse` (§6.6); `2^64 − 1` when not computed | +| 104 | 4 | `manifest_len`: the envelope's `man_len` | +| 108 | 4 | reserved | +| 112 | 8 | `payload_len`: the envelope's payload bytes (for a `WAI2`, every component's) | +| 120 | 8 | reserved | + +`byte_len`, `manifest_len` and `payload_len` are the envelope's own lengths; for an +object whose payload format WAI checks they are its counted bytes +([staged-measurement](staged-measurement.md) §3), so a verifier also requires each such +payload to parse as its format, after the other object rules of §6.3 +(`object-lengths`). + +A class record is `u16` name (a string index), `u8 max_layer`, `u8` flags (bit 0 +floor only), `u32 max_kbps` (0 for none) and 8 reserved bytes, in the policy's class +order. The class plan is `n_classes` rows of `n_pieces` bytes, each the highest layer +the class lists for the piece or `0xFF` for the floor only (§8.1), zero-padded to a +multiple of 8. The string table is `n_strings` strings, each a `u16` length and that +many bytes of UTF-8, at least one, zero-padded to a multiple of 8. It is the component's +id, then the class ids in policy order, then each capability at its first use, and a +package's verifier holds an index to that (§11.3). + + index_digest = BLAKE3("wai:pack-stage-index\x01" ‖ file) + leaf_i = BLAKE3("wai:pack-stage\x01" ‖ entry record i) + stage_root = the jwp-receipts §2.2 Merkle root over the leaves + +A reader **MUST** refuse a file that is not this layout, the first that applies, in +this order: + +1. its first four bytes are not `WSIX`, a file of fewer included (`index-magic`); +2. a file of fewer than six bytes (`index-truncated`), or a version other than 1 + (`index-version`); +3. a file shorter than the header, or than the tables its counts give: the piece, entry + and class records, the class plan with its padding, each string's length and bytes, + and the string table's padding, every size taken from its count before anything is + allocated (`index-truncated`); +4. a reserved or padding byte that is not zero (`index-reserved`); +5. a media class with no meaning (`index-media-class`); then, entry by entry, a role, + then a tier, with none (`index-role`, `index-tier`); +6. a string that is empty or not UTF-8, then a string index past the table + (`index-string`); +7. bytes after the string table's padding (`index-trailing`). + +**6.3** *Rules.* An index lists every WAI object of its component, in piece order. +Files that are not WAI envelopes (CMAF initialization segments and fragments, FLAC +files) **MUST NOT** appear in it. A verifier **MUST** refuse an index that breaks one +of these, checked in this order: + +1. A timebase of zero (`index-timebase`). An index with no previous window + (`prev_index_digest` zero: on-demand content, or a chain's first window) whose + `first_piece` or `first_entry` is not 0, or whose first piece does not start at 0, the + start of the presentation timeline (§4.5) (`window-start`). Then piece by piece: a + piece that lasts no time or does not start where the one before it ended + (`piece-order`); whose + `first_entry` is not the number of entries before it, or that has none + (`piece-entries`); of a kind or with a flag that has no meaning (`piece-kind`). Then + pieces whose entries are not the index's entries (`piece-entries`). +2. Entry by entry, in order: an entry that does not name its piece's absolute number + (`piece-entries`); a flag with no meaning (`entry-flags`); within its piece, a role + before one that comes earlier in the order base, refinement, floor, any other, or a + refinement at a lower layer than the refinement before it (`entry-order`); an entry + that is not a refinement above layer 0, a refinement at layer 0, or `refines` on an + entry that is not a refinement or missing from one (`layer-role`); a + refinement whose `refines` is outside the window (`refines-outside-window`), is not + an earlier entry of its own piece (`refines-mismatch`) or is not one layer below it + (`layer-gap`); its tier (§6.4: `tier-entropy-passthrough`, `tier-weakest`, then + `tier-mismatch`); a non-zero `prefix_digest` with a tier other than + decode-equivalence (`digest-without-tier`); a deadline on an entry that is not a + refinement (`deadline-role`); a floor without flag bit 2 (`floor-derived-from`); a + content hash an earlier entry has at another length (`dup-length`); the duplicate + flag other than set exactly when an earlier entry has the content hash (`dup-flag`). +3. Class by class: a flag with no meaning (`class-flags`); a plan value other than + `0xFF` for a floor-only class, or other than `0xFF` or at most both the class's + `max_layer` and the piece's highest layer otherwise (`class-plan`). + +With the objects in hand, a verifier **MUST** also refuse, entry by entry: bytes of +another length (`object-length`) or that do not hash to the entry's content hash +(`object-hash`); bytes that are not one `WAI1` or `WAI2` envelope and nothing else +(`non-envelope-entry`); a `man_len` or a payload length other than the entry's +(`object-lengths`); a capability other than the entry's (`object-capability`; for a +`WAI2`, its first entry's); a refinement whose object is not a `WAI1` whose `refines` +names its piece's base, its layer and producers of the layers below among which the +entry it refines (§5.4, `object-refines`); and `derived_from` (SPEC §7.1) present +otherwise than bit 2 says, or not well formed (`object-derived-from`). A member whose +value is `null` is absent; for a `WAI2`, `derived_from` is present when any entry +carries it, and well formed when every entry that carries it is. A manifest that does +not parse, or names a member twice, is not an envelope (`non-envelope-entry`). + +**6.4** *Tier.* The tier a capability's own decode holds is decode-equivalence for a +capability whose registry class reconstructs exactly (`StandardDefined`, and +`NeuralIntegerExact`, whose pin a sink verifies before it decodes), +entropy-consistency for `NeuralEntropyExact`, and none otherwise (SPEC §5, §7). + +- An entry at layer 0 states its capability's tier, except a floor, which states none + (§7.4). +- A prefix with one or more refinements holds decode-equivalence only if every object + along it does, and none otherwise ([derived-tracks](derived-tracks.md) §3, rules 1 + and 2): a refinement's tier is decode-equivalence exactly when the entry it refines + states decode-equivalence and its own capability's tier is decode-equivalence. +- An entry with `layer` > 0 **MUST NOT** state entropy-consistency + (`tier-entropy-passthrough`); one with `layer` > 0 that states decode-equivalence over + a prefix that does not hold it is refused `tier-weakest`; any entry stating another + tier than the one this section gives, `tier-mismatch`. + +**6.5** `prefix_digest` is the SHA-256 decoded-output digest (SPEC §7, *Digests*) of the +prefix the entry completes when `prefix_tier` is decode-equivalence, and all zero +otherwise: for video, the clip digest over RGB8 frames; for an image, the RGB8 frame +digest; for audio, the digest of the s16le interleaved samples; for world, feed, avatar +and film entries, zero (their state hashes live in their receipts). A verifier decodes +the prefix and compares. + +**6.6** `sse` is the integer sum, over every output sample of the prefix, of the +squared difference from the source sample the packager coded (RGB8 channels, or s16 +samples). It is the producer's statement: a verifier holding the source recomputes it, +and one without the source cannot. A floor's `sse` is `2^64 − 1`. + +## 7. Floor and `derived_from` + +**7.1** Every piece of a video, image or audio component **MUST** have a floor rendition +in a capability of SPEC §5's mandatory floor or recommended set; a packager refuses a +video component with no floor rung (`floor-missing`). A film or world package SHOULD +have one. This revision packs a supplied AV1 floor (`wai.video.av1` or +`wai.video.av1.lossless`, `source = "supplied"`). + +**7.2** *Floor objects.* Each piece's floor is one object of the floor rung's capability +(§5.2), whose manifest carries `derived_from` (SPEC §7.1, *Derived renditions*) naming +the piece's base: `motif` the base's content hash, `motif_capability` its capability, +`renderer` the floor's maker, and `settings` `BLAKE3("wai:render-settings\x01" ‖ +JCS(settings))` (§3.6). For a supplied floor, `renderer` is `supplied@` and the settings are `{"source": "supplied", "supplied_listing": +""}`, so neither names a position. This profile fixes these members: they +are not the producer's statement here (SPEC §7.1), and a verifier holds every floor +object to them, to its framing header and to its target (§11.3). An AV1 floor object's payload is the +length-prefixed framing the reference sink decodes (`wai-rs` `codecs::video`): `n`, +`h`, `w`, `fps_num`, `fps_den` as `u32` little-endian (the piece's temporal units, the +sequence header's maximum frame height and width, the component's timebase `num` and +`den`), then each temporal unit as a `u32` little-endian length and its bytes as the +stream holds them. + +**7.3** *A supplied floor* is a directory holding, per video component, `.obu`: an +AV1 stream in the low-overhead OBU format (every OBU with its size field), one temporal +unit per source frame. Its listing (SPEC §3.1) of every file is pinned by the policy's +`floor.supplied_listing`. A packager **MUST** refuse, before it writes anything: + +- a directory whose listing does not hash to `floor.supplied_listing` + (`floor-listing-mismatch`), and a component with no stream in it + (`floor-stream-missing`); +- a stream that does not split into temporal units each holding a frame, whose first + unit holds no sequence header, with a sequence header whose bytes differ from the + first's, or with a unit that starts at a shown key frame (below) without a sequence + header (`floor-av1-malformed`); +- another number of temporal units than source frames (`floor-frame-count`); +- a unit that does not start at a shown key frame at a piece's first frame, or one that + does elsewhere unless `floor.extra_keyframes` is true (`floor-keyframe-mismatch`). A + unit starts at a shown key frame when its first frame header has `frame_type` + `KEY_FRAME` and `show_frame` 1, or the sequence uses the reduced still-picture header; + a unit that shows an existing frame does not. + +The plan a supplied floor is made against is the **keyframe plan**, the file +`plan-.txt`: each piece's first frame in decimal, one per line, each line ended by a +line feed. Its pieces' starts are the plan of §4.2, and `wai-pack plan` writes it from the +source and the policy before any floor is encoded. + +**7.4** The floor is presentation. Its output is never compared with the primary's, and +no tier or `replicate` claim covers it. + +**7.5** A floor produced by an encoder that is not a pure function of its input (§11) +**MUST** name that encoder and its version in `derived_from.renderer`. + +**7.6** *CMAF.* The floor of a video component is also written as CMAF files, which no +stage index lists: `floor//init.mp4`, and one fragment per piece, +`floor//.m4s`. Box layouts are ISO/IEC 14496-12's, with the +AV1 binding's sample entry; every integer is big-endian and every time 0: + +- `init.mp4`: `ftyp` (major brand `cmfc`, minor version 0, compatible `cmfc`, `iso6`, + `av01`), then `moov` holding `mvhd` (version 0, timescale `tb_num`, rate 1, volume 1, + the unity matrix, next track 2), `trak` (`tkhd` version 0 with flags 3, track 1, + layer, alternate group and volume 0, the unity matrix, the width and height in 16.16; `mdia`: `mdhd` version 0, timescale + `tb_num`, language `und`; `hdlr` `vide` named `video` with its terminating zero byte; + `minf`: `vmhd` with flags 1, + `dinf` with one self-contained `url `, and `stbl`: `stsd` with one `av01` sample entry + of the width and height, data reference 1, resolution 72 dpi, frame count 1, an empty + compressor name, depth 24 and pre-defined −1, holding `av1C`, then empty `stts`, + `stsc`, `stsz` and `stco`), and `mvex` with `trex` (track 1, sample description 1). + `av1C` is marker and version 1, then the sequence header's profile, level, tier, + bit depth, monochrome and chroma fields, no initial presentation delay, then the + sequence header OBU as the stream holds it. The width and height are the sequence + header's maximum frame width and height. +- A fragment: `moof` holding `mfhd` (sequence number piece + 1) and `traf` (`tfhd` with + default-base-is-moof, track 1; `tfdt` version 1, the piece's start × `tb_den`; `trun` + version 0 with data offset, sample duration, size and flags, the offset pointing at + the `mdat` payload), then `mdat`. Each sample is a temporal unit without its temporal + delimiter, lasting `tb_den`; its flags are `0x02000000` at a shown key frame and + `0x01010000` otherwise. + +The codecs string of the floor is the AV1 binding's short form, +`av01...`. + +## 8. Classes, catalogs and delivery + +**8.1** *What a class lists.* The stages listed for a class are a pure function of the +class, the piece and the effective policy, so every viewer of a class receives the same +catalog bytes. + +- A piece that starts at unit `start` and lasts `duration` units spans the + milliseconds from `s_ms` up to `e_ms`, the floors of `start · den · 1000 / num` and of + `(start + duration) · den · 1000 / num`; it lies in moment *i* when `s_ms < to_ms` and + `e_ms > from_ms`. Rule *i* matches it when each condition the rule states holds: its + kind is in `when.piece_kind`, and a tag of a moment it lies in is in + `when.moment_tag`. Matching rules fold in document order (§3.4). +- A floor-only class lists the floor (`0xFF`). Otherwise the class lists + `min(max_layer, the rules' then.max_layer, the piece's highest layer)`; under + `max_kbps`, while the listed prefix needs more than `max_kbps`, that is + `ceil(8 · bytes · num / (1000 · duration · den))` kilobits per second over the bytes of + the listed bases and refinements, the top layer is dropped, and a base that alone + needs more makes the piece floor-only for the class. + +**8.2** A catalog **MUST NOT** list a refinement without the stages below it. + +**8.3** *HTTP.* A package is offered in [http-delivery](http-delivery.md)'s Mode N +(§11.1 there): the classical presentation is the floor alone, and WAI objects are +offered in playlists of their own. + +- A WAI object is none of the media segment formats of RFC 8216 §3. A WAI playlist is + offered only to a sink that recognizes its codecs value from its own capability list, + and a packager **MUST** write a classical master that names nothing of WAI. +- A browser's media-capabilities query answers only for the browser's own decoders, + and **MUST NOT** be used to decide whether a `wai1.*` value can be decoded + ([http-delivery](http-delivery.md) §4.3). +- Each class's WAI playlist **MUST** name its class and its stage view. + +Durations: a piece of *d* units lasts `floor(d · den · 1000 / num)` milliseconds, +written `S.mmm` (whole seconds, a full stop, three digits). A target duration is the +largest `ceil(d · den / num)`. A rate is `ceil(8 · bytes · num / (d · den))` bits per +second; a variant's `BANDWIDTH` is the largest over the pieces it lists and +`AVERAGE-BANDWIDTH` the rate of all their bytes over all their durations. A floor +variant's bytes per piece are its fragment's; a WAI variant's are the listed bases' and +refinements', over the pieces the class does not take as floor only. Every playlist +line ends with a line feed. For component *c*, of frame size `W×H`, whose floor has +codecs string *F* and maximum frame size `FW×FH`: + +- `hls/classic/master.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, + `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="F",RESOLUTION=FWxFH`, + `floor-.m3u8`. +- `hls/classic/floor-.m3u8`: `#EXTM3U`, `#EXT-X-VERSION:7`, + `#EXT-X-TARGETDURATION:`, `#EXT-X-MEDIA-SEQUENCE:0`, `#EXT-X-PLAYLIST-TYPE:VOD`, + `#EXT-X-INDEPENDENT-SEGMENTS`, `#EXT-X-MAP:URI="../../floor//init.mp4"`, then per + piece `#EXTINF:,` and `../../floor//.m4s`, then `#EXT-X-ENDLIST`. +- `hls//master.m3u8`: the classical master's lines, with, when the class lists a + WAI object, `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"` + after `#EXT-X-INDEPENDENT-SEGMENTS`, and the variant's URI `../classic/floor-.m3u8`. +- `hls//wai.m3u8`, when the class lists a WAI object: `#EXTM3U`, + `#EXT-X-VERSION:7`, `#EXT-X-INDEPENDENT-SEGMENTS`, + `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE=""`, + `#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/.json"`, + `#EXT-X-STREAM-INF:BANDWIDTH=,AVERAGE-BANDWIDTH=,CODECS="",X-WAI-FLOOR="",RESOLUTION=WxH`, + `wai-.m3u8`. The union and the floor are [http-delivery](http-delivery.md) §4.2's + over the objects the class lists. +- `hls//wai-.m3u8`: the floor playlist's lines without `#EXT-X-MAP`, adjacent + to it ([http-delivery](http-delivery.md) §11.2); per piece, `#EXT-X-GAP` when the + class takes it as floor only, `#EXTINF:,`, its base as + `../../objects/.wai#n=`, then each refinement the class lists, in + entry order, as `#WAI-LAYER:N=,URI="../../objects/.wai#n="` + ([http-delivery](http-delivery.md) §10.4). + +DASH, the same as two MPDs ([http-delivery](http-delivery.md) §11.1), each +``, then +`` +over the component's whole duration and its target duration, ``, +one `AdaptationSet`, `` and ``, indented by two spaces a level: + +- `dash/classic.mpd`: `` + holding `` with + ``: ``, + a `SegmentTimeline` of one `` per + piece, and one `` per piece. +- `dash/.mpd`, when the class lists a WAI object: + `` + holding ``, + ``s of schemes `tag:transaction.science,2026:wai/floor` (value the + floor), `tag:transaction.science,2026:wai/class` (the class) and + `tag:transaction.science,2026:wai/stages` (`../stages/.json`), then + per layer *k* up to the highest the class lists, a `Representation` with attributes + `id="-l"`, for *k* > 0 `dependencyId="-l"`, and `bandwidth` the peak rate of its own + layer's objects over the pieces it lists, whose + `SegmentList` has a timeline entry and a `SegmentURL media="../objects/.wai"` + for each piece the class lists to layer *k* (a gap where it does not). + +The media type `application/wai` is proposed, not registered +([http-delivery](http-delivery.md) §3). This revision writes the catalogs of a package +of one component; how the variants and groups of several components are written is +defined with the packing of several components. + +**8.4** *MoQ.* `moq/catalog.json` is RFC 8785 canonical JSON and a line feed: +`{"tracks": [...], "version": 1}`, the tracks by layer, then by deadline ascending, +then the floor. Each (component, layer, deadline) is one track of +`packaging` `"wai"`: name `/l`, or `/l@` for a refinement whose +deadline is not `defaults.deadline_ms`; `renderGroup` 1; `priority` §8.5's; +`wai_capabilities` its entries' capabilities by first appearance; `wai_layer` *k*; +`wai_receipts` false (no object of this revision carries a `WAI_RECEIPT` property); +`wai_determinism` the weakest tier of its entries, absent when that is none. The base +track has `altGroup` 1 when the component has a floor. MSF's catalog defines `depends` +as the tracks on which a track is dependent, and this section has a sink receive every +one of them (below): a dependency is all of its tracks, not any one. So a refinement track +`depends` on every track that carries an entry its own entries refine, in track order — +the layer below at the same deadline, where a piece's layers share its deadline as the +reference packager writes them — and never on a track the catalog does not hold. It +carries `wai_delivery_timeout_ms` (§8.6). The floor is a track +`/floor` of `packaging` `"cmaf"` with `altGroup` 1, `renderGroup` 1, `codec`, +`width` and `height`. A sink **MUST NOT** subscribe to a track unless it also receives +every track that track depends on, so a sink that selects the floor subscribes to no +layer track. `wai_layer` and `wai_delivery_timeout_ms` are catalog fields this +extension defines under the `wai_` prefix of +[moq-streaming-format](moq-streaming-format.md) §4; a later revision of that extension +may define them there instead. + +**8.5** *Priority.* The base track's publisher priority is 64 and layer *k*'s +`64 + 32·k`: a refinement track's value is strictly greater than that of the track it +depends on, so it is delivered after it (MOQT: a lower value is delivered first). + +**8.6** *Deadline.* An entry's `deadline_ms` is the longest a refinement may arrive +after the base of its piece was published and still be presented; the rules' or +`defaults.deadline_ms`. A refinement track carries DELIVERY_TIMEOUT +`deadline_ms − live.layer_lag_ms[k]` (§3.4 has refused a policy where that is below 1). +Entries of one layer with different deadlines travel on different tracks. A relay may +drop a late refinement (MOQT §5.2), so a refinement never delays the base. + +**8.7** *Presentation.* A sink presents piece *p* at the highest prefix whose objects +all arrived and verified by *p*'s presentation instant, and **MUST NOT** delay the base +to wait for a refinement. What the receipts of a dropped object say is +[jwp-receipts](jwp-receipts.md)' to define, not this extension's. + +**8.8** *Stage views.* `stages/.json` is RFC 8785 canonical JSON and a line feed: +`{"class": "", "components": {"": {"media_playlist": +"../hls//wai-.m3u8", "pieces": [...]}}, "policy_digest": ""}`, with +`media_playlist` only when the class lists a WAI object, and each piece +`{"dur", "p", "start", "stages": [...]}` with `"floor_only": true` when the class takes +it as floor only (and `stages` empty), and each listed stage `{"bytes", "h", "layer"}`, +a refinement's with `"refines"` (the content hash of the entry it refines) and +`"deadline_ms"`. + +## 9. Package receipt + +**9.1** A version-2 package receipt binds the asset id; the policy digest; its parent +kind and parent receipt hash; per component its id, media class, index digest and stage +root; the entry, distinct-object, referenced-byte and stored-byte counts, which a +verifier recomputes from the indexes; the packager `builtin:@`; the +energy figure with its class, or the unmetered form; the grant; and the signer. Its +signing payload, big-endian: + + labelled_domain("wai:pack-receipt\x01", label) + u32 len ‖ asset_id ‖ policy_digest[32] + u8 parent_kind (0 none, 1 live-window, 2 re-pack) ‖ [parent_receipt_hash[32] if not 0] + u32 n ‖ n × ( u32 len ‖ id ‖ u8 media_class ‖ index_digest[32] ‖ stage_root[32] ) + u64 total_entries ‖ u64 unique_objects ‖ u64 total_bytes ‖ u64 stored_bytes ‖ u64 work_units + u32 len ‖ packager + u64 joules_micro ‖ write_label(label) + grant_hash[32] ‖ u32 len ‖ capability ‖ u64 joule_ceiling_micro ‖ u8 0 | u8 1 ‖ u64 funds_ceiling + signer_pubkey[32] ‖ u32 len ‖ signer_id + + receipt_hash = BLAKE3("wai:video-package-receipt-id\x01" ‖ payload ‖ sig) + +`labelled_domain` and `write_label` are [energy-measurement](energy-measurement.md) +§2.8's: unmetered, the domain as written and no label; metered, its final byte `\x02` +and the class's signed encoding. `total_entries` counts every entry; `unique_objects` +the distinct content hashes over every component; `total_bytes` the entries' +`byte_len`; `stored_bytes` the `byte_len` of each content hash's first entry; +`work_units` equals `total_entries`. A receipt whose content hash comes at two lengths +has no counts. The signature is Ed25519 (RFC 8032) by `signer_pubkey`. + +Its JSON form, `package.receipt.json`, is RFC 8785 canonical, hexadecimal lowercase, +integers as numbers no greater than `2^53 − 1` (a packager refuses to seal anything +else, `receipt-json`): `asset_id`; `components`, each `{"id", "index": +"index-.wsix", "index_digest", "media_class", "stage_root"}`; `grant` +`{"capability", "funds_ceiling", "grant_hash", "joule_ceiling_micro"}` with `null` for +no ceiling (a joule ceiling of `2^64 − 1`); `joules_micro`; `kind` `"package"`; +`packager`; `parent_kind` (`none`, `live-window` or `re-pack`); `parent_receipt_hash` +(`null` for none); `policy_digest`; `receipt_hash`; `sig`; `signer_id`; +`signer_pubkey`; `stored_bytes`; `total_bytes`; `total_entries`; `unique_objects`; +`version` 2; `work_units`; `energy_provenance`, the class's label (energy-measurement +§2.7) or `null` when unmetered; and the class's evidence members exactly as its class +carries them. Every member is present, with `null` where it has no value +(`parent_receipt_hash`, `energy_provenance`, a grant's ceilings) and nowhere else. + +A reader refuses, before anything else, JSON that is not RFC 8259 or that names a member +twice in one object, at any depth (`receipt-json`). It then reads `"version"`: absent or +1 is a version-1 receipt (§9.3), 2 a version-2 one, anything else `receipt-version`. It +then refuses (`receipt-json`) a member it does not define or one missing, `null` where +the member has a value, a value of another form (a number that is not an integer of its +range, `-0` among them; a string that is not Unicode scalar values), evidence other than +exactly what the class carries, a class label energy-measurement §2.7 refuses, a parent +kind and hash that disagree, an `index` other than `index-.wsix`, and a +`receipt_hash` that is not the receipt's. A reader does not require the canonical form; +a writer writes it, and a package's `package.receipt.json` must be it (§11.3). + +A verifier of a receipt against its indexes, in its components' order, refuses, the +first that applies: no component, or another number of indexes than components, a live +window's receipt included (`receipt-index-count`); an index that breaks §6 (its +reason); one that does not hash to its `index_digest` (`receipt-index-digest`), has +another stage root (`receipt-stage-root`), names another component or media class +(`receipt-component`) or another policy digest (`receipt-policy-digest`); two components +of one id (`receipt-component`); other counts, or a content hash at two lengths in any of the +indexes (`receipt-counts`); a parent kind that does not +match the indexes' `prev_index_digest`, which is zero in every index for none and a +re-pack and non-zero in every one for a live window (`receipt-parent-kind`); an energy +figure and class that do not cohere, or a legacy label (`receipt-energy`); a signature +that does not verify (`receipt-signature`). + +**9.2** Live windows chain by the parent link with parent kind `live-window`. A re-pack +of an earlier package **MAY** name it with parent kind `re-pack`. + +**9.3** A version-1 receipt (`wai.video.package`'s segment map) keeps its bytes and +still verifies. Its JSON form is RFC 8785 canonical with `version` 1: `asset_id`, +`energy_provenance` (`null` when unmetered) with its evidence, `grant`, `joules_micro`, +`kind` `"package"`, `parent_receipt_hash`, `receipt_hash`, `segment_map_root`, +`segments` (each `{"byte_len", "content_hash", "duration_ms", "index"}`), `sig`, +`signer_id`, `signer_pubkey`, `stored_bytes`, `total_bytes`, `total_segments`, +`unique_segments`, `work_segment_ops`. A reader reads one without `version` as version 1, +and, because version-1 receipts were issued before this form, keeps them verifying: +`energy_provenance` may be absent (read as unmetered), an evidence member written `null` +is absent, and an on-chip counter's filtering, absent, reads as unknown. It refuses a +member named twice as version 2's reader does. +Its signing payload, big-endian, is + + "wai:video-package-receipt\x01" + u32 len ‖ asset_id ‖ segment_map_root[32] + u64 total_segments ‖ u64 unique_segments ‖ u64 total_bytes ‖ u64 stored_bytes ‖ u64 work_segment_ops + u64 joules_micro ‖ class (u8 0 when unmetered, else the class's signed encoding) + the grant as §9.1 writes it + u8 0 | u8 1 ‖ parent_receipt_hash[32] + u64 n ‖ n × BLAKE3("wai:video-package-seg\x01" ‖ u64 index ‖ content_hash[32] ‖ u64 duration_ms ‖ u64 byte_len) + signer_pubkey[32] ‖ u32 len ‖ signer_id + +`segment_map_root` is the jwp-receipts §2.2 Merkle root over those segment leaves, the +counts are the segments' (the bytes of one copy per content hash for `stored_bytes`), and +`receipt_hash` is version 2's. +Both versions share the identity domain, so the figure profile +`wai:video-package-receipt-id` ([energy-measurement](energy-measurement.md) §6.1) +covers both; a version-2 receipt's figure is read only with the indexes it verifies +against. + +**9.4** Where the energy figure is not the unmetered form, the packager **MUST** publish +a `measurement` claim about it ([jwp-receipts](jwp-receipts.md) §2.4.8, +[energy-measurement](energy-measurement.md) §6) as +`claims/.measurement.json`: subject the receipt, figure profile +`wai:video-package-receipt-id`, signed with the package key and checked against the +receipt before it is written. A verifier refuses a metered package without it +(`measurement-claim-missing`) or with one that does not verify and check against the +receipt (`measurement-claim-invalid`). + +## 10. Royalty-free packaging + +A packager run with `royalty_free_only = true` **MUST** refuse to emit any object whose +capability the registry does not classify as royalty-free, including one it does not +classify at all (§3.6, rule 5). A package receipt says nothing about patents. +`royalty_free_only` records the registry class the packager read, "licensed +royalty-free by its standards body"; it is not a freedom-to-operate opinion. + +## 11. Package, re-pack equivalence and verification + +**11.1** *The package* is a directory of these files and no other file or directory +(every directory below it holds one of them): + + policy.toml policy.json plan-.txt index-.wsix package.receipt.json + objects/.wai … + floor//init.mp4 floor//.m4s … + hls/classic/… hls//… dash/… moq/catalog.json stages/.json + claims/.measurement.json (a metered package) + LISTING + +`policy.toml` is the policy as given; `policy.json` its effective canonical JSON +(§3.2). `LISTING` is the listing (SPEC §3.1) of every other file, and its SHA-256 is the +**pack digest**. A component or class id that cannot be one path segment of a listing, +two class ids equal once `A`–`Z` are mapped to `a`–`z`, and a class named `classic` in +any case (ids name paths, and paths fold case) are refused (`pack-id`). + +**11.2** *Re-pack equivalence.* Given the same source bytes, effective policy, supplied +floor, signing key and packager identity, a conforming reference packager **MUST** +produce byte-identical package files on every machine, except the energy figure and +measurement claim of a metered package and floor objects §7.5 covers. A verifier +**MAY** pack again and compare listings. A packager whose identity differs is not held +to this. + +**11.3** *Verification.* A verifier of a package, without its sources, applies these +checks in this order and refuses at the first that fails; a check that reads files a +failed check names is not applied: + +1. Every file below the package's directory, read strictly — a name beginning with `.` + and a `*.sha256` file included, as a listing (SPEC §3.1) would not skip them — is a + regular file whose path a listing can carry; a symbolic link, a directory that + cannot be read or holds nothing, or anything else is refused (`unexpected-file`). +2. `LISTING` is present (`listing-missing`) and is the listing of every other file + (`listing-mismatch`). +3. `package.receipt.json` reads as a version-2 receipt (§9.1: `receipt-json`, + `receipt-version`) and is that receipt's canonical form, byte for byte + (`receipt-json`). +4. `policy.json` hashes to the receipt's policy digest (`policy-digest-mismatch`); + `policy.toml` is a policy (its own reason, §3.2–§3.4); `policy.json` is its + effective canonical JSON, `royalty_free_only` forced on where `policy.json` says + `true` (`policy-json-mismatch`; an override never turns it off, §3.2, so a + `policy.json` that says `false` over a `policy.toml` that says `true` is refused + here); its ids can name files (`pack-id`, §11.1); and under that effective policy it + asks for nothing a packager may not emit (§3.6's reason; under `royalty_free_only`, a + patent-pool floor is `license-refused`). +5. The receipt's components are the policy's, by id, in order, each of the policy's + media (`policy-component-mismatch`), so a package is never a receipt over fewer + components than its policy has; each index is present (`index-missing`); the receipt + verifies against them (§9.1, the index rules of §6 among its reasons). +6. Per component, in order: + - the index against the policy (`index-policy-mismatch`): its timebase and media + class are the component's; its string table is the component's id, the class ids + in policy order, then each capability at its first use; its class records are the + policy's classes, in order, with their `max_layer`, floor-only flag and `max_kbps`; + every entry's role, capability and layer are a rung's of its component (a base, a + refinement at the rung's layer, or a floor); every piece's kind is one a video piece + has, and its rules, moments and class plan are §8.1's from its kind, span and + entries; + - the plan file holds the index's starts and its digest is the header's + (`plan-mismatch`); + - every entry's object is present (`object-missing`) and keeps the object rules of + §6.3, its counted bytes included; + - every decode-equivalent prefix decodes to its `prefix_digest` (`prefix-digest`); + - when all of that holds, the floor: per piece, one floor object of the floor rung's + capability whose renderer, settings, framing header, unit count and target are + §7.2's (`floor-object-mismatch`) and which presents the piece's base + (`floor-motif-mismatch`); its units each one temporal unit holding a frame, the + first of the first piece carrying a sequence header, every sequence header the + first's bytes, every shown key frame carrying one (`floor-av1-malformed`); each + piece opening on a shown key frame, and no other unit being one unless the policy + allows extra key frames (`floor-keyframe-mismatch`); and the CMAF files are the + ones its temporal units give (§7.6, `floor-cmaf-mismatch`); + - then the catalogs are the ones the index, the objects and the floor give, and an + index no catalog can be written for (a start past `2^53 − 1`) is refused + (`catalog-mismatch`). +7. A metered package's measurement claim (§9.4: `measurement-claim-missing`, + `measurement-claim-invalid`); an object file no entry names (`object-unlisted`); a + catalog file past the ones given (`catalog-unexpected`); any other file + (`unexpected-file`). + +**11.4** *What this revision packs.* The reference packager packs one video component +with a `wai.video.int_motion` base (§5.5, §5.6) and a supplied AV1 floor (§7.3). It +refuses by name, before writing anything, what it does not pack yet: another medium +(`pack-media-unsupported`), a refinement rung (`pack-refinement-unsupported`), a rule's +ladder subset (`pack-rule-ladder-unsupported`), a floor to encode +(`pack-floor-encode-unsupported`), and a rung of a capability it does not encode +(`pack-capability-unsupported:`, every one naming the capability of the rung it +does not pack); and a component with no source (`pack-source-missing`) or whose source's +frame rate is not its timebase (`pack-source-rate`). Where its own work fails it +refuses by name too, and writes nothing: an encode the encoder refuses, such as a piece +past the samples it holds, or an object it wrote that does not count (`pack-encode`); a +piece past `2^32 − 1` units (`pack-piece-length`); catalogs it cannot write, the case +§11.3 refuses as `catalog-mismatch` (`pack-catalog`); a meter that was asked for and +gives no figure, a span too short to reach a microjoule or a figure past `2^64 − 1` +(`pack-meter`, never an unmetered package in its place); a measurement claim it cannot +write (`pack-measurement-claim`); and a listing it cannot write (`pack-listing`). It +writes a package only into a directory that is new or holds nothing, and refuses any +other before it packs (`pack-out-not-empty`): written among another package's files, it +would leave behind the ones it does not write. ## 12. Reasons @@ -347,3 +979,13 @@ re-pack equivalence; they are specified with the packager stages that write them | `policy-too-many-components`, `policy-too-many-rungs`, `policy-too-many-classes`, `policy-too-many-rules`, `policy-too-many-moments`, `policy-duplicate-id`, `policy-lead`, `policy-open-gop`, `policy-piece-bounds`, `policy-ladder-component`, `policy-refines-role`, `policy-dangling-refines`, `policy-refines-cycle`, `policy-refines-base`, `policy-refines-branch`, `policy-no-base`, `policy-class-layer`, `policy-rule-ladder`, `policy-layer-lag`, `policy-too-many-deadlines`, `policy-deadline-unreachable` | §3.4 | | `capability-unknown`, `capability-not-payload`, `capability-candidate`, `capability-role`, `license-unclassified`, `license-refused`, each naming the capability (and the licence label) | §3.6 | | `y4m-header`, `y4m-interlaced`, `y4m-colorspace`, `y4m-size-change`, `y4m-truncated`, `wav-format`, `wav-truncated` | §4.7 | +| `index-magic`, `index-version`, `index-truncated`, `index-reserved`, `index-media-class`, `index-role`, `index-tier`, `index-string`, `index-trailing` | §6.2 | +| `index-timebase`, `window-start`, `piece-order`, `piece-entries`, `piece-kind`, `entry-flags`, `entry-order`, `layer-role`, `refines-outside-window`, `refines-mismatch`, `layer-gap`, `tier-entropy-passthrough`, `tier-weakest`, `tier-mismatch`, `digest-without-tier`, `deadline-role`, `floor-derived-from`, `dup-length`, `dup-flag`, `class-plan`, `class-flags` | §6.3, §6.4 | +| `object-length`, `object-hash`, `non-envelope-entry`, `object-lengths`, `object-capability`, `object-refines`, `object-derived-from` | §6.3 | +| `floor-missing`, `floor-listing-mismatch`, `floor-stream-missing`, `floor-av1-malformed`, `floor-frame-count`, `floor-keyframe-mismatch` | §7.1, §7.3 | +| `floor-object-mismatch`, `floor-motif-mismatch`, `floor-av1-malformed`, `floor-keyframe-mismatch`, `floor-cmaf-mismatch`, against a package | §11.3 | +| `receipt-json`, `receipt-version`, `receipt-index-count`, `receipt-index-digest`, `receipt-stage-root`, `receipt-component`, `receipt-policy-digest`, `receipt-counts`, `receipt-parent-kind`, `receipt-energy`, `receipt-signature` | §9.1 | +| `measurement-claim-missing`, `measurement-claim-invalid` | §9.4 | +| `unexpected-file`, `listing-missing`, `listing-mismatch`, `policy-digest-mismatch`, `policy-json-mismatch`, `policy-component-mismatch`, `index-missing`, `index-policy-mismatch`, `plan-mismatch`, `object-missing`, `prefix-digest`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted` | §11.3 | +| `pack-id`, `pack-source-missing`, `pack-source-rate`, `pack-media-unsupported`, `pack-refinement-unsupported`, `pack-rule-ladder-unsupported`, `pack-floor-encode-unsupported`, `pack-capability-unsupported` (naming the capability) | §11.1, §11.4 | +| `pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, `pack-measurement-claim`, `pack-listing`, `pack-out-not-empty` | §11.4 | diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 663b82419..9a656fe94 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -6,7 +6,8 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi - `policy/` — policies a packager accepts (every rung one it may emit, §3.6), and `expected.json`: each policy's effective canonical JSON and - digest (`example.toml` also with `royalty_free_only` overridden to false), and the + digest (`eq-defaults-a.toml` also with `royalty_free_only` forced on, the one way an + override changes it, §3.2), and the pairs `eq--a.toml` and `eq--b.toml`, two spellings of one policy that must have one digest: every default omitted or written out; standard or inline tables, dotted keys, member order, string escapes, integer notation and comments. @@ -36,6 +37,33 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi every fixture (its first 12 frames), under two bounds: each candidate's SSE and SSE per sample in thousandths, the step chosen, and the chosen payload's BLAKE3. +- `packs/video-base-01/` — a golden package (§5–§11). Its inputs: `policy.toml`, the + supplied floor `floor/v.obu` (an AV1 stream with a key frame at each piece start of + the plan, each piece coded alone, written by an AV1 encoder from the fixture; the + policy pins its listing), and `key.hex` (a test signing seed, never a production + key). `package/` is every file the packer writes from them and the fixture + `video-base-01`: objects, the stage index, the floor's objects and CMAF files, the + catalogs, the receipt and `LISTING`. The fixture's bumper plays twice, so its pieces + are one object each time. +- `mutations/` — stage indexes, objects and version-2 receipts, and `expected.json`: + each case's reason (§6.2–§6.4, §9.1), a well-formed case of each kind among them. +- `receipts/` — version-1 package receipts as JSON (§9.3), with each one's receipt + hash: the legacy receipt whose hash `wai-rs` pins, and an unmetered one. +- `catalogs/` — stage indexes no golden package has (refinements on more than one + deadline), with the MoQ catalog each gives (§8.4). +- `package-mutations/` — packages a verifier must refuse (§11.3), each the golden + altered and, where the receipt or `LISTING` alone would catch the alteration, + re-sealed with the golden's key: what a dishonest packager holding the key could + publish. Each is an overlay on the golden (`/`, the files that differ or are + new; `expected.json`, the files it removes, the directories it adds that hold + nothing or cannot be read, and its reason), written out as a directory and read + strictly by each verifier: a receipt over no component, an index that is not its + policy's, the policy's own refusals (a `policy.json` that turns `royalty_free_only` + off among them), a missing or corrupted object, a live window no catalog can be + written for, floor objects that do not open on a key frame or are not §7.2's, files + and directories a strict read finds, and a receipt file that is not its canonical + form; and one the verifier must accept (an extra key frame the policy allows). + The fixtures are not committed. `wai-pack fixture gen ` writes each from its definition (`wai-pack/src/fixture.rs`), every byte an integer function of the frame index and the pixel position, and the corpus pins its SHA-256: @@ -44,6 +72,8 @@ index and the pixel position, and the corpus pins its SHA-256: a fade, which is not cut; and two shots cut against each other, each return a cut. - `kinds-01` — one shot of each kind, then a static shot longer than the longest piece, cut where `max` forces it. +- `video-base-01` — 64 × 36: three shots with a one-second bumper between them, the + bumper the same frames both times (its span reads time from its own start). Files are compared as bytes; `.gitattributes` turns off line-ending conversion. @@ -57,10 +87,13 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. - `wai-pack conformance check ../pack-conformance` recomputes every expected file and every generated input and requires them to equal the committed ones, requires the digests of each equal pair to be equal, decodes every encode payload through - `decode_int` to the encoder's reconstruction and recomputes its SSE. `cargo test` + `decode_int` to the encoder's reconstruction and recomputes its SSE. It packs each + golden again and compares every file, verifies the committed package + (`wai-pack verify`), and gives every mutation and every package mutation its + reason. `cargo test` runs the same check, with the scene cuts of each fixture stated as tests (`tests/scenecut.rs`). -- `pack_verify.py`, Python standard library only (TOML through `tomllib`, so 3.11 or +- `pack_verify.py`, Python standard library and, for Ed25519, `cryptography` (TOML through `tomllib`, so 3.11 or later) and sharing no code with wai-pack, written from the extension: its own policy schema, constraints, canonical JSON and digest; its own Y4M and WAV readers; the fixtures regenerated from their definition; `wai.pack.scenecut/1` in its on-demand @@ -69,7 +102,17 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. (`../registry/capabilities.json`); and every encode payload decoded to its frames' SHA-256, with its SSE recomputed from the regenerated source. BLAKE3 comes from `../claim-conformance/_blake3.py`, and the `WIV1` parser and decoder from - `../int-payload-conformance/int_payload_verify.py`. + `../int-payload-conformance/int_payload_verify.py`. For the packages it has its own + stage index reader and rules, object rules (each payload parsed as its pinned format, + a `WIR1` reader included), version-1 and version-2 receipts (Ed25519 through the + `cryptography` package, imported only where a signature is checked), an AV1 stream + and sequence header reader, the CMAF files, and every catalog: it verifies each golden + package in §11.3's order, rebuilding the floor's files and re-rendering the catalogs + byte for byte, gives every mutation its reason, checks the version-1 receipts, and + verifies every package mutation in the same order, refused with its reason. + From the regenerated fixture it re-derives what only the source decides: the pieces, + their rules, moments and class plan, each base's SSE and step, and the encoder's + choices; and it checks the supplied floor against the policy's listing and the plan. python3 pack-conformance/pack_verify.py pack-conformance diff --git a/wai/pack-conformance/catalogs/both-layers-on-800.wsix b/wai/pack-conformance/catalogs/both-layers-on-800.wsix new file mode 100644 index 000000000..f07c7d470 Binary files /dev/null and b/wai/pack-conformance/catalogs/both-layers-on-800.wsix differ diff --git a/wai/pack-conformance/catalogs/expected.json b/wai/pack-conformance/catalogs/expected.json new file mode 100644 index 000000000..9be559248 --- /dev/null +++ b/wai/pack-conformance/catalogs/expected.json @@ -0,0 +1,49 @@ +{ + "cases": [ + { + "index": "catalogs/one-deadline.wsix", + "layer_lag_ms": [ + 0, + 200, + 400 + ], + "default_deadline": 1500, + "floor": { + "codec": "av01.0.00M.08", + "w": 64, + "h": 36 + }, + "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1300,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1\"],\"name\":\"v/l2\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + }, + { + "index": "catalogs/layer-1-on-its-own-deadline.wsix", + "layer_lag_ms": [ + 0, + 200, + 400 + ], + "default_deadline": 1500, + "floor": { + "codec": "av01.0.00M.08", + "w": 64, + "h": 36 + }, + "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1@800\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1@800\"],\"name\":\"v/l2\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":1100,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + }, + { + "index": "catalogs/both-layers-on-800.wsix", + "layer_lag_ms": [ + 0, + 200, + 400 + ], + "default_deadline": 1500, + "floor": { + "codec": "av01.0.00M.08", + "w": 64, + "h": 36 + }, + "moq": "{\"tracks\":[{\"altGroup\":1,\"name\":\"v/l0\",\"packaging\":\"wai\",\"priority\":64,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_motion\"],\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":0,\"wai_receipts\":false},{\"depends\":[\"v/l0\"],\"name\":\"v/l1@800\",\"packaging\":\"wai\",\"priority\":96,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":600,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":1,\"wai_receipts\":false},{\"depends\":[\"v/l1@800\"],\"name\":\"v/l2@800\",\"packaging\":\"wai\",\"priority\":128,\"renderGroup\":1,\"wai_capabilities\":[\"wai.video.int_refine\"],\"wai_delivery_timeout_ms\":400,\"wai_determinism\":\"decode-equivalence\",\"wai_layer\":2,\"wai_receipts\":false},{\"altGroup\":1,\"codec\":\"av01.0.00M.08\",\"height\":36,\"name\":\"v/floor\",\"packaging\":\"cmaf\",\"renderGroup\":1,\"width\":64}],\"version\":1}\n" + } + ] +} diff --git a/wai/pack-conformance/catalogs/layer-1-on-its-own-deadline.wsix b/wai/pack-conformance/catalogs/layer-1-on-its-own-deadline.wsix new file mode 100644 index 000000000..b939b8a23 Binary files /dev/null and b/wai/pack-conformance/catalogs/layer-1-on-its-own-deadline.wsix differ diff --git a/wai/pack-conformance/catalogs/one-deadline.wsix b/wai/pack-conformance/catalogs/one-deadline.wsix new file mode 100644 index 000000000..f21dc3110 Binary files /dev/null and b/wai/pack-conformance/catalogs/one-deadline.wsix differ diff --git a/wai/pack-conformance/mutations/expected.json b/wai/pack-conformance/mutations/expected.json new file mode 100644 index 000000000..aa002b3e1 --- /dev/null +++ b/wai/pack-conformance/mutations/expected.json @@ -0,0 +1,505 @@ +{ + "cases": [ + { + "name": "ok", + "kind": "index", + "index": "index/ok.wsix", + "reason": "ok" + }, + { + "name": "refines-flipped", + "kind": "index", + "index": "index/refines-flipped.wsix", + "reason": "refines-mismatch" + }, + { + "name": "layer-gap", + "kind": "index", + "index": "index/layer-gap.wsix", + "reason": "layer-gap" + }, + { + "name": "refines-outside-window", + "kind": "index", + "index": "index/refines-outside-window.wsix", + "reason": "refines-outside-window" + }, + { + "name": "tier-entropy-over-entropy", + "kind": "index", + "index": "index/tier-entropy-over-entropy.wsix", + "reason": "tier-entropy-passthrough" + }, + { + "name": "tier-decode-over-entropy", + "kind": "index", + "index": "index/tier-decode-over-entropy.wsix", + "reason": "tier-weakest" + }, + { + "name": "tier-on-floor", + "kind": "index", + "index": "index/tier-on-floor.wsix", + "reason": "tier-mismatch" + }, + { + "name": "digest-without-tier", + "kind": "index", + "index": "index/digest-without-tier.wsix", + "reason": "digest-without-tier" + }, + { + "name": "dup-flag-missing", + "kind": "index", + "index": "index/dup-flag-missing.wsix", + "reason": "dup-flag" + }, + { + "name": "dup-flag-lie", + "kind": "index", + "index": "index/dup-flag-lie.wsix", + "reason": "dup-flag" + }, + { + "name": "dup-length", + "kind": "index", + "index": "index/dup-length.wsix", + "reason": "dup-length" + }, + { + "name": "floor-at-layer-1", + "kind": "index", + "index": "index/floor-at-layer-1.wsix", + "reason": "layer-role" + }, + { + "name": "base-with-refines", + "kind": "index", + "index": "index/base-with-refines.wsix", + "reason": "layer-role" + }, + { + "name": "floor-before-refinement", + "kind": "index", + "index": "index/floor-before-refinement.wsix", + "reason": "entry-order" + }, + { + "name": "deadline-on-base", + "kind": "index", + "index": "index/deadline-on-base.wsix", + "reason": "deadline-role" + }, + { + "name": "floor-without-derived-from", + "kind": "index", + "index": "index/floor-without-derived-from.wsix", + "reason": "floor-derived-from" + }, + { + "name": "entry-flag-unknown", + "kind": "index", + "index": "index/entry-flag-unknown.wsix", + "reason": "entry-flags" + }, + { + "name": "piece-gap", + "kind": "index", + "index": "index/piece-gap.wsix", + "reason": "piece-order" + }, + { + "name": "piece-empty", + "kind": "index", + "index": "index/piece-empty.wsix", + "reason": "piece-order" + }, + { + "name": "piece-entries-overlap", + "kind": "index", + "index": "index/piece-entries-overlap.wsix", + "reason": "piece-entries" + }, + { + "name": "entry-in-another-piece", + "kind": "index", + "index": "index/entry-in-another-piece.wsix", + "reason": "piece-entries" + }, + { + "name": "piece-kind-unknown", + "kind": "index", + "index": "index/piece-kind-unknown.wsix", + "reason": "piece-kind" + }, + { + "name": "plan-past-piece", + "kind": "index", + "index": "index/plan-past-piece.wsix", + "reason": "class-plan" + }, + { + "name": "plan-past-class", + "kind": "index", + "index": "index/plan-past-class.wsix", + "reason": "class-plan" + }, + { + "name": "plan-floor-only-class", + "kind": "index", + "index": "index/plan-floor-only-class.wsix", + "reason": "class-plan" + }, + { + "name": "class-flag-unknown", + "kind": "index", + "index": "index/class-flag-unknown.wsix", + "reason": "class-flags" + }, + { + "name": "timebase-zero", + "kind": "index", + "index": "index/timebase-zero.wsix", + "reason": "index-timebase" + }, + { + "name": "on-demand-first-piece", + "kind": "index", + "index": "index/on-demand-first-piece.wsix", + "reason": "window-start" + }, + { + "name": "on-demand-first-entry", + "kind": "index", + "index": "index/on-demand-first-entry.wsix", + "reason": "window-start" + }, + { + "name": "on-demand-start", + "kind": "index", + "index": "index/on-demand-start.wsix", + "reason": "window-start" + }, + { + "name": "truncated-table", + "kind": "index", + "index": "index/truncated-table.wsix", + "reason": "index-truncated" + }, + { + "name": "trailing", + "kind": "index", + "index": "index/trailing.wsix", + "reason": "index-trailing" + }, + { + "name": "magic", + "kind": "index", + "index": "index/magic.wsix", + "reason": "index-magic" + }, + { + "name": "version", + "kind": "index", + "index": "index/version.wsix", + "reason": "index-version" + }, + { + "name": "media-class", + "kind": "index", + "index": "index/media-class.wsix", + "reason": "index-media-class" + }, + { + "name": "reserved", + "kind": "index", + "index": "index/reserved.wsix", + "reason": "index-reserved" + }, + { + "name": "role", + "kind": "index", + "index": "index/role.wsix", + "reason": "index-role" + }, + { + "name": "tier", + "kind": "index", + "index": "index/tier.wsix", + "reason": "index-tier" + }, + { + "name": "string-index", + "kind": "index", + "index": "index/string-index.wsix", + "reason": "index-string" + }, + { + "name": "objects-ok", + "kind": "objects", + "index": "objects-index/ok.wsix", + "objects": "objects/", + "reason": "ok" + }, + { + "name": "objects-cmaf-listed", + "kind": "objects", + "index": "objects-index/cmaf-listed.wsix", + "objects": "objects/", + "reason": "non-envelope-entry" + }, + { + "name": "objects-missing", + "kind": "objects", + "index": "objects-index/missing.wsix", + "objects": "objects/", + "reason": "object-missing" + }, + { + "name": "objects-hash", + "kind": "objects", + "index": "objects-index/hash.wsix", + "objects": "objects/", + "reason": "object-hash" + }, + { + "name": "objects-not-its-format", + "kind": "objects", + "index": "objects-index/not-its-format.wsix", + "objects": "objects/", + "reason": "object-lengths" + }, + { + "name": "objects-length", + "kind": "objects", + "index": "objects-index/length.wsix", + "objects": "objects/", + "reason": "object-length" + }, + { + "name": "objects-lengths", + "kind": "objects", + "index": "objects-index/lengths.wsix", + "objects": "objects/", + "reason": "object-lengths" + }, + { + "name": "objects-capability", + "kind": "objects", + "index": "objects-index/capability.wsix", + "objects": "objects/", + "reason": "object-capability" + }, + { + "name": "objects-derived-from", + "kind": "objects", + "index": "objects-index/derived-from.wsix", + "objects": "objects/", + "reason": "object-derived-from" + }, + { + "name": "objects-refines-names-another", + "kind": "objects", + "index": "objects-index/refines-names-another.wsix", + "objects": "objects/", + "reason": "object-refines" + }, + { + "name": "receipt-ok", + "kind": "receipt", + "receipt": "receipt/ok.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "ok" + }, + { + "name": "receipt-ok-labelled", + "kind": "receipt", + "receipt": "receipt/ok-labelled.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "ok" + }, + { + "name": "receipt-ok-live-window", + "kind": "receipt", + "receipt": "receipt/ok-live-window.json", + "indexes": [ + "receipt/live/index-v.wsix" + ], + "reason": "ok" + }, + { + "name": "receipt-counts", + "kind": "receipt", + "receipt": "receipt/counts.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-counts" + }, + { + "name": "receipt-parent-kind", + "kind": "receipt", + "receipt": "receipt/parent-kind.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-parent-kind" + }, + { + "name": "receipt-parent-kind-live", + "kind": "receipt", + "receipt": "receipt/parent-kind-live.json", + "indexes": [ + "receipt/live/index-v.wsix" + ], + "reason": "receipt-parent-kind" + }, + { + "name": "receipt-stage-root", + "kind": "receipt", + "receipt": "receipt/stage-root.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-stage-root" + }, + { + "name": "receipt-index-digest", + "kind": "receipt", + "receipt": "receipt/index-digest.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-index-digest" + }, + { + "name": "receipt-media-class", + "kind": "receipt", + "receipt": "receipt/media-class.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-component" + }, + { + "name": "receipt-policy-digest", + "kind": "receipt", + "receipt": "receipt/policy-digest.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-policy-digest" + }, + { + "name": "receipt-energy", + "kind": "receipt", + "receipt": "receipt/energy.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-energy" + }, + { + "name": "receipt-signature", + "kind": "receipt", + "receipt": "receipt/signature.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-signature" + }, + { + "name": "receipt-unknown-member", + "kind": "receipt", + "receipt": "receipt/unknown-member.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-duplicate-member", + "kind": "receipt", + "receipt": "receipt/duplicate-member.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-minus-zero", + "kind": "receipt", + "receipt": "receipt/minus-zero.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-lone-surrogate", + "kind": "receipt", + "receipt": "receipt/lone-surrogate.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-null-value", + "kind": "receipt", + "receipt": "receipt/null-value.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-energy-member-missing", + "kind": "receipt", + "receipt": "receipt/energy-member-missing.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-evidence-without-class", + "kind": "receipt", + "receipt": "receipt/evidence-without-class.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-no-component", + "kind": "receipt", + "receipt": "receipt/no-component.json", + "indexes": [], + "reason": "receipt-index-count" + }, + { + "name": "receipt-receipt-hash", + "kind": "receipt", + "receipt": "receipt/receipt-hash.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + }, + { + "name": "receipt-two-components-one-id", + "kind": "receipt", + "receipt": "receipt/two-components-one-id.json", + "indexes": [ + "receipt/index-v.wsix", + "receipt/index-v.wsix" + ], + "reason": "receipt-component" + } + ] +} diff --git a/wai/pack-conformance/mutations/index/base-with-refines.wsix b/wai/pack-conformance/mutations/index/base-with-refines.wsix new file mode 100644 index 000000000..f85a564ad Binary files /dev/null and b/wai/pack-conformance/mutations/index/base-with-refines.wsix differ diff --git a/wai/pack-conformance/mutations/index/class-flag-unknown.wsix b/wai/pack-conformance/mutations/index/class-flag-unknown.wsix new file mode 100644 index 000000000..e5a72f2b0 Binary files /dev/null and b/wai/pack-conformance/mutations/index/class-flag-unknown.wsix differ diff --git a/wai/pack-conformance/mutations/index/deadline-on-base.wsix b/wai/pack-conformance/mutations/index/deadline-on-base.wsix new file mode 100644 index 000000000..5f65c34d8 Binary files /dev/null and b/wai/pack-conformance/mutations/index/deadline-on-base.wsix differ diff --git a/wai/pack-conformance/mutations/index/digest-without-tier.wsix b/wai/pack-conformance/mutations/index/digest-without-tier.wsix new file mode 100644 index 000000000..621a1fd13 Binary files /dev/null and b/wai/pack-conformance/mutations/index/digest-without-tier.wsix differ diff --git a/wai/pack-conformance/mutations/index/dup-flag-lie.wsix b/wai/pack-conformance/mutations/index/dup-flag-lie.wsix new file mode 100644 index 000000000..6daedb3a2 Binary files /dev/null and b/wai/pack-conformance/mutations/index/dup-flag-lie.wsix differ diff --git a/wai/pack-conformance/mutations/index/dup-flag-missing.wsix b/wai/pack-conformance/mutations/index/dup-flag-missing.wsix new file mode 100644 index 000000000..0bde71dab Binary files /dev/null and b/wai/pack-conformance/mutations/index/dup-flag-missing.wsix differ diff --git a/wai/pack-conformance/mutations/index/dup-length.wsix b/wai/pack-conformance/mutations/index/dup-length.wsix new file mode 100644 index 000000000..90ba8d8de Binary files /dev/null and b/wai/pack-conformance/mutations/index/dup-length.wsix differ diff --git a/wai/pack-conformance/mutations/index/entry-flag-unknown.wsix b/wai/pack-conformance/mutations/index/entry-flag-unknown.wsix new file mode 100644 index 000000000..945e32858 Binary files /dev/null and b/wai/pack-conformance/mutations/index/entry-flag-unknown.wsix differ diff --git a/wai/pack-conformance/mutations/index/entry-in-another-piece.wsix b/wai/pack-conformance/mutations/index/entry-in-another-piece.wsix new file mode 100644 index 000000000..c1bfc4bcf Binary files /dev/null and b/wai/pack-conformance/mutations/index/entry-in-another-piece.wsix differ diff --git a/wai/pack-conformance/mutations/index/floor-at-layer-1.wsix b/wai/pack-conformance/mutations/index/floor-at-layer-1.wsix new file mode 100644 index 000000000..459b26663 Binary files /dev/null and b/wai/pack-conformance/mutations/index/floor-at-layer-1.wsix differ diff --git a/wai/pack-conformance/mutations/index/floor-before-refinement.wsix b/wai/pack-conformance/mutations/index/floor-before-refinement.wsix new file mode 100644 index 000000000..bfa6daf45 Binary files /dev/null and b/wai/pack-conformance/mutations/index/floor-before-refinement.wsix differ diff --git a/wai/pack-conformance/mutations/index/floor-without-derived-from.wsix b/wai/pack-conformance/mutations/index/floor-without-derived-from.wsix new file mode 100644 index 000000000..779f763d4 Binary files /dev/null and b/wai/pack-conformance/mutations/index/floor-without-derived-from.wsix differ diff --git a/wai/pack-conformance/mutations/index/layer-gap.wsix b/wai/pack-conformance/mutations/index/layer-gap.wsix new file mode 100644 index 000000000..96f31c71e Binary files /dev/null and b/wai/pack-conformance/mutations/index/layer-gap.wsix differ diff --git a/wai/pack-conformance/mutations/index/magic.wsix b/wai/pack-conformance/mutations/index/magic.wsix new file mode 100644 index 000000000..7606029e1 Binary files /dev/null and b/wai/pack-conformance/mutations/index/magic.wsix differ diff --git a/wai/pack-conformance/mutations/index/media-class.wsix b/wai/pack-conformance/mutations/index/media-class.wsix new file mode 100644 index 000000000..b9205986d Binary files /dev/null and b/wai/pack-conformance/mutations/index/media-class.wsix differ diff --git a/wai/pack-conformance/mutations/index/ok.wsix b/wai/pack-conformance/mutations/index/ok.wsix new file mode 100644 index 000000000..f21dc3110 Binary files /dev/null and b/wai/pack-conformance/mutations/index/ok.wsix differ diff --git a/wai/pack-conformance/mutations/index/on-demand-first-entry.wsix b/wai/pack-conformance/mutations/index/on-demand-first-entry.wsix new file mode 100644 index 000000000..64f6c433c Binary files /dev/null and b/wai/pack-conformance/mutations/index/on-demand-first-entry.wsix differ diff --git a/wai/pack-conformance/mutations/index/on-demand-first-piece.wsix b/wai/pack-conformance/mutations/index/on-demand-first-piece.wsix new file mode 100644 index 000000000..065ad525f Binary files /dev/null and b/wai/pack-conformance/mutations/index/on-demand-first-piece.wsix differ diff --git a/wai/pack-conformance/mutations/index/on-demand-start.wsix b/wai/pack-conformance/mutations/index/on-demand-start.wsix new file mode 100644 index 000000000..2711a1608 Binary files /dev/null and b/wai/pack-conformance/mutations/index/on-demand-start.wsix differ diff --git a/wai/pack-conformance/mutations/index/piece-empty.wsix b/wai/pack-conformance/mutations/index/piece-empty.wsix new file mode 100644 index 000000000..5464998dd Binary files /dev/null and b/wai/pack-conformance/mutations/index/piece-empty.wsix differ diff --git a/wai/pack-conformance/mutations/index/piece-entries-overlap.wsix b/wai/pack-conformance/mutations/index/piece-entries-overlap.wsix new file mode 100644 index 000000000..35469a8fc Binary files /dev/null and b/wai/pack-conformance/mutations/index/piece-entries-overlap.wsix differ diff --git a/wai/pack-conformance/mutations/index/piece-gap.wsix b/wai/pack-conformance/mutations/index/piece-gap.wsix new file mode 100644 index 000000000..90ec58cc9 Binary files /dev/null and b/wai/pack-conformance/mutations/index/piece-gap.wsix differ diff --git a/wai/pack-conformance/mutations/index/piece-kind-unknown.wsix b/wai/pack-conformance/mutations/index/piece-kind-unknown.wsix new file mode 100644 index 000000000..f6a168f92 Binary files /dev/null and b/wai/pack-conformance/mutations/index/piece-kind-unknown.wsix differ diff --git a/wai/pack-conformance/mutations/index/plan-floor-only-class.wsix b/wai/pack-conformance/mutations/index/plan-floor-only-class.wsix new file mode 100644 index 000000000..00b58e39e Binary files /dev/null and b/wai/pack-conformance/mutations/index/plan-floor-only-class.wsix differ diff --git a/wai/pack-conformance/mutations/index/plan-past-class.wsix b/wai/pack-conformance/mutations/index/plan-past-class.wsix new file mode 100644 index 000000000..ef6cc9145 Binary files /dev/null and b/wai/pack-conformance/mutations/index/plan-past-class.wsix differ diff --git a/wai/pack-conformance/mutations/index/plan-past-piece.wsix b/wai/pack-conformance/mutations/index/plan-past-piece.wsix new file mode 100644 index 000000000..0e2ad22cf Binary files /dev/null and b/wai/pack-conformance/mutations/index/plan-past-piece.wsix differ diff --git a/wai/pack-conformance/mutations/index/refines-flipped.wsix b/wai/pack-conformance/mutations/index/refines-flipped.wsix new file mode 100644 index 000000000..69762f70b Binary files /dev/null and b/wai/pack-conformance/mutations/index/refines-flipped.wsix differ diff --git a/wai/pack-conformance/mutations/index/refines-outside-window.wsix b/wai/pack-conformance/mutations/index/refines-outside-window.wsix new file mode 100644 index 000000000..ce2ffd340 Binary files /dev/null and b/wai/pack-conformance/mutations/index/refines-outside-window.wsix differ diff --git a/wai/pack-conformance/mutations/index/reserved.wsix b/wai/pack-conformance/mutations/index/reserved.wsix new file mode 100644 index 000000000..68d693f67 Binary files /dev/null and b/wai/pack-conformance/mutations/index/reserved.wsix differ diff --git a/wai/pack-conformance/mutations/index/role.wsix b/wai/pack-conformance/mutations/index/role.wsix new file mode 100644 index 000000000..7b4ac300b Binary files /dev/null and b/wai/pack-conformance/mutations/index/role.wsix differ diff --git a/wai/pack-conformance/mutations/index/string-index.wsix b/wai/pack-conformance/mutations/index/string-index.wsix new file mode 100644 index 000000000..7690e8f0b Binary files /dev/null and b/wai/pack-conformance/mutations/index/string-index.wsix differ diff --git a/wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix b/wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix new file mode 100644 index 000000000..f4ffac37d Binary files /dev/null and b/wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix differ diff --git a/wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix b/wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix new file mode 100644 index 000000000..903c505e3 Binary files /dev/null and b/wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix differ diff --git a/wai/pack-conformance/mutations/index/tier-on-floor.wsix b/wai/pack-conformance/mutations/index/tier-on-floor.wsix new file mode 100644 index 000000000..8f8daa35f Binary files /dev/null and b/wai/pack-conformance/mutations/index/tier-on-floor.wsix differ diff --git a/wai/pack-conformance/mutations/index/tier.wsix b/wai/pack-conformance/mutations/index/tier.wsix new file mode 100644 index 000000000..9c079722e Binary files /dev/null and b/wai/pack-conformance/mutations/index/tier.wsix differ diff --git a/wai/pack-conformance/mutations/index/timebase-zero.wsix b/wai/pack-conformance/mutations/index/timebase-zero.wsix new file mode 100644 index 000000000..66359660b Binary files /dev/null and b/wai/pack-conformance/mutations/index/timebase-zero.wsix differ diff --git a/wai/pack-conformance/mutations/index/trailing.wsix b/wai/pack-conformance/mutations/index/trailing.wsix new file mode 100644 index 000000000..ec2e92070 Binary files /dev/null and b/wai/pack-conformance/mutations/index/trailing.wsix differ diff --git a/wai/pack-conformance/mutations/index/truncated-table.wsix b/wai/pack-conformance/mutations/index/truncated-table.wsix new file mode 100644 index 000000000..ad3b63b58 Binary files /dev/null and b/wai/pack-conformance/mutations/index/truncated-table.wsix differ diff --git a/wai/pack-conformance/mutations/index/version.wsix b/wai/pack-conformance/mutations/index/version.wsix new file mode 100644 index 000000000..1bea7fc74 Binary files /dev/null and b/wai/pack-conformance/mutations/index/version.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/capability.wsix b/wai/pack-conformance/mutations/objects-index/capability.wsix new file mode 100644 index 000000000..403e12961 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/capability.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix b/wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix new file mode 100644 index 000000000..65ff62de7 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/derived-from.wsix b/wai/pack-conformance/mutations/objects-index/derived-from.wsix new file mode 100644 index 000000000..0d2080af3 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/derived-from.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/hash.wsix b/wai/pack-conformance/mutations/objects-index/hash.wsix new file mode 100644 index 000000000..74e39a6c1 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/hash.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/length.wsix b/wai/pack-conformance/mutations/objects-index/length.wsix new file mode 100644 index 000000000..334bdd406 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/length.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/lengths.wsix b/wai/pack-conformance/mutations/objects-index/lengths.wsix new file mode 100644 index 000000000..6029c3981 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/lengths.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/missing.wsix b/wai/pack-conformance/mutations/objects-index/missing.wsix new file mode 100644 index 000000000..6568bc3ca Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/missing.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/not-its-format.wsix b/wai/pack-conformance/mutations/objects-index/not-its-format.wsix new file mode 100644 index 000000000..1ecfbca9b Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/not-its-format.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/ok.wsix b/wai/pack-conformance/mutations/objects-index/ok.wsix new file mode 100644 index 000000000..f21dc3110 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/ok.wsix differ diff --git a/wai/pack-conformance/mutations/objects-index/refines-names-another.wsix b/wai/pack-conformance/mutations/objects-index/refines-names-another.wsix new file mode 100644 index 000000000..978635f47 Binary files /dev/null and b/wai/pack-conformance/mutations/objects-index/refines-names-another.wsix differ diff --git a/wai/pack-conformance/mutations/objects/03707889bfb7d683de788771279b7de5c6daf311bf5eea5121c0c6870829408d.wai b/wai/pack-conformance/mutations/objects/03707889bfb7d683de788771279b7de5c6daf311bf5eea5121c0c6870829408d.wai new file mode 100644 index 000000000..012b51796 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/03707889bfb7d683de788771279b7de5c6daf311bf5eea5121c0c6870829408d.wai differ diff --git a/wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai b/wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai new file mode 100644 index 000000000..928d19b4a Binary files /dev/null and b/wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai differ diff --git a/wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai b/wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai new file mode 100644 index 000000000..c67a8adc7 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai differ diff --git a/wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai b/wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai new file mode 100644 index 000000000..ca2a07ed6 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai differ diff --git a/wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai b/wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai new file mode 100644 index 000000000..3b31d181d Binary files /dev/null and b/wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai differ diff --git a/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai new file mode 100644 index 000000000..ded799e85 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai differ diff --git a/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai new file mode 100644 index 000000000..ded799e85 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai differ diff --git a/wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai b/wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai new file mode 100644 index 000000000..e2d941c30 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai differ diff --git a/wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai b/wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai new file mode 100644 index 000000000..0ed3a0c19 Binary files /dev/null and b/wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai differ diff --git a/wai/pack-conformance/mutations/receipt/counts.json b/wai/pack-conformance/mutations/receipt/counts.json new file mode 100644 index 000000000..ed86abca9 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/counts.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"bb5d938ee67a49d3b1a365d53c99b3035d3f5fef01140c01eac707d0377f88fc","sig":"900be144043bce37a5608f4b6844df6b1a4ba4dabe2d8d0ef564a4ca2d64c4cafe159446aed69c298741f723ff700547b56fe4a20708609ae99d89b0090a8106","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2302,"total_bytes":2746,"total_entries":6,"unique_objects":4,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/duplicate-member.json b/wai/pack-conformance/mutations/receipt/duplicate-member.json new file mode 100644 index 000000000..4fed30be1 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/duplicate-member.json @@ -0,0 +1 @@ +{"asset_id":"forged","asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/energy-member-missing.json b/wai/pack-conformance/mutations/receipt/energy-member-missing.json new file mode 100644 index 000000000..09613ffcc --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/energy-member-missing.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/energy.json b/wai/pack-conformance/mutations/receipt/energy.json new file mode 100644 index 000000000..71bb277fa --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/energy.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":12,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"b0f4d57cd186a32e5ab6e0186e875f97716e5d11e7e0f8bc3c2a1f6bca815885","sig":"ea3e286e2b489730120665d2bd6869bb356ddd85d413e50a2dc78e0644b8a2a78ad61580ac2cf0259d361fa603af6a4427b2564b5802677db74ca1896dbb6403","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/evidence-without-class.json b/wai/pack-conformance/mutations/receipt/evidence-without-class.json new file mode 100644 index 000000000..fcba70a92 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/evidence-without-class.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"energy_uncertainty":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/index-digest.json b/wai/pack-conformance/mutations/receipt/index-digest.json new file mode 100644 index 000000000..f34a76a59 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/index-digest.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"0000000000000000000000000000000000000000000000000000000000000000","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"a49d887c53b7520c90f25995a862a6700834e4996a4758d3dd9c5233e238495d","sig":"cba5ac5bcb072db6b72b9295e9bdd8cecc7ba62fe71e7668ba4efd982b5d76eadec6bea34c875da7f66b79dcdbf11a58c9885a49ebcf41d9c563f17c45aa0701","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/index-v.wsix b/wai/pack-conformance/mutations/receipt/index-v.wsix new file mode 100644 index 000000000..f21dc3110 Binary files /dev/null and b/wai/pack-conformance/mutations/receipt/index-v.wsix differ diff --git a/wai/pack-conformance/mutations/receipt/live/index-v.wsix b/wai/pack-conformance/mutations/receipt/live/index-v.wsix new file mode 100644 index 000000000..7d93f29ae Binary files /dev/null and b/wai/pack-conformance/mutations/receipt/live/index-v.wsix differ diff --git a/wai/pack-conformance/mutations/receipt/lone-surrogate.json b/wai/pack-conformance/mutations/receipt/lone-surrogate.json new file mode 100644 index 000000000..c9b8100ca --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/lone-surrogate.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"\ud800did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/media-class.json b/wai/pack-conformance/mutations/receipt/media-class.json new file mode 100644 index 000000000..c3a4b8853 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/media-class.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"audio","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"e88ed17039da041c5f34112d541ff4c5be9839dff10ace2d2b4c8d02dbb3080a","sig":"9bc82aa4a6428eae4a0460eb517cf178c110bf9fbefdf07af9994ccee17fa9688f3c9202c5a64c3d4394775dd7bccaf9598bf75c9944536c1c3d8eef95257405","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/minus-zero.json b/wai/pack-conformance/mutations/receipt/minus-zero.json new file mode 100644 index 000000000..cc1567e1b --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/minus-zero.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":-0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/no-component.json b/wai/pack-conformance/mutations/receipt/no-component.json new file mode 100644 index 000000000..cc3b0cb7f --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/no-component.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"e77fea3f0ba63518d3c517764335cabf024d6291b12acdbfc3130be6e3f9bef4","sig":"4d60d7be1a72000593ed93273499eb488e869bc179568b0dac29505bc999591f7450ab9bd519fdc42c5ce90a8097b6e9602a29671c6131a4126059b182844704","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":0,"total_bytes":0,"total_entries":0,"unique_objects":0,"version":2,"work_units":0} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/null-value.json b/wai/pack-conformance/mutations/receipt/null-value.json new file mode 100644 index 000000000..52156ca08 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/null-value.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":null,"parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/ok-labelled.json b/wai/pack-conformance/mutations/receipt/ok-labelled.json new file mode 100644 index 000000000..c13efbd35 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/ok-labelled.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":"ModelBased","grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":25000,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"re-pack","parent_receipt_hash":"0303030303030303030303030303030303030303030303030303030303030303","policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"25641733a1cd68a0b2ad3186733259b2dc297df04be31e7d37f8075f7392bb19","sig":"65ac5b18375489d2658f4ac55bac013c1ca135a510070b132de86cf31f3b635e6b7eecdb285106cd00ba4c8b92b5344a2e1b860e0831e8c21538eca397920c05","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/ok-live-window.json b/wai/pack-conformance/mutations/receipt/ok-live-window.json new file mode 100644 index 000000000..2570e57a8 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/ok-live-window.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"1a198b17cf576faadf0cfe4dcbe7be1fee7e451307145120c9075f26b2cc1521","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"live-window","parent_receipt_hash":"0404040404040404040404040404040404040404040404040404040404040404","policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"d5462e7c710ba3719759ff61e569cf185dcebf1ca12f374fca9a8dd88601c305","sig":"f37f38431ed3e731e27e05d7f3a86c109846fe3cbf45c6383cc1ed937ea77d4edca47becd7c4a1ab98e179d3763356b8a4105a85c714e3899c9533bb27282d0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/ok.json b/wai/pack-conformance/mutations/receipt/ok.json new file mode 100644 index 000000000..0d8fd760a --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/ok.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/parent-kind-live.json b/wai/pack-conformance/mutations/receipt/parent-kind-live.json new file mode 100644 index 000000000..535fef9e0 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/parent-kind-live.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"1a198b17cf576faadf0cfe4dcbe7be1fee7e451307145120c9075f26b2cc1521","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"a5e6c44c2268270461028a317c325e419af15afd006608773f9104e27b2d0808","sig":"273eb0d3e1de76a01e01c1098e6208fdc9c4efed0fe6c05c0faac106ca14d5524ad645610c831940823fdb8e06e763c1324c82b66d744adcfcb2d5b3202a2901","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/parent-kind.json b/wai/pack-conformance/mutations/receipt/parent-kind.json new file mode 100644 index 000000000..1b6436f81 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/parent-kind.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"live-window","parent_receipt_hash":"0404040404040404040404040404040404040404040404040404040404040404","policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"607237889e3366d7547df87faeff129c94a8fb630cb2c2db9b7734d9b5b23f07","sig":"1f13e250c8893ad224a90496f9792e94aedf101028a9f3e8f0aa844e1324eac4431da3120325523167d7a0a46a5c9aaa4187ccd8484b17de5a39a57a00887206","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/policy-digest.json b/wai/pack-conformance/mutations/receipt/policy-digest.json new file mode 100644 index 000000000..ec4f244b5 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/policy-digest.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0202020202020202020202020202020202020202020202020202020202020202","receipt_hash":"7a87af7d26d74407cc93461bb67ecfd0f166bbc74609d454eedd605de51dfc30","sig":"f46605ce5837222b96d7f8da2c5a58640af0c0dd39a73869992f69d25e4da6ed226ded18fae12d8b2e790e765ed386dbfe7150321b7e6b8b8926b052271b6006","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/receipt-hash.json b/wai/pack-conformance/mutations/receipt/receipt-hash.json new file mode 100644 index 000000000..6b103033a --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/receipt-hash.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2747,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/signature.json b/wai/pack-conformance/mutations/receipt/signature.json new file mode 100644 index 000000000..3a58b0fec --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/signature.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"c47f4c9274634eb903492e7ddae2629fcb8c048d25d8ebd10eef07029897be61","sig":"1e484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/stage-root.json b/wai/pack-conformance/mutations/receipt/stage-root.json new file mode 100644 index 000000000..97b3f8a9d --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/stage-root.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"0000000000000000000000000000000000000000000000000000000000000000"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"59a6e3757d95053e656d6849d3b728e1207a44f7cfb69e6fc6b1d4383eb9b115","sig":"eadcbecd10d8a24617af708ae31c898f16115151eeceba3521046e478c5ea92401096409d5e5dd59f225cffcab79185ecaf0aced21eef885b367c2a8539ea305","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/two-components-one-id.json b/wai/pack-conformance/mutations/receipt/two-components-one-id.json new file mode 100644 index 000000000..f58cc7599 --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/two-components-one-id.json @@ -0,0 +1 @@ +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"},{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"31c0763dfaf0816febf5cff79fa80c7014f3afe7f54d2f0059ed33df127ab175","sig":"e7010f2da7361cbddb9e3da971fec47193e85b6cbc3bb103105beae5bcf7f3bceb0b65a91932c0d6c46b9f46d9cf199427463dac011963aca3409e53495c470a","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":5492,"total_entries":12,"unique_objects":5,"version":2,"work_units":12} \ No newline at end of file diff --git a/wai/pack-conformance/mutations/receipt/unknown-member.json b/wai/pack-conformance/mutations/receipt/unknown-member.json new file mode 100644 index 000000000..8efc200ac --- /dev/null +++ b/wai/pack-conformance/mutations/receipt/unknown-member.json @@ -0,0 +1 @@ +{"extra":1,"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"fa8179343882a12c696cd1fdf2eb5e83b2c461b546bca97b683897e815543ff0","sig":"1f484235104243d9836144636997793a2cd54698c00f6472080d4c0af75c40cef879bcd64b956d173ff1f280bf3813bd8a8a109dbe5b9b59f05be8bf482c630c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index a32a71a8b..8008ab14f 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -6,7 +6,8 @@ corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py and the WIV1 parser keyframe-0 decoder from ../int-payload-conformance/int_payload_verify.py, both written from their specifications, reads the capability registry from its published export (../registry/capabilities.json), reads TOML with the standard library's tomllib (TOML -1.0), and implements the rest: +1.0), checks Ed25519 signatures (RFC 8032) with the `cryptography` package, imported only +where a signature is checked, and implements the rest: - the policy (§3): the value-kind walk, the schema with every default, the constraints and their refusal names, the effective policy as JSON, RFC 8785 canonical JSON and the @@ -29,7 +30,31 @@ from their specifications, reads the capability registry from its published expo every coded residual (the stream's symbols) equal to quantize(source − prediction); and the residual table and its offset equal to the fit of those residuals; - the choice of step (§5.6): every candidate's SSE from an encoder of its own (search, - quantization, reconstruction, closed loop), and the step the rule chooses. + quantization, reconstruction, closed loop), and the step the rule chooses; +- the stage index (§6.2–§6.4): the WSIX reader and its refusals, the rules in their + order, each tier from the registry's replicate class, the index digest and the stage + root (a jwp-receipts §2.2 Merkle root); and the object rules of §6.3 over WAI1 and WAI2 + envelopes. Every case of mutations/ (an index; an index and its objects; a receipt and + its indexes) gets its reason, and every file of mutations/ is used by a case; +- the package receipt (§9.1): its JSON reader, its signing payload with the energy class + of energy-measurement §2.7–§2.8, its receipt hash, and its verification against its + indexes; and version 1 (§9.3), every receipt of receipts/: its payload, receipt hash, + segment map root, counts and signature; +- the package (packs/, §11.3, in its order, without the source): the LISTING, the + receipt, policy.json and policy.toml, the indexes, every object, each prefix digest by + decoding its WIV1 base, the plan file, the floor objects, the CMAF files rebuilt byte + for byte from their temporal units (an AV1 OBU reader and sequence header parser, §7.6's + boxes), every catalog rendered byte for byte (§8.3–§8.8), §9.4's claim rule, and no + unlisted or unexpected file; then what only the source decides: the fixture + regenerated and cut, each piece's start, duration, flags and kind, its rules, moments + and class plan (§8.1), each base's sse (§6.6) and step (§5.6) and its decode equal to the + encoder's reconstruction, and the supplied floor (§7.3): its listing, a temporal unit + per frame, shown key frames exactly at the piece starts, and each floor object holding + its piece's units of the stream; +- the package mutations (package-mutations/): each case the golden altered (re-sealed + with its key where the receipt or LISTING alone would catch it), written out and + verified as above without its inputs, and refused with the reason the corpus gives it + (the first check that fails), or accepted where the corpus says `ok`. python3 pack_verify.py @@ -38,7 +63,10 @@ Exit status 0 only if every vector verifies. Python 3.11 or later (tomllib). import hashlib import json import os +import re +import shutil import sys +import tempfile import tomllib HERE = os.path.dirname(os.path.abspath(__file__)) @@ -774,7 +802,21 @@ def noise_shot(lo, hi, seed, cb, cr): return (lambda x, y, t: lo + noise(seed, x, y, t) % (hi - lo + 1)), (cb, cr) +# A span's optional fourth member: LOCAL reads `t` as the frame's index within the span +# (0..n-1) instead of the global frame index, so two LOCAL spans of one shot are equal +# frame for frame. +LOCAL = "local" + + def fixture_spans(name): + """(w, h, spans): each span (frames, shot, blend target or None[, LOCAL]).""" + if name == "video-base-01": + shot = blocks([40, 120, 200], 8, 6, 1, 100, 160) + return 64, 36, [(60, ramp(20, 200, 1, 1, 1, True, 11, 110, 140), None), + (25, shot, None, LOCAL), + (50, ramp(30, 220, 2, 1, 1, True, 12, 150, 120), None), + (25, shot, None, LOCAL), + (40, ramp(16, 160, 1, 2, 1, True, 13, 128, 128), None)] if name == "cuts-01": a = ramp(20, 100, 1, 1, 1, True, 1, 110, 140) b = ramp(120, 220, 2, 1, 1, True, 2, 150, 120) @@ -799,15 +841,18 @@ def fixture(name): cw, ch = (w + 1) // 2, (h + 1) // 2 out = bytearray(f"YUV4MPEG2 W{w} H{h} F25:1 Ip A1:1 C420jpeg\n".encode()) t = 0 - for n, (y_of, chroma), to in spans: + for span in spans: + n, (y_of, chroma), to = span[:3] + local = span[3:] == (LOCAL,) for i in range(n): out += b"FRAME\n" def blend(a, b): return (a * (n - i - 1) + b * (i + 1)) // n + tt = i if local else t for y in range(h): for x in range(w): - v = y_of(x, y, t) - out.append(blend(v, to[0](x, y, t)) if to else v) + v = y_of(x, y, tt) + out.append(blend(v, to[0](x, y, tt)) if to else v) cb, cr = chroma if to: cb, cr = blend(cb, to[1][0]), blend(cr, to[1][1]) @@ -1088,6 +1133,1216 @@ def choose_q(metric_of, candidates, bound): return qs[-1] +# ---------------------------------------------------------------- the stage index (§6.2–§6.4) + + +MEDIA_CLASSES = ["video", "image", "audio", "world", "feed", "avatar", "film"] +ROLES = ["base", "refinement", "floor", "snapshot", "ops", "receipt", "asset"] +BASE, REFINEMENT, FLOOR = 0, 1, 2 +NO_REFINES = 0xFFFFFFFF +PIECE_KINDS = KINDS + ["world", "audio"] +ENTRY_DUP, ENTRY_DERIVED = 2, 4 +TIER_NAMES = [None, "entropy-consistency", "decode-equivalence"] + + +def le(b, at, n): + return int.from_bytes(b[at:at + n], "little") + + +def pad8(n): + return (n + 7) // 8 * 8 + + +def parse_index(b): + """§6.2: a WSIX file read into its header, records, class plan and strings, or the + reader's refusal, the first that applies.""" + if b[:4] != b"WSIX": + raise Refused("index-magic") + if len(b) < 6: + raise Refused("index-truncated") + if le(b, 4, 2) != 1: + raise Refused("index-version") + if len(b) < 160: + raise Refused("index-truncated") + n_p, n_e, n_s, n_c = le(b, 16, 4), le(b, 20, 4), le(b, 24, 4), le(b, 28, 4) + # Every table's size from its count, before anything is read from it. + at_e = 160 + 32 * n_p + at_c = at_e + 128 * n_e + at_plan = at_c + 16 * n_c + at_s = at_plan + pad8(n_c * n_p) + if at_s > len(b): + raise Refused("index-truncated") + raw, pos = [], at_s + for _ in range(n_s): + if pos + 2 > len(b) or pos + 2 + le(b, pos, 2) > len(b): + raise Refused("index-truncated") + raw.append(b[pos + 2:pos + 2 + le(b, pos, 2)]) + pos += 2 + le(b, pos, 2) + end = at_s + pad8(pos - at_s) + if end > len(b): + raise Refused("index-truncated") + P = [b[160 + 32 * i:192 + 32 * i] for i in range(n_p)] + E = [b[at_e + 128 * i:at_e + 128 * (i + 1)] for i in range(n_e)] + C = [b[at_c + 16 * i:at_c + 16 * (i + 1)] for i in range(n_c)] + reserved = [b[146:160], b[at_plan + n_c * n_p:at_s], b[pos:end]] + reserved += [p[28:32] for p in P] + [e[58:60] + e[108:112] + e[120:128] for e in E] + [c[8:16] for c in C] + if any(any(r) for r in reserved): + raise Refused("index-reserved") + if le(b, 6, 2) >= len(MEDIA_CLASSES): + raise Refused("index-media-class") + for e in E: + if e[54] >= len(ROLES): + raise Refused("index-role") + if e[56] >= len(TIER_NAMES): + raise Refused("index-tier") + strings = [] + for s in raw: + try: + strings.append(s.decode("utf-8")) + except UnicodeDecodeError: + raise Refused("index-string") + if not all(strings) or any(i >= n_s for i in [le(b, 144, 2)] + [le(e, 52, 2) for e in E] + [le(c, 0, 2) for c in C]): + raise Refused("index-string") + if end != len(b): + raise Refused("index-trailing") + return { + "raw": b, "media_class": le(b, 6, 2), "tb": (le(b, 8, 4), le(b, 12, 4)), "policy_digest": b[32:64], + "prev": b[64:96], "first_piece": le(b, 96, 8), "first_entry": le(b, 104, 8), "plan_digest": b[112:144], + "component": strings[le(b, 144, 2)], "strings": strings, + "pieces": [{"start": le(p, 0, 8), "duration": le(p, 8, 4), "first_entry": le(p, 12, 4), + "n_entries": le(p, 16, 2), "kind": p[18], "flags": p[19], "rules": le(p, 20, 4), + "moments": le(p, 24, 4)} for p in P], + "entries": [{"record": e, "hash": e[0:32], "byte_len": le(e, 32, 8), "piece": le(e, 40, 8), + "refines": le(e, 48, 4), "cap": strings[le(e, 52, 2)], "role": e[54], "layer": e[55], + "tier": e[56], "flags": e[57], "deadline": le(e, 60, 4), "prefix": e[64:96], + "sse": le(e, 96, 8), "man_len": le(e, 104, 4), "payload_len": le(e, 112, 8)} for e in E], + "classes": [{"name": strings[le(c, 0, 2)], "max_layer": c[2], "flags": c[3], "max_kbps": le(c, 4, 4)} + for c in C], + "plan": [list(b[at_plan + k * n_p:at_plan + (k + 1) * n_p]) for k in range(n_c)], + } + + +def cap_tier(cap): + """§6.4: decode-equivalence (2) for a registry class that reconstructs exactly, + entropy-consistency (1) for neural-entropy-exact, none (0) otherwise.""" + rep = (REGISTRY.get(cap) or {}).get("replicate") + return 2 if rep in ("standard-defined", "neural-int-exact") else 1 if rep == "neural-entropy-exact" else 0 + + +def due_tier(E, i): + """The tier §6.4 gives entry i: a refinement holds decode-equivalence only over a prefix + that does; a floor states none; any other entry its capability's.""" + e = E[i] + if e["role"] == REFINEMENT: + return 2 if E[e["refines"]]["tier"] == 2 and cap_tier(e["cap"]) == 2 else 0 + return 0 if e["role"] == FLOOR else cap_tier(e["cap"]) + + +def owners(ix): + """The window-relative piece of each entry, by the pieces' entry ranges.""" + return [k for k, pc in enumerate(ix["pieces"]) for _ in range(pc["n_entries"])] + + +def highest_layer(ix, k): + pc = ix["pieces"][k] + es = ix["entries"][pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] + return max((e["layer"] for e in es if e["role"] in (BASE, REFINEMENT)), default=0) + + +def index_rules(ix): + """§6.3's rules in their order, with §6.4's tiers; the first broken is the refusal.""" + if 0 in ix["tb"]: + raise Refused("index-timebase") + # An index with no previous window starts the timeline and the chain (§4.5). + if not any(ix["prev"]) and (ix["first_piece"] or ix["first_entry"] or (ix["pieces"] and ix["pieces"][0]["start"])): + raise Refused("window-start") + pieces, E, before = ix["pieces"], ix["entries"], 0 + for k, pc in enumerate(pieces): + if pc["duration"] == 0 or (k and pc["start"] != pieces[k - 1]["start"] + pieces[k - 1]["duration"]): + raise Refused("piece-order") + if pc["first_entry"] != before or pc["n_entries"] == 0: + raise Refused("piece-entries") + if pc["kind"] >= len(PIECE_KINDS) or pc["flags"] > 0x0F: + raise Refused("piece-kind") + before += pc["n_entries"] + if before != len(E): + raise Refused("piece-entries") + rank = {BASE: 0, REFINEMENT: 1, FLOOR: 2} + own = owners(ix) + for i, e in enumerate(E): + pc = pieces[own[i]] + if e["piece"] != ix["first_piece"] + own[i]: + raise Refused("piece-entries") + if e["flags"] > 0x07: + raise Refused("entry-flags") + if i > pc["first_entry"]: + p = E[i - 1] + if rank.get(e["role"], 3) < rank.get(p["role"], 3) or \ + (e["role"] == p["role"] == REFINEMENT and e["layer"] < p["layer"]): + raise Refused("entry-order") + refinement = e["role"] == REFINEMENT + if (e["role"] in (BASE, FLOOR) and e["layer"] > 0) or (refinement and e["layer"] == 0) \ + or refinement != (e["refines"] != NO_REFINES): + raise Refused("layer-role") + if refinement: + if e["refines"] >= len(E): + raise Refused("refines-outside-window") + if not pc["first_entry"] <= e["refines"] < i: + raise Refused("refines-mismatch") + if E[e["refines"]]["layer"] != e["layer"] - 1: + raise Refused("layer-gap") + due = due_tier(E, i) + if e["layer"] > 0 and e["tier"] == 1: + raise Refused("tier-entropy-passthrough") + if e["layer"] > 0 and e["tier"] == 2 and due != 2: + raise Refused("tier-weakest") + if e["tier"] != due: + raise Refused("tier-mismatch") + if any(e["prefix"]) and e["tier"] != 2: + raise Refused("digest-without-tier") + if e["deadline"] and not refinement: + raise Refused("deadline-role") + if e["role"] == FLOOR and not e["flags"] & ENTRY_DERIVED: + raise Refused("floor-derived-from") + earlier = [x for x in E[:i] if x["hash"] == e["hash"]] + if any(x["byte_len"] != e["byte_len"] for x in earlier): + raise Refused("dup-length") + if bool(e["flags"] & ENTRY_DUP) != bool(earlier): + raise Refused("dup-flag") + if len(ix["plan"]) != len(ix["classes"]): + raise Refused("class-plan") + for cl, row in zip(ix["classes"], ix["plan"]): + if cl["flags"] > 0x01: + raise Refused("class-flags") + if len(row) != len(pieces): + raise Refused("class-plan") + for k, v in enumerate(row): + if cl["flags"] & 1: + ok = v == 0xFF + else: + ok = v == 0xFF or (v <= cl["max_layer"] and v <= highest_layer(ix, k)) + if not ok: + raise Refused("class-plan") + + +def merkle_root(items): + """jwp-receipts §2.2: each item under the leaf domain, pairs under the node domain, an + odd level's last node duplicated; no items, 32 zero bytes.""" + if not items: + return bytes(32) + level = [blake3(b"moq-jwp:merkle-leaf\x01" + x) for x in items] + while len(level) > 1: + if len(level) % 2: + level.append(level[-1]) + level = [blake3(b"moq-jwp:merkle-node\x01" + level[i] + level[i + 1]) for i in range(0, len(level), 2)] + return level[0] + + +def index_digest(b): + return blake3(b"wai:pack-stage-index\x01" + b) + + +def stage_root(ix): + """§6.2: the Merkle root over BLAKE3("wai:pack-stage\\x01" ‖ entry record).""" + return merkle_root([blake3(b"wai:pack-stage\x01" + e["record"]) for e in ix["entries"]]) + + +# ---------------------------------------------------------------- objects (§5, §6.3) + + +def content_hash(b): + """jwp-receipts §2.1.""" + return blake3(b"moq-jwp:object\x01" + b) + + +def read_json(b, minus_zero=False): + """JSON in UTF-8 whose objects name no member twice (SPEC §2), or None. With + `minus_zero`, the number -0 is read as the float -0.0, as RFC 8259 readers that keep + its sign do, so no rule that requires an integer accepts it.""" + def members(pairs): + d = {} + for k, v in pairs: + if k in d: + raise ValueError("a member named twice") + d[k] = v + return d + + def no_constant(s): + raise ValueError(s) + + def finite(t): + # RFC 8259 readers that hold numbers as binary64 refuse one past its range. + v = float(t) + if v in (float("inf"), float("-inf")): + raise ValueError(t) + return v + try: + hook = (lambda t: float(t) if t == "-0" else int(t)) if minus_zero else int + return json.loads(b.decode("utf-8"), object_pairs_hook=members, parse_constant=no_constant, parse_int=hook, + parse_float=finite) + except (UnicodeDecodeError, ValueError, RecursionError): + return None + + +def opt_str(o, k): + """An OPTIONAL string member: absent, null, or a string.""" + return k not in o or o[k] is None or isinstance(o[k], str) + + +def prior_ok(p): + """SPEC §3.1: a pin is null, or an object whose id, sha256 and determinism are strings, + form and tier OPTIONAL strings, supersedes null or an object of a string sha256 and an + OPTIONAL string form.""" + if p is None: + return True + if not isinstance(p, dict) or not all(isinstance(p.get(k), str) for k in ("id", "sha256", "determinism")): + return False + sup = p.get("supersedes") + return opt_str(p, "form") and opt_str(p, "tier") and \ + (sup is None or (isinstance(sup, dict) and isinstance(sup.get("sha256"), str) and opt_str(sup, "form"))) + + +def is_number(v): + return isinstance(v, (int, float)) and not isinstance(v, bool) + + +def manifest_typed(m, v2): + """SPEC §3 and §7.1: a manifest that parses with its members' types. wai, media and + intent are strings. A WAI1's model_requirement is an object whose capability is a + string, fallback and sim_metric OPTIONAL strings, min_sim null or a number; its + conditioning an object whose kind is a string and prior a pin; security null or an + object of an OPTIONAL string encryption and an OPTIONAL u64 kid. A WAI2's renditions are + objects whose capability and kind are strings, id, sha256, role, companion_of and + companion_of_id OPTIONAL strings, prior a pin. Members this revision does not define + may have any value; refines, derived_from, envelope_manifest and target any JSON.""" + if not isinstance(m, dict) or not all(isinstance(m.get(k), str) for k in ("wai", "media", "intent")): + return False + if v2: + rs = m.get("renditions") + return isinstance(rs, list) and all( + isinstance(r, dict) and isinstance(r.get("capability"), str) and isinstance(r.get("kind"), str) + and all(opt_str(r, k) for k in ("id", "sha256", "role", "companion_of", "companion_of_id")) and prior_ok(r.get("prior")) + for r in rs) + mr, c, sec = m.get("model_requirement"), m.get("conditioning"), m.get("security") + if not isinstance(mr, dict) or not isinstance(mr.get("capability"), str) or not opt_str(mr, "fallback") \ + or not opt_str(mr, "sim_metric") or not (mr.get("min_sim") is None or is_number(mr["min_sim"])): + return False + if not isinstance(c, dict) or not isinstance(c.get("kind"), str) or not prior_ok(c.get("prior")): + return False + return sec is None or (isinstance(sec, dict) and opt_str(sec, "encryption") + and (sec.get("kid") is None or (is_int(sec["kid"]) and 0 <= sec["kid"] < 1 << 64))) + + +def read_envelope(b): + """Exactly one WAI1 or WAI2 envelope and nothing after it (http-delivery §5.1), its + manifest JSON that parses (SPEC §2, §7.1): (man_len, payload_len, the member that + names the capability, the capability, each payload with the capabilities that decode + it), or None. The member is the WAI1 manifest, or the WAI2's first rendition entry; a + WAI2's payload length is every component's; a WAI1's payload is decoded by its + capability and its fallback (SPEC §4).""" + if len(b) < 8 or b[:4] not in (b"WAI1", b"WAI2"): + return None + ml = le(b, 4, 4) + if 8 + ml > len(b): + return None + man = read_json(b[8:8 + ml]) + # §6.3: "does not parse" is the typed parse with SPEC §3's members. + if not manifest_typed(man, b[:4] == b"WAI2"): + return None + if b[:4] == b"WAI1": + if 12 + ml > len(b) or len(b) != 12 + ml + le(b, 8 + ml, 4): + return None + mr = man.get("model_requirement") if isinstance(man.get("model_requirement"), dict) else {} + caps = [c for c in (mr.get("capability"), mr.get("fallback")) if isinstance(c, str)] + return ml, le(b, 8 + ml, 4), man, mr.get("capability"), [(caps, b[12 + ml:])] + rs = man.get("renditions") + at = 8 + ml + if not isinstance(rs, list) or not rs or not all(isinstance(r, dict) for r in rs) or at + 2 > len(b): + return None + n = le(b, at, 2) + head = at + 2 + 8 * n + if n != len(rs) or head > len(b): + return None + total, parts = 0, [] + for i in range(n): + if le(b, at + 2 + 8 * i, 4) != total: + return None + parts.append(([rs[i].get("capability")], b[head + total:head + total + le(b, at + 6 + 8 * i, 4)])) + total += le(b, at + 6 + 8 * i, 4) + return (ml, total, rs[0], rs[0].get("capability"), parts) if len(b) == head + total else None + + +WIR1_KIND = {"wai.image.int_refine": "rgb8", "wai.video.int_refine": "rgb8", "wai.audio.int_refine": "pcm", + "wai.splat.int_refine": "tensor"} + + +def wir1_parses(cap, p): + """staged-delivery A.1–A.3: a WIR1 payload's parse, for the capability's unit kind.""" + if p[:4] != b"WIR1" or len(p) < 44 or p[4] != 1 or le(p, 42, 2): + return False + elem, pred, mode, n, d0, d1, d2 = p[5], p[6], p[7], le(p, 8, 4), le(p, 12, 4), le(p, 16, 4), le(p, 20, 4) + rn, rd, q, cdf_len = le(p, 24, 4), le(p, 28, 4), le(p, 32, 4), le(p, 40, 2) + if elem not in (1, 2) or pred not in (0, 1, 2) or mode not in (0, 1) or 0 in (n, d0, d1, d2): + return False + if not ((rn >= 1 and rd >= 1) if pred == 2 else rn == rd == 0): + return False + if not 1 <= q <= 65535 or not 3 <= cdf_len <= 4097: + return False + kind = WIR1_KIND[cap] + if kind == "rgb8" and (elem != 1 or pred not in (0, 1) or d2 != 3): + return False + if kind == "pcm" and (elem != 2 or pred not in (0, 2) or d1 != 1): + return False + if kind == "tensor" and (elem != 1 or pred != 0): + return False + if d0 * d1 * d2 > 1 << 26 or n > 1 << 20 or n * d0 * d1 * d2 > 1 << 30: + return False + at = 44 + for _ in range(n if mode else 1): + if at + 4 * cdf_len > len(p): + return False + try: + IPV.check_cdf([le(p, at + 4 * k, 4) for k in range(cdf_len)]) + except IPV.Refused: + return False + at += 4 * cdf_len + if at + 4 * n > len(p): + return False + lens = [le(p, at + 4 * k, 4) for k in range(n)] + if any(x % 4 or x < 8 for x in lens): + return False + return at + 4 * n + sum(lens) == len(p) + + +def payload_parses(cap, p): + """§6.2 with staged-measurement §3.2 and its Appendix A: a payload of a capability WAI + pins to a binary format parses as that format, by the parse a sink runs; a capability + with no pinned format asks nothing of it.""" + if cap in IPV.CAP_MAGIC: + try: + IPV.parse(cap, p) + return True + except IPV.Refused: + return False + return wir1_parses(cap, p) if cap in WIR1_KIND else True + + +def derived_from_ok(df): + """SPEC §7.1, *Derived renditions*: the member's four values in their forms.""" + return isinstance(df, dict) and all(isinstance(df.get(k), str) and df[k] for k in + ("motif", "motif_capability", "renderer", "settings")) \ + and is_hex64(df["motif"]) and is_hex64(df["settings"]) + + +def refines_ok(ix, i, rf): + """§5.4: `base` the content hash of its piece's base, `layer` its layer, and as + producers in `prev` only objects of the layers below it, the entry it refines among them.""" + E, pc = ix["entries"], ix["pieces"][owners(ix)[i]] + e, mine = E[i], E[pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] + base = next((x for x in mine if x["role"] == BASE), None) + if not isinstance(rf, dict) or base is None or rf.get("base") != base["hash"].hex(): + return False + if not is_int(rf.get("layer")) or rf["layer"] != e["layer"]: + return False + prev = rf.get("prev") + if isinstance(prev, str): + producers = [prev] + elif isinstance(prev, list) and prev and all(isinstance(r, dict) for r in prev): + producers = [r.get("producer") for r in prev] + else: + return False + below = {x["hash"].hex() for x in mine if x["role"] in (BASE, REFINEMENT) and x["layer"] < e["layer"]} + return all(p in below for p in producers) and E[e["refines"]]["hash"].hex() in producers + + +def object_rules(ix, i, data): + """§6.3, with the objects in hand: entry i's object, the first rule broken is the refusal.""" + e = ix["entries"][i] + if len(data) != e["byte_len"]: + raise Refused("object-length") + if content_hash(data) != e["hash"]: + raise Refused("object-hash") + env = read_envelope(data) + if env is None: + raise Refused("non-envelope-entry") + ml, pl, m, cap, parts = env + if ml != e["man_len"] or pl != e["payload_len"]: + raise Refused("object-lengths") + if cap != e["cap"]: + raise Refused("object-capability") + wai2 = data[:4] == b"WAI2" + # A refinement's object is a WAI1 that carries refines (§5.4). + if e["role"] == REFINEMENT and (wai2 or not refines_ok(ix, i, m.get("refines"))): + raise Refused("object-refines") + # derived_from: a WAI1's manifest member; a WAI2's entries', present when any entry + # carries it and well formed only when every one that does is. null is absent. + if wai2: + dfs = [r.get("derived_from") for r in read_json(data[8:8 + ml])["renditions"] if r.get("derived_from") is not None] + has, good = bool(dfs), all(derived_from_ok(x) for x in dfs) + else: + has = m.get("derived_from") is not None + good = not has or derived_from_ok(m["derived_from"]) + if has != bool(e["flags"] & ENTRY_DERIVED) or not good: + raise Refused("object-derived-from") + # §6.2, after the other object rules: where WAI checks a payload's format, the lengths + # are its counted bytes (staged-measurement §3), so every such payload parses as its + # format. + if not all(payload_parses(c, p) for caps, p in parts for c in caps): + raise Refused("object-lengths") + return m + + +# ---------------------------------------------------------------- package receipts (§9) + + +PARENT_KINDS = ["none", "live-window", "re-pack"] +# energy-measurement §2.7: a class's tag, and the evidence members each class carries. +CLASS_TAGS = {"HwShunt": 1, "ModelBased": 2, "Estimator": 3, "OnChipCounter": 4, "CalibratedInstrument": 5} +FILTERING = {"Unknown": 0, "Off": 1, "On": 2} +EVIDENCE = {"OnChipCounter": {"energy_uncertainty", "energy_counter_filtering"}, + "CalibratedInstrument": {"energy_uncertainty", "energy_calibration_ref"}} +EVIDENCE_MEMBERS = {"energy_uncertainty", "energy_counter_filtering", "energy_calibration_ref"} +V2_MEMBERS = {"asset_id", "components", "grant", "joules_micro", "kind", "packager", "parent_kind", + "parent_receipt_hash", "policy_digest", "receipt_hash", "sig", "signer_id", "signer_pubkey", + "stored_bytes", "total_bytes", "total_entries", "unique_objects", "version", "work_units"} +V1_MEMBERS = {"asset_id", "grant", "joules_micro", "kind", "parent_receipt_hash", + "receipt_hash", "segment_map_root", "segments", "sig", "signer_id", "signer_pubkey", "stored_bytes", + "total_bytes", "total_segments", "unique_segments", "work_segment_ops"} +RECEIPT_ID = b"wai:video-package-receipt-id\x01" + + +def be(v, n): + return v.to_bytes(n, "big") + + +def lp(s): + s = s.encode() + return be(len(s), 4) + s + + +def is_hex(s, n): + return isinstance(s, str) and len(s) == n and all(c in "0123456789abcdef" for c in s) + + +def is_u53(v): + return is_int(v) and 0 <= v <= MAX_INTEGER + + +def need(ok): + if not ok: + raise Refused("receipt-json") + + +def energy_label(j, strict): + """energy-measurement §2.7: the class's signed encoding from its JSON members, or None + for no class. Evidence without its class, a class without its evidence, and evidence a + class does not carry are refused. Version 2 (`strict`, §9.1) writes the evidence exactly + as its class carries it, with no null in its place; version 1 (§9.3) reads a null + evidence member as absent, and an on-chip counter's filtering, absent, as Unknown.""" + cls = j.get("energy_provenance") + if strict: + need(not any(k in j and j[k] is None for k in EVIDENCE_MEMBERS)) + present = EVIDENCE_MEMBERS & set(j) + else: + present = {k for k in EVIDENCE_MEMBERS if j.get(k) is not None} + if cls is None: + need(not present) + return None + allowed = EVIDENCE.get(cls, set()) + lenient = set() if strict else {"energy_counter_filtering"} + need(isinstance(cls, str) and cls in CLASS_TAGS and present <= allowed and (allowed - present) <= lenient) + out = bytes([CLASS_TAGS[cls]]) + if cls in EVIDENCE: + u = j["energy_uncertainty"] + need(isinstance(u, dict) and set(u) == {"relative_ppm", "window_us"} and is_u53(u["relative_ppm"]) + and u["relative_ppm"] <= U32 and is_u53(u["window_us"])) + out += be(u["relative_ppm"], 4) + be(u["window_us"], 8) + if cls == "OnChipCounter": + f = j.get("energy_counter_filtering") or "Unknown" + need(isinstance(f, str) and f in FILTERING) + out += bytes([FILTERING[f]]) + else: + ref = j["energy_calibration_ref"] + need(isinstance(ref, str) and ref != "") + out += lp(ref) + return out + + +def grant_bytes(g): + """The grant as §9.1 (and version 1) sign it: grant_hash ‖ u32 len ‖ capability ‖ u64 + joule_ceiling_micro (null: 2^64 − 1) ‖ u8 0, or u8 1 ‖ u64 funds_ceiling.""" + need(isinstance(g, dict) and set(g) == {"capability", "funds_ceiling", "grant_hash", "joule_ceiling_micro"}) + need(isinstance(g["capability"], str) and is_hex(g["grant_hash"], 64)) + need(g["joule_ceiling_micro"] is None or is_u53(g["joule_ceiling_micro"])) + need(g["funds_ceiling"] is None or is_u53(g["funds_ceiling"])) + out = bytes.fromhex(g["grant_hash"]) + lp(g["capability"]) + out += be(U64 if g["joule_ceiling_micro"] is None else g["joule_ceiling_micro"], 8) + return out + (b"\x00" if g["funds_ceiling"] is None else b"\x01" + be(g["funds_ceiling"], 8)) + + +def v1_leaf(s): + return blake3(b"wai:video-package-seg\x01" + be(s["index"], 8) + bytes.fromhex(s["content_hash"]) + + be(s["duration_ms"], 8) + be(s["byte_len"], 8)) + + +def read_receipt(b): + """§9.1 and §9.3: a receipt's JSON read as version 1 or 2, every member in its form, + its signing payload rebuilt and its receipt_hash checked; refuses receipt-version and + receipt-json. Returns the version, the JSON, the payload and the class encoding.""" + # RFC 8259 with no member named twice; -0 is read as a number that is not an integer, + # so it is refused where an integer is required; a string that cannot be written in + # UTF-8 (a lone surrogate escape) has no form (receipt-json). + j = read_json(b, minus_zero=True) + need(isinstance(j, dict)) + try: + r = receipt_payload(j) + need(blake3(RECEIPT_ID + r["payload"] + bytes.fromhex(j["sig"])).hex() == j["receipt_hash"]) + except (UnicodeEncodeError, OverflowError, ValueError): + raise Refused("receipt-json") + return r + + +def receipt_payload(j): + """A receipt's members read in their forms and its signing payload: version 2's + (§9.1), or version 1's (§9.3), the version a receipt without `version` has.""" + v = j.get("version", 1) + if not is_int(v) or v not in (1, 2): + raise Refused("receipt-version") + need(isinstance(j.get("asset_id"), str) and j.get("kind") == "package") + # Version 2 writes every member, null where there is none (energy_provenance among + # them, §9.1); version 1 may leave energy_provenance out (§9.3). + if v == 2: + need(V2_MEMBERS | {"energy_provenance"} <= set(j) and set(j) <= V2_MEMBERS | {"energy_provenance"} | EVIDENCE_MEMBERS) + else: + need(V1_MEMBERS <= set(j) and set(j) <= V1_MEMBERS | {"version", "energy_provenance"} | EVIDENCE_MEMBERS) + label = energy_label(j, v == 2) + counts = ("total_entries", "unique_objects", "total_bytes", "stored_bytes", "work_units") if v == 2 else \ + ("total_segments", "unique_segments", "total_bytes", "stored_bytes", "work_segment_ops") + need(all(is_u53(j[k]) for k in counts + ("joules_micro",))) + need(is_hex(j["receipt_hash"], 64) and is_hex(j["sig"], 128) and is_hex(j["signer_pubkey"], 64)) + need(isinstance(j["signer_id"], str)) + grant = grant_bytes(j["grant"]) + signer = bytes.fromhex(j["signer_pubkey"]) + lp(j["signer_id"]) + figure = be(j["joules_micro"], 8) + if v == 2: + need(j["parent_kind"] in PARENT_KINDS if isinstance(j["parent_kind"], str) else False) + none = j["parent_kind"] == "none" + need(j["parent_receipt_hash"] is None if none else is_hex(j["parent_receipt_hash"], 64)) + need(is_hex(j["policy_digest"], 64) and isinstance(j["packager"], str)) + cs = j["components"] + need(isinstance(cs, list)) + for c in cs: + need(isinstance(c, dict) and set(c) == {"id", "index", "index_digest", "media_class", "stage_root"}) + need(isinstance(c["id"], str) and c["id"] != "" and c["index"] == f"index-{c['id']}.wsix") + need(is_hex(c["index_digest"], 64) and is_hex(c["stage_root"], 64)) + need(isinstance(c["media_class"], str) and c["media_class"] in MEDIA_CLASSES) + payload = b"wai:pack-receipt" + (b"\x01" if label is None else b"\x02") + payload += lp(j["asset_id"]) + bytes.fromhex(j["policy_digest"]) + payload += bytes([PARENT_KINDS.index(j["parent_kind"])]) + payload += b"" if none else bytes.fromhex(j["parent_receipt_hash"]) + payload += be(len(cs), 4) + for c in cs: + payload += lp(c["id"]) + bytes([MEDIA_CLASSES.index(c["media_class"])]) + payload += bytes.fromhex(c["index_digest"]) + bytes.fromhex(c["stage_root"]) + payload += b"".join(be(j[k], 8) for k in counts) + lp(j["packager"]) + payload += figure + (label or b"") + grant + signer + else: + need(j["parent_receipt_hash"] is None or is_hex(j["parent_receipt_hash"], 64)) + need(is_hex(j["segment_map_root"], 64) and isinstance(j["segments"], list)) + for s in j["segments"]: + need(isinstance(s, dict) and set(s) == {"byte_len", "content_hash", "duration_ms", "index"}) + need(is_u53(s["byte_len"]) and is_u53(s["duration_ms"]) and is_u53(s["index"])) + need(is_hex(s["content_hash"], 64)) + payload = b"wai:video-package-receipt\x01" + lp(j["asset_id"]) + bytes.fromhex(j["segment_map_root"]) + payload += b"".join(be(j[k], 8) for k in counts) + figure + (label or b"\x00") + grant + payload += b"\x00" if j["parent_receipt_hash"] is None else b"\x01" + bytes.fromhex(j["parent_receipt_hash"]) + payload += be(len(j["segments"]), 8) + b"".join(v1_leaf(s) for s in j["segments"]) + signer + return {"version": v, "json": j, "payload": payload, "label": label} + + +def ed25519_verifies(pk, sig, msg): + """RFC 8032, through the `cryptography` package, imported here so the rest of the + verifier runs on the standard library alone.""" + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric import ed25519 + try: + ed25519.Ed25519PublicKey.from_public_bytes(pk).verify(sig, msg) + return True + except (InvalidSignature, ValueError): + return False + + +def ed25519_from_seed(seed, msg): + """The public key of a 32-byte seed and its signature over msg (RFC 8032; deterministic).""" + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import ed25519 + sk = ed25519.Ed25519PrivateKey.from_private_bytes(seed) + pk = sk.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return pk, sk.sign(msg) + + +def package_counts(ixs): + """§9.1: entries, distinct content hashes, referenced and stored bytes, work units; None + when a content hash comes at two lengths.""" + E = [e for ix in ixs for e in ix["entries"]] + first = {} + for e in E: + if first.setdefault(e["hash"], e["byte_len"]) != e["byte_len"]: + return None + return {"total_entries": len(E), "unique_objects": len(first), "total_bytes": sum(e["byte_len"] for e in E), + "stored_bytes": sum(first.values()), "work_units": len(E)} + + +def verify_receipt_v2(r, index_files): + """§9.1's verification of a version-2 receipt against its indexes, in its components' + order: the first refusal that applies. Returns the parsed indexes.""" + j = r["json"] + if r["version"] != 2: + raise Refused("receipt-version") + if not j["components"] or len(index_files) != len(j["components"]): + raise Refused("receipt-index-count") + ixs = [] + for c, b in zip(j["components"], index_files): + ix = parse_index(b) + index_rules(ix) + if index_digest(b).hex() != c["index_digest"]: + raise Refused("receipt-index-digest") + if stage_root(ix).hex() != c["stage_root"]: + raise Refused("receipt-stage-root") + if ix["component"] != c["id"] or MEDIA_CLASSES[ix["media_class"]] != c["media_class"]: + raise Refused("receipt-component") + if ix["policy_digest"].hex() != j["policy_digest"]: + raise Refused("receipt-policy-digest") + ixs.append(ix) + if len({c["id"] for c in j["components"]}) != len(j["components"]): + raise Refused("receipt-component") + counts = package_counts(ixs) + if counts is None or any(counts[k] != j[k] for k in counts): + raise Refused("receipt-counts") + live = [any(ix["prev"]) for ix in ixs] + if not (all(live) if j["parent_kind"] == "live-window" else not any(live)): + raise Refused("receipt-parent-kind") + lab = r["label"] + if (lab is None) != (j["joules_micro"] == 0) or (lab is not None and lab[0] == CLASS_TAGS["HwShunt"]): + raise Refused("receipt-energy") + if not ed25519_verifies(bytes.fromhex(j["signer_pubkey"]), bytes.fromhex(j["sig"]), r["payload"]): + raise Refused("receipt-signature") + return ixs + + +# ---------------------------------------------------------------- the AV1 floor (§7) + + +class Malformed(Exception): + pass + + +def obus(b): + """The low-overhead OBU format (AV1 §5.3): every OBU with its size field, read into + (type, the OBU's bytes, its payload).""" + out, p = [], 0 + while p < len(b): + hdr = b[p] + if hdr & 0x80 or not hdr & 0x02: + raise Malformed("the forbidden bit, or an OBU without its size field") + q, size, shift = p + 1 + (hdr >> 2 & 1), 0, 0 + for k in range(8): + if q >= len(b): + raise Malformed("a size field past the end") + size |= (b[q] & 0x7F) << shift + shift, q = shift + 7, q + 1 + if not b[q - 1] & 0x80: + break + else: + raise Malformed("a size field over 8 bytes") + if q + size > len(b): + raise Malformed("an OBU past the end") + out.append((hdr >> 3 & 0x0F, b[p:q + size], b[q:q + size])) + p = q + size + return out + + +OBU_SEQ, OBU_TD, OBU_FRAME_HEADER, OBU_FRAME = 1, 2, 3, 6 + + +def temporal_units(b): + """A stream split at each temporal delimiter: (the unit's bytes, its OBUs).""" + units = [] + for t, whole, payload in obus(b): + if t == OBU_TD: + units.append([b"", []]) + elif not units: + raise Malformed("an OBU before the first temporal delimiter") + units[-1][0] += whole + units[-1][1].append((t, whole, payload)) + return [(u, o) for u, o in units] + + +class Bits: + def __init__(self, b): + self.b, self.p = b, 0 + + def f(self, n): + v = 0 + for _ in range(n): + if self.p >= 8 * len(self.b): + raise Malformed("a sequence header past its end") + v = v << 1 | (self.b[self.p >> 3] >> (7 - (self.p & 7)) & 1) + self.p += 1 + return v + + def uvlc(self): + zeros = 0 + while not self.f(1): + zeros += 1 + return (1 << 32) - 1 if zeros >= 32 else self.f(zeros) + (1 << zeros) - 1 + + +def sequence_header(payload): + """sequence_header_obu (AV1 §5.5): profile, operating point 0's level and tier, the + frame size, and color_config's bit depth, monochrome and chroma fields.""" + r = Bits(payload) + s = {"profile": r.f(3), "still": r.f(1), "reduced": r.f(1)} + if s["reduced"]: + s["level"], s["tier"] = r.f(5), 0 + else: + decoder_model = 0 + if r.f(1): # timing_info_present_flag + r.f(32), r.f(32) + if r.f(1): # equal_picture_interval + r.uvlc() + decoder_model = r.f(1) + if decoder_model: + delay_bits = r.f(5) + 1 + r.f(32), r.f(5), r.f(5) + initial_delay = r.f(1) + for i in range(r.f(5) + 1): + r.f(12) + level = r.f(5) + tier = r.f(1) if level > 7 else 0 + if i == 0: + s["level"], s["tier"] = level, tier + if decoder_model and r.f(1): + r.f(delay_bits), r.f(delay_bits), r.f(1) + if initial_delay and r.f(1): + r.f(4) + wb, hb = r.f(4) + 1, r.f(4) + 1 + s["max_w"], s["max_h"] = r.f(wb) + 1, r.f(hb) + 1 + if not s["reduced"] and r.f(1): # frame_id_numbers_present_flag + r.f(4), r.f(3) + r.f(1), r.f(1), r.f(1) # 128x128 superblock, filter intra, intra edge filter + if not s["reduced"]: + r.f(1), r.f(1), r.f(1), r.f(1) # interintra, masked compound, warped motion, dual filter + order_hint = r.f(1) + if order_hint: + r.f(1), r.f(1) # jnt_comp, ref_frame_mvs + screen = 2 if r.f(1) else r.f(1) # seq_choose_screen_content_tools, else its value + if screen > 0 and not r.f(1): # seq_choose_integer_mv + r.f(1) + if order_hint: + r.f(3) + r.f(1), r.f(1), r.f(1) # superres, cdef, restoration + hbd = r.f(1) + s["high_bitdepth"], s["twelve_bit"] = hbd, 0 + if s["profile"] == 2 and hbd: + s["twelve_bit"] = r.f(1) + s["bit_depth"] = 12 if s["twelve_bit"] else 10 + else: + s["bit_depth"] = 10 if hbd else 8 + s["mono"] = 0 if s["profile"] == 1 else r.f(1) + cp = tc = mc = 2 + if r.f(1): # color_description_present_flag + cp, tc, mc = r.f(8), r.f(8), r.f(8) + s["csp"] = 0 + if s["mono"]: + r.f(1) + s["ssx"] = s["ssy"] = 1 + elif cp == 1 and tc == 13 and mc == 0: + s["ssx"] = s["ssy"] = 0 + else: + r.f(1) + if s["profile"] == 0: + s["ssx"] = s["ssy"] = 1 + elif s["profile"] == 1: + s["ssx"] = s["ssy"] = 0 + elif s["bit_depth"] == 12: + s["ssx"] = r.f(1) + s["ssy"] = r.f(1) if s["ssx"] else 0 + else: + s["ssx"], s["ssy"] = 1, 0 + if s["ssx"] and s["ssy"]: + s["csp"] = r.f(2) + return s + + +def unit_frame(obu_list): + """The first frame header or frame OBU of a unit, or None.""" + return next((p for t, _, p in obu_list if t in (OBU_FRAME_HEADER, OBU_FRAME)), None) + + +def shown_key(obu_list, seq): + """§7.3: a unit starts at a shown key frame when its first frame header has + show_existing_frame 0, frame_type KEY_FRAME and show_frame 1 (AV1 §5.9.2), or the + sequence uses the reduced still-picture header.""" + p = unit_frame(obu_list) + if p is None: + return False + if seq["reduced"]: + return True + return len(p) > 0 and p[0] >> 7 == 0 and p[0] >> 5 & 3 == 0 and p[0] >> 4 & 1 == 1 + + +def key_frame(obu_list, seq): + """A unit whose first frame header is a key frame, shown or not.""" + p = unit_frame(obu_list) + return p is not None and (seq["reduced"] or (len(p) > 0 and p[0] >> 7 == 0 and p[0] >> 5 & 3 == 0)) + + +def av1c(seq, seq_obu): + """§7.6: the AV1 binding's configuration record: marker and version 1, the profile, + level, tier, bit depth, monochrome and chroma fields, no initial presentation delay, + then the sequence header OBU.""" + return bytes([0x81, seq["profile"] << 5 | seq["level"], + seq["tier"] << 7 | seq["high_bitdepth"] << 6 | seq["twelve_bit"] << 5 | seq["mono"] << 4 + | seq["ssx"] << 3 | seq["ssy"] << 2 | seq["csp"], 0]) + seq_obu + + +def av1_codecs(seq): + """§7.6: av01....""" + return f"av01.{seq['profile']}.{seq['level']:02d}{'H' if seq['tier'] else 'M'}.{seq['bit_depth']:02d}" + + +def floor_payload(p): + """§7.2: n, h, w, fps_num, fps_den as u32 LE, then n units, each a u32 LE length and its + bytes; None when it is not that.""" + if len(p) < 20: + return None + n, h, w, fn, fd = (le(p, 4 * i, 4) for i in range(5)) + units, at = [], 20 + for _ in range(n): + if at + 4 > len(p) or at + 4 + le(p, at, 4) > len(p): + return None + units.append(p[at + 4:at + 4 + le(p, at, 4)]) + at += 4 + le(p, at, 4) + return (n, h, w, fn, fd, units) if at == len(p) else None + + +# ---------------------------------------------------------------- CMAF (§7.6) + + +def u16(v): + return be(v, 2) + + +def u32(v): + return be(v, 4) + + +def box(kind, *parts): + body = b"".join(parts) + return u32(8 + len(body)) + kind + body + + +def full_box(kind, version, flags, *parts): + return box(kind, bytes([version]) + be(flags, 3), *parts) + + +UNITY = b"".join(u32(x) for x in (0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000)) + + +def language(code): + """ISO/IEC 14496-12 mdhd: three letters, each minus 0x60 in five bits.""" + return (ord(code[0]) - 0x60) << 10 | (ord(code[1]) - 0x60) << 5 | (ord(code[2]) - 0x60) + + +def init_mp4(tb_num, w, h, config): + """§7.6's init.mp4: ftyp, then moov (mvhd, trak, mvex), every time 0.""" + ftyp = box(b"ftyp", b"cmfc", u32(0), b"cmfc", b"iso6", b"av01") + mvhd = full_box(b"mvhd", 0, 0, u32(0), u32(0), u32(tb_num), u32(0), u32(0x00010000), u16(0x0100), + bytes(10), UNITY, bytes(24), u32(2)) + tkhd = full_box(b"tkhd", 0, 3, u32(0), u32(0), u32(1), u32(0), u32(0), bytes(8), u16(0), u16(0), u16(0), + u16(0), UNITY, u32(w << 16), u32(h << 16)) + mdhd = full_box(b"mdhd", 0, 0, u32(0), u32(0), u32(tb_num), u32(0), u16(language("und")), u16(0)) + hdlr = full_box(b"hdlr", 0, 0, u32(0), b"vide", bytes(12), b"video\x00") + vmhd = full_box(b"vmhd", 0, 1, u16(0), bytes(6)) + dinf = box(b"dinf", full_box(b"dref", 0, 0, u32(1), full_box(b"url ", 0, 1))) + av01 = box(b"av01", bytes(6), u16(1), u16(0), u16(0), bytes(12), u16(w), u16(h), u32(0x00480000), + u32(0x00480000), u32(0), u16(1), bytes(32), u16(24), u16(0xFFFF), box(b"av1C", config)) + stbl = box(b"stbl", full_box(b"stsd", 0, 0, u32(1), av01), full_box(b"stts", 0, 0, u32(0)), + full_box(b"stsc", 0, 0, u32(0)), full_box(b"stsz", 0, 0, u32(0), u32(0)), + full_box(b"stco", 0, 0, u32(0))) + trak = box(b"trak", tkhd, box(b"mdia", mdhd, hdlr, box(b"minf", vmhd, dinf, stbl))) + mvex = box(b"mvex", full_box(b"trex", 0, 0, u32(1), u32(1), u32(0), u32(0), u32(0))) + return ftyp + box(b"moov", mvhd, trak, mvex) + + +def fragment(piece, start, tb_den, samples): + """§7.6's fragment: moof (mfhd, traf: tfhd, tfdt, trun with the offset of the mdat + payload), then mdat. samples: (bytes, at a shown key frame).""" + def moof(offset): + trun = full_box(b"trun", 0, 0x000701, u32(len(samples)), u32(offset), + *(u32(tb_den) + u32(len(s)) + u32(0x02000000 if key else 0x01010000) for s, key in samples)) + traf = box(b"traf", full_box(b"tfhd", 0, 0x020000, u32(1)), full_box(b"tfdt", 1, 0, be(start * tb_den, 8)), + trun) + return box(b"moof", full_box(b"mfhd", 0, 0, u32(piece + 1)), traf) + m = moof(0) + return moof(len(m) + 8) + box(b"mdat", *(s for s, _ in samples)) + + +def without_delimiters(obu_list): + return b"".join(whole for t, whole, _ in obu_list if t != OBU_TD) + + +# ---------------------------------------------------------------- classes and catalogs (§8) + + +def ms_text(units, num, den): + """§8.3: floor(d · den · 1000 / num) milliseconds as S.mmm.""" + ms = units * den * 1000 // num + return f"{ms // 1000}.{ms % 1000:03d}" + + +def bps(nbytes, units, num, den): + return -(-8 * nbytes * num // (units * den)) + + +def listed_bytes(ix, k, top): + """The bytes of piece k's bases and refinements up to layer `top`.""" + pc = ix["pieces"][k] + return sum(e["byte_len"] for e in ix["entries"][pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] + if e["role"] in (BASE, REFINEMENT) and e["layer"] <= top) + + +def class_plan(P, ix): + """§8.1: each piece's moments and matching rules as bits, and per class the highest + layer it lists for each piece, 0xFF for the floor only.""" + num, den = ix["tb"] + bits, rows = [], [[] for _ in P["class"]] + for k, pc in enumerate(ix["pieces"]): + s_ms, e_ms = pc["start"] * den * 1000 // num, (pc["start"] + pc["duration"]) * den * 1000 // num + moments = [i for i, m in enumerate(P["moment"]) if s_ms < m["to_ms"] and e_ms > m["from_ms"]] + tags = {t for i in moments for t in P["moment"][i]["tags"]} + rules, then = [], {} + for i, r in enumerate(P["rule"]): + w = r["when"] + if "piece_kind" in w and PIECE_KINDS[pc["kind"]] not in w["piece_kind"]: + continue + if "moment_tag" in w and not tags & set(w["moment_tag"]): + continue + rules.append(i) + then.update(r["then"]) + bits.append((sum(1 << i for i in rules), sum(1 << i for i in moments))) + for row, cl in zip(rows, P["class"]): + if cl["floor_only"]: + row.append(0xFF) + continue + top = min(cl["max_layer"], then.get("max_layer", 255), highest_layer(ix, k)) + if "max_kbps" in cl: + while top >= 0 and -(-8 * listed_bytes(ix, k, top) * num // (1000 * pc["duration"] * den)) \ + > cl["max_kbps"]: + top -= 1 + row.append(0xFF if top < 0 else top) + return bits, rows + + +def listed(ix, k, top): + """The bases and refinements piece k lists up to layer `top`, in entry order.""" + pc = ix["pieces"][k] + if top == 0xFF: + return [] + return [e for e in ix["entries"][pc["first_entry"]:pc["first_entry"] + pc["n_entries"]] + if e["role"] in (BASE, REFINEMENT) and e["layer"] <= top] + + +def codecs_value(objects): + """http-delivery §4.2 over a sequence of WAI1 objects (entries): the union, an alt per + distinct capability of the objects that are not layers, a layer's capability after `+` + in its base's alt; and the floor, the alts every such object has.""" + alts, joined = [], {} + for e in objects: + if e["role"] != REFINEMENT and e["cap"] not in alts: + alts.append(e["cap"]) + bases = [e for e in objects if e["role"] != REFINEMENT] + for e in objects: + if e["role"] == REFINEMENT: + base_cap = e.get("base_cap") + joined.setdefault(base_cap, []) + if e["cap"] not in joined[base_cap]: + joined[base_cap].append(e["cap"]) + + def alt(c): + return "+".join(x[len("wai."):] for x in [c] + joined.get(c, [])) + union = "wai1." + "|".join(alt(c) for c in alts) + floor = "wai1." + "|".join(alt(c) for c in alts if all(b["cap"] == c for b in bases)) + return union, floor + + +def render_catalogs(P, ix, c, floor, size, policy_hex): + """§8.3–§8.8 for one component c: every catalog file as the index, the objects and the + floor give it. floor: (codecs, width, height, [(fragment name, bytes)]); size: (W, H).""" + num, den = ix["tb"] + pieces, plan = ix["pieces"], ix["plan"] + F, FW, FH, frags = floor + W, H = size + first_piece, first_start = ix["first_piece"], pieces[0]["start"] + total = sum(pc["duration"] for pc in pieces) + tgt = max(-(-pc["duration"] * den // num) for pc in pieces) + out = {} + + def text(lines): + return "".join(x + "\n" for x in lines).encode() + + def rates(per): + """(BANDWIDTH, AVERAGE-BANDWIDTH) over (bytes, units) pairs.""" + return (max(bps(n, d, num, den) for n, d in per), + bps(sum(n for n, _ in per), sum(d for _, d in per), num, den)) + fb, fa = rates([(len(fr), pc["duration"]) for (_, fr), pc in zip(frags, pieces)]) + head = ["#EXTM3U", "#EXT-X-VERSION:7", "#EXT-X-INDEPENDENT-SEGMENTS"] + inf = f'#EXT-X-STREAM-INF:BANDWIDTH={fb},AVERAGE-BANDWIDTH={fa},CODECS="{F}",RESOLUTION={FW}x{FH}' + out["hls/classic/master.m3u8"] = text(head + [inf, f"floor-{c}.m3u8"]) + media = ["#EXTM3U", "#EXT-X-VERSION:7", f"#EXT-X-TARGETDURATION:{tgt}", "#EXT-X-MEDIA-SEQUENCE:0", + "#EXT-X-PLAYLIST-TYPE:VOD", "#EXT-X-INDEPENDENT-SEGMENTS"] + lines = media + [f'#EXT-X-MAP:URI="../../floor/{c}/init.mp4"'] + for (name, _), pc in zip(frags, pieces): + lines += [f"#EXTINF:{ms_text(pc['duration'], num, den)},", f"../../floor/{c}/{name}"] + out[f"hls/classic/floor-{c}.m3u8"] = text(lines + ["#EXT-X-ENDLIST"]) + mpd_head = ['', + '', + ' '] + mpd_tail = [" ", " ", ""] + + def timeline(ks): + return [" "] + \ + [f' ' + for k in ks] + [" "] + lines = mpd_head + [ + f' ', + f' ', f' ', + f' '] + timeline(range(len(pieces))) + lines += [f' ' for name, _ in frags] + out["dash/classic.mpd"] = text(lines + [" ", " "] + mpd_tail) + E = ix["entries"] + by_hash = {} + for e in E: + by_hash.setdefault(e["hash"], e) + for ci, cl in enumerate(P["class"]): + name, row = cl["id"], plan[ci] + lists = [listed(ix, k, row[k]) for k in range(len(pieces))] + wai = any(lists) + lines = head[:] + if wai: + lines.append('#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8"') + out[f"hls/{name}/master.m3u8"] = text(lines + [inf, f"../classic/floor-{c}.m3u8"]) + views = [] + for k, pc in enumerate(pieces): + v = {"dur": pc["duration"], "p": first_piece + k, "start": pc["start"], "stages": []} + if row[k] == 0xFF: + v["floor_only"] = True + for e in lists[k]: + s = {"bytes": e["byte_len"], "h": e["hash"].hex(), "layer": e["layer"]} + if e["role"] == REFINEMENT: + s["refines"] = E[e["refines"]]["hash"].hex() + s["deadline_ms"] = e["deadline"] + v["stages"].append(s) + views.append(v) + comp = {"pieces": views} + if any(lists[k] for k in range(len(pieces))): + comp["media_playlist"] = f"../hls/{name}/wai-{c}.m3u8" + view = {"class": name, "components": {c: comp}, "policy_digest": policy_hex} + out[f"stages/{name}.json"] = (jcs(view) + "\n").encode() + if not wai: + continue + objs = [] + for k in range(len(pieces)): + base = next((e for e in lists[k] if e["role"] == BASE), None) + for e in lists[k]: + objs.append(dict(e, base_cap=base and base["cap"])) + union, fl = codecs_value(objs) + wb, wa = rates([(sum(e["byte_len"] for e in lists[k]), pieces[k]["duration"]) + for k in range(len(pieces)) if lists[k]]) + out[f"hls/{name}/wai.m3u8"] = text(head + [ + f'#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="{name}"', + f'#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/{name}.json"', + f'#EXT-X-STREAM-INF:BANDWIDTH={wb},AVERAGE-BANDWIDTH={wa},CODECS="{union}",X-WAI-FLOOR="{fl}",' + f'RESOLUTION={W}x{H}', f"wai-{c}.m3u8"]) + lines = media[:] + for k, pc in enumerate(pieces): + p0 = pc["first_entry"] + base = next(e for e in E[p0:p0 + pc["n_entries"]] if e["role"] == BASE) + if row[k] == 0xFF: + lines.append("#EXT-X-GAP") + lines += [f"#EXTINF:{ms_text(pc['duration'], num, den)},", + f"../../objects/{base['hash'].hex()}.wai#n={base['byte_len']}"] + lines += [f'#WAI-LAYER:N={e["layer"]},URI="../../objects/{e["hash"].hex()}.wai#n={e["byte_len"]}"' + for e in lists[k] if e["role"] == REFINEMENT] + out[f"hls/{name}/wai-{c}.m3u8"] = text(lines + ["#EXT-X-ENDLIST"]) + lines = mpd_head + [ + f' ', + ' ', + f' ', + f' ', + ' '] + for layer in range(max(v for v in row if v != 0xFF) + 1): + ks = [k for k in range(len(pieces)) if any(e["layer"] == layer for e in lists[k])] + mine = [[e for e in lists[k] if e["layer"] == layer] for k in ks] + peak = max((bps(sum(e["byte_len"] for e in m), pieces[k]["duration"], num, den) + for k, m in zip(ks, mine)), default=0) + dep = f' dependencyId="{c}-l{layer - 1}"' if layer else "" + lines += [f' ', + f' '] + timeline(ks) + lines += [f' ' for m in mine for e in m] + lines += [" ", " "] + out[f"dash/{name}.mpd"] = text(lines + mpd_tail) + has_floor = any(e["role"] == FLOOR for e in E) + out["moq/catalog.json"] = render_moq(c, E, P["live"]["layer_lag_ms"], P["defaults"]["deadline_ms"], + (F, FW, FH) if has_floor else None) + return out + + +def render_moq(c, E, lag, default_deadline, floor): + """§8.4: moq/catalog.json. One track per (layer, deadline), by layer, then by deadline + ascending; a refinement track depends on every track that carries an entry its entries + refine, in track order, so it never names a track the catalog does not hold. floor: + (codecs, width, height) or None.""" + keys = sorted({(e["layer"], e["deadline"]) for e in E if e["role"] in (BASE, REFINEMENT)}) + + def name(key): + layer, deadline = key + return f"{c}/l{layer}" + (f"@{deadline}" if layer and deadline != default_deadline else "") + tracks = [] + for ti, (layer, deadline) in enumerate(keys): + es = [e for e in E if e["role"] in (BASE, REFINEMENT) and (e["layer"], e["deadline"]) == (layer, deadline)] + caps = [] + for e in es: + if e["cap"] not in caps: + caps.append(e["cap"]) + t = {"name": name((layer, deadline)), "packaging": "wai", "renderGroup": 1, + "priority": 64 + 32 * layer, "wai_capabilities": caps, "wai_layer": layer, "wai_receipts": False} + if min(e["tier"] for e in es): + t["wai_determinism"] = TIER_NAMES[min(e["tier"] for e in es)] + if layer == 0 and floor is not None: + t["altGroup"] = 1 + if layer: + deps = set() + for e in es: + below = E[e["refines"]] + dt = keys.index((below["layer"], below["deadline"])) + if dt >= ti: + raise ValueError("a refinement refines an entry on no earlier track") + deps.add(dt) + t["depends"] = [name(keys[x]) for x in sorted(deps)] + t["wai_delivery_timeout_ms"] = max(0, deadline - (lag[layer] if layer < len(lag) else 0)) + tracks.append(t) + if floor is not None: + tracks.append({"altGroup": 1, "codec": floor[0], "height": floor[2], "name": f"{c}/floor", "packaging": "cmaf", + "renderGroup": 1, "width": floor[1]}) + return (jcs({"tracks": tracks, "version": 1}) + "\n").encode() + + # ---------------------------------------------------------------- the corpus @@ -1108,13 +2363,579 @@ def load(path): return f.read() +def tree(root): + """Every file below root, as paths, in byte order, read strictly (§11.3 step 1): every + name, one beginning with `.` and a `*.sha256` file included; None when an entry is a + symbolic link or neither a file nor a directory, when a directory cannot be read, or + when a directory below root holds nothing (§11.1: no other file or directory).""" + def unreadable(e): + raise e + out = [] + try: + for at, dirs, files in os.walk(root, onerror=unreadable): + if at != root and not dirs and not files: + return None + for n in dirs + files: + full = os.path.join(at, n) + if os.path.islink(full) or not (os.path.isdir(full) or os.path.isfile(full)): + return None + out += [os.path.relpath(os.path.join(at, f), root).replace(os.sep, "/") for f in files] + except OSError: + return None + return sorted(out, key=lambda p: p.encode()) + + +SEGMENT_FIRST = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-") + + +def segment_ok(s): + """SPEC §3.1: a path segment.""" + return s != "" and s[0] in SEGMENT_FIRST and all(c in SEGMENT_FIRST | {"."} for c in s) and not s.endswith(".") + + +def listing(root, paths): + """SPEC §3.1: a line per file, its SHA-256, two spaces, its path and a line feed.""" + return "".join(hashlib.sha256(load(os.path.join(root, p))).hexdigest() + " " + p + "\n" for p in paths).encode() + + +def seg_params(P, tb): + s = P["segmentation"] + return {"min": units_from_ms(s["min_ms"], *tb), "target": units_from_ms(s["target_ms"], *tb), + "max": units_from_ms(s["max_ms"], *tb), "cut_abs": s["cut_abs_permille"], "cut_rel": s["cut_rel_permille"], + "window": s["cut_window"], "lookahead": s["lookahead_frames"]} + + +def clip_digest(frames): + """SPEC §7, *Digests*: the SHA-256 of the frames' SHA-256 digests concatenated.""" + return hashlib.sha256(b"".join(hashlib.sha256(f).digest() for f in frames)).digest() + + +def verify_pack(rep, d, name, pkg=None, inputs=True): + """packs//: package/ (or `pkg`) verified in §11.3's order, without the source; + then, with `inputs`, the package's inputs: policy.toml as given, and what only the + source decides (the plan, each base's sse and step) and the supplied floor.""" + root = os.path.join(d, "packs", name) + pkg = pkg or os.path.join(root, "package") + tag = f"packs/{name}" + files = tree(pkg) + if files is None: + rep.check(False, f"{tag}: a symbolic link or an entry that is neither file nor directory (unexpected-file)") + return + paths_ok = all(all(segment_ok(s) for s in p.split("/")) for p in files) and len({p.lower() for p in files}) == len(files) + rep.check(paths_ok, f"{tag}: every file, read strictly, has a listing path (SPEC §3.1) (unexpected-file)") + if not paths_ok: + return + if "LISTING" not in files: + rep.check(False, f"{tag}: no LISTING (listing-missing)") + return + others = [p for p in files if p != "LISTING"] + ok = load(os.path.join(pkg, "LISTING")) == listing(pkg, others) + rep.check(ok, f"{tag}: LISTING is the listing of the other files (listing-mismatch)") + if not ok: + return + def get(p): + return load(os.path.join(pkg, p)) if p in files else b"" + raw = get("package.receipt.json") + try: + r = read_receipt(raw) + if r["version"] != 2: + raise Refused("receipt-version") + except Refused as e: + rep.check(False, f"{tag}: package.receipt.json does not read as version 2 ({e.code})") + return + j = r["json"] + ok = raw == jcs(j).encode() + rep.check(ok, f"{tag}: package.receipt.json is RFC 8785 canonical JSON, its receipt's own form (receipt-json)") + if not ok: + return + + # The policy: as given, its effective canonical JSON, its digest (§3.2, §11.1). + toml_b, pj = get("policy.toml"), get("policy.json") + if inputs: + rep.check(toml_b == load(os.path.join(root, "policy.toml")), f"{tag}: policy.toml is the policy as given") + rep.check(blake3(b"wai:pack-policy\x01" + pj).hex() == j["policy_digest"], + f"{tag}: policy.json hashes to the receipt's policy digest (policy-digest-mismatch)") + # policy.toml parsed (§3.2–§3.4); the override applied, policy.json's + # royalty_free_only, which only ever forces it on (§3.2); then §3.6 under that + # effective policy, below. + try: + P = parse_policy_bytes(toml_b) + except Refused as e: + rep.check(False, f"{tag}: policy.toml refused ({e.code})") + return + given = read_json(pj) + if isinstance(given, dict) and given.get("royalty_free_only") is True: + P["royalty_free_only"] = True + ok = jcs(P).encode() == pj + rep.check(ok, f"{tag}: policy.json is policy.toml's effective canonical JSON (policy-json-mismatch)") + if not ok: + return + ids = [c["id"] for c in P["component"]] + [c["id"] for c in P["class"]] + cls = [c["id"].lower() for c in P["class"]] + ok = all(segment_ok(i) for i in ids) and len(set(cls)) == len(cls) and "classic" not in cls + rep.check(ok, f"{tag}: component and class ids are path segments, class ids distinct in any case, none `classic` (pack-id)") + if not ok: + return + # §3.6 under the effective policy: royalty_free_only as policy.json states it. + refused = next((code for code in (rung_refusal(x["capability"], x["role"], P["royalty_free_only"]) for x in P["ladder"]) if code), None) + rep.check(refused is None, f"{tag}: the effective policy asks for nothing a packager may not emit ({refused})") + if refused: + return + # The receipt's components are the policy's, by id, order and media (§11.3). + ok = [(c["id"], c["media_class"]) for c in j["components"]] == [(x["id"], x["media"]) for x in P["component"]] + rep.check(ok, f"{tag}: the receipt's components are the policy's, by id, order and media (policy-component-mismatch)") + if not ok: + return + + # The indexes and the receipt's own refusals (§9.1). + index_files = [] + for c in j["components"]: + present = c["index"] in files + rep.check(present, f"{tag}: {c['index']} present (index-missing)") + if not present: + return + index_files.append(load(os.path.join(pkg, c["index"]))) + try: + ixs = verify_receipt_v2(r, index_files) + rep.check(True, f"{tag}: the receipt verifies against its indexes (§9.1)") + except Refused as e: + rep.check(False, f"{tag}: the receipt against its indexes ({e.code})") + return + seed = bytes.fromhex(load(os.path.join(root, "key.hex")).decode().strip()) + pk, sig = ed25519_from_seed(seed, r["payload"]) + rep.check(pk.hex() == j["signer_pubkey"], f"{tag}: the receipt's signer is the package key") + rep.check(sig.hex() == j["sig"], f"{tag}: the signature is the package key's over the payload (§11.2)") + + named = {"LISTING", "policy.toml", "policy.json", "package.receipt.json"} | {c["index"] for c in j["components"]} + for c, ix in zip(j["components"], ixs): + try: + named |= verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs) + except (Refused, Malformed, ValueError, OverflowError, UnicodeError, KeyError, IndexError) as x: + # A package this verifier cannot read further is refused, never a crash. + rep.check(False, f"{tag} {c['id']}: refused ({type(x).__name__}: {x})") + return + + # §9.4: a metered package publishes its measurement claim; an unmetered one, none. + claim = f"claims/{j['receipt_hash']}.measurement.json" + if j["joules_micro"]: + rep.check(claim in files, f"{tag}: a metered package's measurement claim (measurement-claim-missing)") + rep.check(False, f"{tag}: this verifier does not check a measurement claim (measurement-claim-invalid)") + named.add(claim) + else: + rep.check(not any(p.startswith("claims/") for p in others), + f"{tag}: an unmetered package publishes no measurement claim (§9.4)") + unlisted = [p for p in others if p.startswith("objects/") and p not in named] + rep.check(not unlisted, f"{tag}: object files no entry names: {unlisted} (object-unlisted)") + catalogs = ("hls/", "dash/", "moq/", "stages/") + extra = [p for p in others if p.startswith(catalogs) and p not in named] + rep.check(not extra, f"{tag}: catalogs past the ones the index gives: {extra} (catalog-unexpected)") + other = [p for p in others if p not in named and not p.startswith(("objects/",) + catalogs)] + rep.check(not other, f"{tag}: other files: {other} (unexpected-file)") + + +class Capture(Report): + """A report that keeps what fails, in order, and prints nothing.""" + def __init__(self): + super().__init__() + self.failed = [] + + def check(self, ok, what): + self.checked += 1 + if not ok: + self.failures += 1 + self.failed.append(what) + + +REASON_AT_END = re.compile(r"\(([a-z0-9][a-z0-9.:-]*)\)$") + + +def package_verdict(d, name, pkg): + """The reason §11.3 gives the package in `pkg` (the first check that fails, by the + reason it names), or `ok`; the inputs of packs// are not consulted.""" + cap = Capture() + verify_pack(cap, d, name, pkg=pkg, inputs=False) + if not cap.failed: + return "ok" + m = REASON_AT_END.search(cap.failed[0]) + return m.group(1) if m else f"no reason named: {cap.failed[0]}" + + +def verify_package_mutations(rep, d): + """package-mutations/: each case the golden less the files it removes (and the + directories that leaves with nothing in them), with its overlay and the directories + it adds that hold nothing (`empty_dirs`) or cannot be read (`unreadable_dirs`, mode 0 + where the platform has modes), written out and verified as a package (§11.3); refused + with the corpus's reason, or accepted where it says `ok`.""" + m = os.path.join(d, "package-mutations") + held = tree(m) + if held is None: + rep.check(False, "package-mutations/: a symbolic link or an entry that is neither file nor directory") + return + pm = json.loads(load(os.path.join(m, "expected.json"))) + golden = os.path.join(d, "packs", pm["pack"], "package") + used = {"expected.json"} + with tempfile.TemporaryDirectory() as tmp: + for c in pm["cases"]: + pkg = os.path.join(tmp, c["name"]) + shutil.copytree(golden, pkg) + for p in c["remove"]: + os.remove(os.path.join(pkg, p)) + for at, dirs, files in os.walk(pkg, topdown=False): + if at != pkg and not os.listdir(at): + os.rmdir(at) + for p in held: + if p.startswith(c["name"] + "/"): + used.add(p) + dst = os.path.join(pkg, p[len(c["name"]) + 1:]) + os.makedirs(os.path.dirname(dst), exist_ok=True) + shutil.copyfile(os.path.join(m, p), dst) + for p in c.get("empty_dirs", []) + c.get("unreadable_dirs", []): + os.makedirs(os.path.join(pkg, p)) + for p in c.get("unreadable_dirs", []): + os.chmod(os.path.join(pkg, p), 0) + got = package_verdict(d, pm["pack"], pkg) + for p in c.get("unreadable_dirs", []): + os.chmod(os.path.join(pkg, p), 0o755) + rep.check(got == c["reason"], f"package-mutations {c['name']}: {got}, the corpus says {c['reason']}") + rest = [p for p in held if p not in used] + rep.check(not rest, f"package-mutations/: files no case uses: {rest}") + + +def unreached(files, c): + """The floor and catalog files of a component whose verification stopped at an earlier + refusal: named, so that refusal is not reported again as unexpected files.""" + return {p for p in files if p.startswith((f"floor/{c}/", "hls/", "dash/", "moq/", "stages/"))} + + +def index_against_policy(P, comp, c, ix): + """§11.3 step 6, the index against its component's policy, in its order: its timebase + and media; its string table (the component's id, the class ids, each capability at its + first use); its class records (the policy's classes); each entry's role, capability + and layer a rung's of the component; each piece's kind one a video piece has, and its + rules, moments and class plan §8.1's. None when it holds, else what fails.""" + if ix["tb"] != (comp["timebase"]["num"], comp["timebase"]["den"]) or MEDIA_CLASSES[ix["media_class"]] != comp["media"]: + return "the timebase or media class" + caps = [] + for e in ix["entries"]: + if e["cap"] not in caps: + caps.append(e["cap"]) + if ix["strings"] != [c["id"]] + [x["id"] for x in P["class"]] + caps or ix["strings"][0] != ix["component"]: + return "the string table" + want = [(x["id"], min(x["max_layer"], 255), int(x["floor_only"]), min(x.get("max_kbps", 0), U32)) for x in P["class"]] + if [(x["name"], x["max_layer"], x["flags"], x["max_kbps"]) for x in ix["classes"]] != want: + return "the class records" + lay = layers(P) + for i, e in enumerate(ix["entries"]): + role = ROLES[e["role"]] + if not any(r["component"] == c["id"] and r["role"] == role and r["capability"] == e["cap"] and lay[r["id"]] == e["layer"] + for r in P["ladder"]): + return f"entry {i}: no rung of the component" + if any(pc["kind"] >= len(KINDS) for pc in ix["pieces"]): + return "a piece kind a video piece does not have" + bits, rows = class_plan(P, ix) + if bits != [(pc["rules"], pc["moments"]) for pc in ix["pieces"]]: + return "the rules and moments" + if rows != ix["plan"]: + return "the class plan" + return None + + +def verify_component(rep, tag, root, pkg, files, P, j, c, ix, inputs=True): + """§11.3 per component, then (with `inputs`) the source's side. Returns the files it + names.""" + t = f"{tag} {c['id']}" + comp = next((x for x in P["component"] if x["id"] == c["id"]), None) + if comp is None: + rep.check(False, f"{t}: the receipt's component is not the policy's") + return set() + tb = (comp["timebase"]["num"], comp["timebase"]["den"]) + num, den = tb + pieces, E = ix["pieces"], ix["entries"] + why = index_against_policy(P, comp, c, ix) + rep.check(why is None, f"{t}: the index against the policy: {why} (index-policy-mismatch)") + if why is not None: + return set(unreached(files, c["id"])) | {f"plan-{c['id']}.txt"} | {f"objects/{e['hash'].hex()}.wai" for e in E} + starts = [pc["start"] for pc in pieces] + plan_file = f"plan-{c['id']}.txt" + rep.check(plan_file in files and load(os.path.join(pkg, plan_file)) == "".join(f"{s}\n" for s in starts).encode(), + f"{t}: {plan_file} is the index's plan (plan-mismatch)") + rep.check(ix["plan_digest"].hex() == plan_digest(starts), f"{t}: the index's plan_digest is its starts' (§4.2)") + if inputs: + # Of the golden, not a rule a package is held to: a live window is a package too + # (§6.1, §9.2). + rep.check(ix["first_piece"] == 0 and ix["first_entry"] == 0 and not any(ix["prev"]), + f"{t}: the golden is on-demand content, one window (§6.1)") + named = {plan_file} + + # Every entry's object (§6.3), the decode-equivalent prefixes (§6.5). + objs, mans, decoded = {}, {}, {} + for i, e in enumerate(E): + path = f"objects/{e['hash'].hex()}.wai" + named.add(path) + if path not in files: + rep.check(False, f"{t}: entry {i}: {path} (object-missing)") + continue + objs[i] = load(os.path.join(pkg, path)) + try: + mans[i] = object_rules(ix, i, objs[i]) + rep.check(True, f"{t}: entry {i}: the object rules (§6.3)") + except Refused as x: + rep.check(False, f"{t}: entry {i}: the object rules (§6.3) ({x.code})") + if len(mans) != len(E): + return named | unreached(files, c['id']) + size = None + for i, e in enumerate(E): + if e["role"] != BASE: + continue + ml = e["man_len"] + try: + w = IPV.parse_wiv1(objs[i][12 + ml:], 0) + if e["hash"] not in decoded: + decoded[e["hash"]] = (w, IPV.decode_wiv1_k0(w)) + except IPV.Refused as x: + rep.check(False, f"{t}: entry {i}: the WIV1 payload is refused ({x})") + return named | unreached(files, c['id']) + size = size or (w["w"], w["h"]) + # §5.2: the reference packager's base manifest, as SPEC §3 orders it. + want = {"wai": "1.0", "media": comp["media"], "intent": "replicate", + "model_requirement": {"capability": e["cap"]}, "conditioning": {"kind": e["cap"].split(".")[-1]}, + "target": {"frames": pieces[owners(ix)[i]]["duration"], "h": w["h"], "w": w["w"]}} + rep.check(objs[i][8:8 + ml] == json.dumps(want, separators=(",", ":")).encode(), + f"{t}: entry {i}: the base's manifest is §5.2's") + for i, e in enumerate(E): + if e["tier"] != 2: + continue + if e["role"] != BASE: + rep.check(False, f"{t}: entry {i}: a refinement's prefix, which this verifier does not decode") + continue + rep.check(clip_digest(decoded[e["hash"]][1]) == e["prefix"], + f"{t}: entry {i}: the prefix decodes to its prefix_digest (prefix-digest)") + + # The floor objects (§7.2) and the CMAF files their temporal units give (§7.6). + rung = next((r for r in P["ladder"] if r["component"] == c["id"] and r["role"] == "floor"), None) + sup = P["floor"].get("supplied_listing") + settings = blake3(b"wai:render-settings\x01" + jcs({"source": "supplied", "supplied_listing": sup}).encode()).hex() \ + if sup else None + seq = seq_obu = None + units_of, frags = [], [] + own = owners(ix) + for k, pc in enumerate(pieces): + mine = [i for i in range(len(E)) if own[i] == k] + bases = [i for i in mine if E[i]["role"] == BASE] + fl = [i for i in mine if E[i]["role"] == FLOOR] + rep.check(len(bases) == 1 and rung is not None and len(fl) == 1 and E[fl[0]]["cap"] == rung["capability"], + f"{t}: piece {k}: one base, and one floor object of the floor rung's capability (§5, §7.2) (floor-object-mismatch)") + if len(bases) != 1 or len(fl) != 1: + return named | unreached(files, c['id']) + base, i = bases[0], fl[0] + df = mans[i]["derived_from"] + rep.check(df["motif"] == E[base]["hash"].hex() and df["motif_capability"] == E[base]["cap"], + f"{t}: piece {k}: the floor presents its piece's base (floor-motif-mismatch)") + rep.check(sup is not None and df["renderer"] == f"supplied@{sup}" and df["settings"] == settings, + f"{t}: piece {k}: a supplied floor's renderer and settings (§7.2) (floor-object-mismatch)") + fp = floor_payload(objs[i][12 + E[i]["man_len"]:]) + if fp is None: + rep.check(False, f"{t}: piece {k}: the floor payload is not §7.2's framing (floor-object-mismatch)") + return named | unreached(files, c['id']) + rep.check(len(fp[5]) == pc["duration"], f"{t}: piece {k}: one temporal unit per frame (floor-object-mismatch)") + try: + units = [obus(u) for u in fp[5]] + if any(not o or o[0][0] != OBU_TD or sum(x[0] == OBU_TD for x in o) != 1 or unit_frame(o) is None for o in units): + raise Malformed("a unit that is not one temporal unit holding a frame") + if seq is None: + seq_obu = next((whole for typ, whole, _ in units[0] if typ == OBU_SEQ), None) if units else None + if seq_obu is None: + raise Malformed("the first unit holds no sequence header") + seq = sequence_header(obus(seq_obu)[0][2]) + # §7.3 against the package: every sequence header the first's bytes, a shown key + # frame with one. + for o in units: + if any(typ == OBU_SEQ and whole != seq_obu for typ, whole, _ in o): + raise Malformed("a sequence header other than the first") + if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): + raise Malformed("a shown key frame without a sequence header") + except Malformed as x: + rep.check(False, f"{t}: piece {k}: the floor payload ({x}) (floor-av1-malformed)") + return named | unreached(files, c['id']) + rep.check(fp[:5] == (pc["duration"], seq["max_h"], seq["max_w"], num, den), + f"{t}: piece {k}: the floor payload's n, h, w, fps_num and fps_den (§7.2) (floor-object-mismatch)") + rep.check(mans[i].get("target") == {"frames": pc["duration"], "h": seq["max_h"], "w": seq["max_w"]}, + f"{t}: piece {k}: the floor's target is its piece's frames and its size (§5.2, §7.2) (floor-object-mismatch)") + keys = [shown_key(o, seq) for o in units] + ok = bool(keys) and keys[0] and (P["floor"]["extra_keyframes"] or not any(keys[1:])) + rep.check(ok, f"{t}: piece {k}: it opens on a shown key frame, with no other unless the policy allows (floor-keyframe-mismatch)") + if not ok: + return named | unreached(files, c['id']) + units_of.append(fp[5]) + frag = fragment(ix["first_piece"] + k, pc["start"], den, [(without_delimiters(o), shown_key(o, seq)) for o in units]) + frags.append((hashlib.sha256(frag).hexdigest() + ".m4s", frag)) + init = init_mp4(num, seq["max_w"], seq["max_h"], av1c(seq, seq_obu)) + for p, want in [("init.mp4", init)] + frags: + path = f"floor/{c['id']}/{p}" + named.add(path) + rep.check(path in files and load(os.path.join(pkg, path)) == want, + f"{t}: {path} is the one the floor's temporal units give (floor-cmaf-mismatch)") + + # The catalogs (§8.3–§8.8). + try: + cats = render_catalogs(P, ix, c["id"], (av1_codecs(seq), seq["max_w"], seq["max_h"], frags), size, + j["policy_digest"]) + except (ValueError, OverflowError) as x: + # A time or count past the canonical range (a start past 2^53 − 1): no catalog can + # be written for this index (§11.3). + rep.check(False, f"{t}: no catalog can be written for the index ({x}) (catalog-mismatch)") + return named | unreached(files, c['id']) + for path in sorted(cats): + named.add(path) + rep.check(path in files and load(os.path.join(pkg, path)) == cats[path], + f"{t}: {path} is the one the index, the objects and the floor give (catalog-mismatch)") + if inputs: + verify_sources(rep, t, root, P, comp, ix, decoded, units_of) + return named + + +def verify_sources(rep, t, root, P, comp, ix, decoded, units_of): + """What only the source decides: the pieces (§4.3, §4.6) and their rules, moments and + class plan (§8.1), each base's sse (§6.6) and step (§5.6), and the supplied floor (§7.3).""" + num, den = comp["timebase"]["num"], comp["timebase"]["den"] + source = comp.get("source", "") + name = source[:-len(".y4m")] + if not source.endswith(".y4m") or fixture_spans(name) is None: + rep.check(False, f"{t}: the source {source!r} is no fixture this verifier generates (pack-source-missing)") + return + w, h, rate, _, frames = read_y4m(fixture(name)) + rep.check(rate == (num, den), f"{t}: the source's frame rate is its timebase (pack-source-rate)") + pieces, _ = segment(frames, w, h, seg_params(P, (num, den))) + got = [(pc["start"], pc["len"], pc["flags"], KINDS.index(classify(pc, P["classify"])[0])) for pc in pieces] + rep.check(got == [(pc["start"], pc["duration"], pc["flags"], pc["kind"]) for pc in ix["pieces"]], + f"{t}: every piece's start, duration, flags and kind are the segmenter's (§4.3, §4.6)") + bits, rows = class_plan(P, ix) + rep.check(bits == [(pc["rules"], pc["moments"]) for pc in ix["pieces"]], + f"{t}: every piece's rules and moments (§8.1)") + rep.check(rows == ix["plan"], f"{t}: the class plan is §8.1's") + rung = next(r for r in P["ladder"] if r["component"] == comp["id"] and r["role"] == "base") + p = rung["params"] + own, cache, chosen = owners(ix), {}, set() + for i, e in enumerate(ix["entries"]): + if e["role"] != BASE: + continue + pc = ix["pieces"][own[i]] + src = frames[pc["start"]:pc["start"] + pc["duration"]] + wv, dec = decoded[e["hash"]] + sse = sum((a - b) ** 2 for s, f in zip(src, dec) for a, b in zip(s, f)) + rep.check(len(dec) == len(src) and sse == e["sse"], f"{t}: entry {i}: sse against the source (§6.6)") + n = len(src) + rep.check((wv["block"], wv["mv_precision"]) == ((p["block"], p["mv_precision"]) if n > 1 else (1, 1)), + f"{t}: entry {i}: the payload's block and precision are the rung's (§5.5)") + key = hashlib.sha256(b"".join(src)).digest() + + def sse_of(q, key=key, src=src): + if (key, q) not in cache: + cache[key, q] = closed_loop(src, h, w, p["block"], p["search"], p["mv_precision"], q) + return sse_px_milli(cache[key, q][1], h, w, len(src)) + q = choose_q(sse_of, p["q_candidates"], p["max_sse_px_milli"]) + rep.check(wv["q"] == q, f"{t}: entry {i}: the step is §5.6's choice ({wv['q']}, the rule {q})") + rep.check([bytes(f) for f in cache[key, q][0]] == dec, + f"{t}: entry {i}: the payload decodes to the encoder's reconstruction at that step (§5.5)") + if (e["hash"], key) not in chosen: + chosen.add((e["hash"], key)) + bad = encoder_choices(wv, p, src, dec) + rep.check(not bad, f"{t}: entry {i}: every vector, coded residual and the table are the encoder's (§5.5)" + + (f": {bad[0]}" if bad else "")) + + # The supplied floor (§7.3): its listing, its temporal units and its key frames. + sup = os.path.join(root, "floor") + names = tree(sup) + rep.check(hashlib.sha256(listing(sup, names)).hexdigest() == P["floor"].get("supplied_listing"), + f"{t}: the supplied floor's listing hashes to floor.supplied_listing (floor-listing-mismatch)") + stream = f"{comp['id']}.obu" + if stream not in names: + rep.check(False, f"{t}: no {stream} in the supplied floor (floor-stream-missing)") + return + try: + units = temporal_units(load(os.path.join(sup, stream))) + if not units or not any(typ == OBU_SEQ for typ, _, _ in units[0][1]): + raise Malformed("the first unit holds no sequence header") + first = next(whole for typ, whole, _ in units[0][1] if typ == OBU_SEQ) + seq = sequence_header(obus(first)[0][2]) + for u, o in units: + if unit_frame(o) is None: + raise Malformed("a unit holding no frame") + if any(typ == OBU_SEQ and whole != first for typ, whole, _ in o): + raise Malformed("a sequence header other than the first") + if shown_key(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): + raise Malformed("a shown key frame without a sequence header") + rep.check(True, f"{t}: the supplied stream splits into temporal units, each a frame (floor-av1-malformed)") + except Malformed as x: + rep.check(False, f"{t}: the supplied stream ({x}) (floor-av1-malformed)") + return + rep.check(len(units) == len(frames), f"{t}: one temporal unit per source frame (floor-frame-count)") + keys = [f for f, (_, o) in enumerate(units) if shown_key(o, seq)] + starts = [pc["start"] for pc in ix["pieces"]] + ok = set(starts) <= set(keys) and (P["floor"]["extra_keyframes"] or keys == starts) + rep.check(ok, f"{t}: shown key frames at the piece starts {starts}, and only there: {keys} (floor-keyframe-mismatch)") + rep.check([[u for u, _ in units[pc["start"]:pc["start"] + pc["duration"]]] for pc in ix["pieces"]] == units_of, + f"{t}: each floor object holds its piece's temporal units of the supplied stream (§7.2)") + + +def mutation_verdict(m, c, used): + """A case of mutations/: the index's verdict (§6.2, §6.3), then each entry's object's + (§6.3), or the receipt's (§9.1).""" + def get(p): + used.add(p) + return load(os.path.join(m, p)) + try: + if c["kind"] in ("index", "objects"): + ix = parse_index(get(c["index"])) + index_rules(ix) + if c["kind"] == "objects": + for i, e in enumerate(ix["entries"]): + p = os.path.join(c["objects"], e["hash"].hex() + ".wai") + if not os.path.isfile(os.path.join(m, p)): + return "object-missing" + object_rules(ix, i, get(p)) + return "ok" + r = read_receipt(get(c["receipt"])) + verify_receipt_v2(r, [get(p) for p in c["indexes"]]) + return "ok" + except Refused as e: + return e.code + + +def verify_v1_receipt(rep, name, b, want_hash): + """§9.3: a version-1 receipt read, its payload rebuilt, its hash, counts, segment map + root and signature.""" + try: + r = read_receipt(b) + except Refused as e: + rep.check(False, f"{name}: refused ({e.code})") + return + j = r["json"] + rep.check(r["version"] == 1, f"{name}: version 1") + rep.check(j["receipt_hash"] == want_hash, f"{name}: receipt_hash is the corpus's") + rep.check(blake3(RECEIPT_ID + r["payload"] + bytes.fromhex(j["sig"])).hex() == want_hash, + f"{name}: receipt_hash is BLAKE3 over the payload and signature") + segs = j["segments"] + rep.check(merkle_root([v1_leaf(s) for s in segs]).hex() == j["segment_map_root"], + f"{name}: segment_map_root is the Merkle root of the segment leaves") + first = {} + for s in segs: + first.setdefault(s["content_hash"], s["byte_len"]) + rep.check([s["index"] for s in segs] == list(range(len(segs))), f"{name}: segment indices 0..n-1") + rep.check((j["total_segments"], j["unique_segments"], j["total_bytes"], j["stored_bytes"], j["work_segment_ops"]) + == (len(segs), len(first), sum(s["byte_len"] for s in segs), sum(first.values()), len(segs)), + f"{name}: the segment counts") + rep.check(ed25519_verifies(bytes.fromhex(j["signer_pubkey"]), bytes.fromhex(j["sig"]), r["payload"]), + f"{name}: the signature verifies") + + def verify(d): rep = Report() pol = json.loads(load(os.path.join(d, "policy", "expected.json"))) for c in pol["cases"]: P = parse_policy_bytes(load(os.path.join(d, "policy", c["file"]))) - if c.get("overrides", {}).get("royalty_free_only") is not None: - P["royalty_free_only"] = c["overrides"]["royalty_free_only"] + if c.get("overrides", {}).get("royalty_free_only") is True: + P["royalty_free_only"] = True rep.check(jcs(P) == c["canonical"], f"policy/{c['file']}: canonical JSON") rep.check(digest(P) == c["digest"], f"policy/{c['file']}: digest") names = sorted(n for n in os.listdir(os.path.join(d, "policy")) if n.endswith(".toml")) @@ -1231,6 +3052,38 @@ def verify(d): rep.check(got == pc["candidates"], f"qchoice {pc['fixture']} piece {pc['piece']}: each candidate's SSE") rep.check(choose_q(lambda q: sse_of(q)[1], p["q_candidates"], p["max_sse_px_milli"]) == pc["q"], f"qchoice {pc['fixture']} piece {pc['piece']} bound {p['max_sse_px_milli']}: the step chosen") + + for name in sorted(os.listdir(os.path.join(d, "packs"))): + verify_pack(rep, d, name) + + m = os.path.join(d, "mutations") + used = {"expected.json"} + for c in json.loads(load(os.path.join(m, "expected.json")))["cases"]: + got = mutation_verdict(m, c, used) + rep.check(got == c["reason"], f"mutations {c['name']}: {got}, the corpus says {c['reason']}") + rest = [p for p in tree(m) if p not in used] + rep.check(not rest, f"mutations/: files no case uses: {rest}") + + rc = json.loads(load(os.path.join(d, "receipts", "expected.json"))) + for c in rc["cases"]: + verify_v1_receipt(rep, f"receipts/{c['file']}", load(os.path.join(d, "receipts", c["file"])), c["receipt_hash"]) + rep.check(tree(os.path.join(d, "receipts")) == sorted([c["file"] for c in rc["cases"]] + ["expected.json"]), + "receipts/: every receipt is a case") + + # catalogs/: MoQ catalogs of indexes with refinements on more than one deadline (§8.4). + cc = json.loads(load(os.path.join(d, "catalogs", "expected.json"))) + for c in cc["cases"]: + ix = parse_index(load(os.path.join(d, c["index"]))) + index_rules(ix) + f = c["floor"] + got = render_moq(ix["component"], ix["entries"], c["layer_lag_ms"], c["default_deadline"], (f["codec"], f["w"], f["h"])) + rep.check(got == c["moq"].encode(), f"{c['index']}: the MoQ catalog §8.4 gives") + names = {t["name"] for t in json.loads(got)["tracks"]} + rep.check(all(x in names for t in json.loads(got)["tracks"] for x in t.get("depends", [])), + f"{c['index']}: no dependency names a track the catalog does not hold") + rep.check(tree(os.path.join(d, "catalogs")) == sorted([c["index"][len("catalogs/"):] for c in cc["cases"]] + ["expected.json"]), + "catalogs/: every index is a case") + verify_package_mutations(rep, d) return rep diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING new file mode 100644 index 000000000..eecdbdac7 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING @@ -0,0 +1,34 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +13b7a7cf473134c1a9dfb0a380c4464bcd5e3a7f2e007efe65f32fd9422175c0 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +f7c00e64b3ba869ad37e3619325d319ee016df3cc294b674713613bc0c1dc087 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +d57f898e6963d4c23a6dc68d965a8b5cbbc103c4de7a28cf21c1f76c67d1ccde policy.json +7f13bffabbf78b92ef693c5c652e2ab3e391e6adbe0f16a4c0eaf2d77ade1ec7 policy.toml +755a1ba06a6b1e5ea0b6169208003deb71ed4d354e4d1abfed21bbe3c931ede3 stages/Classic.json +19406636eb9381287af74850d62f5b1dcaea9e8ee4a15e721b6fb6f0c77aa75d stages/mobile.json +50a07904652a6807db44c6f628fdcb5e7f0851753527301167c18f5e7536a41a stages/tv.json diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/index-v.wsix b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/index-v.wsix new file mode 100644 index 000000000..c90160d32 Binary files /dev/null and b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/package.receipt.json b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/package.receipt.json new file mode 100644 index 000000000..51a168cd9 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"0fdaa41306fde107a6e19d5a360ce4cb8cc638a3ce15dba75abaa2d1d94c796b","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"3f597d88fc83ef8f93b0a07623642ff01460cf09af22d61339373bbe1cb882ee","receipt_hash":"2d4df45396b6fc0f60b3f323934c327118fc1cc2af178cc94692de12b59c3b89","sig":"f09843f6cd58550809ecdbda30450bcf52996ce9cc637996ccb1ed28d4482c4aba668d2d722b148d4517e1231cde8c3a6816c816c9c25bc3cec6a7941c439a0a","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.json b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.json new file mode 100644 index 000000000..c422463ea --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"Classic","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.toml b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.toml new file mode 100644 index 000000000..752ebaa76 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.toml @@ -0,0 +1,47 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "Classic" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/Classic.json b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/Classic.json new file mode 100644 index 000000000..097876c72 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/Classic.json @@ -0,0 +1 @@ +{"class":"Classic","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"3f597d88fc83ef8f93b0a07623642ff01460cf09af22d61339373bbe1cb882ee"} diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/mobile.json b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/mobile.json new file mode 100644 index 000000000..96833649f --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"3f597d88fc83ef8f93b0a07623642ff01460cf09af22d61339373bbe1cb882ee"} diff --git a/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/tv.json b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/tv.json new file mode 100644 index 000000000..c9b6eec7d --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"3f597d88fc83ef8f93b0a07623642ff01460cf09af22d61339373bbe1cb882ee"} diff --git a/wai/pack-conformance/package-mutations/class-named-classic/LISTING b/wai/pack-conformance/package-mutations/class-named-classic/LISTING new file mode 100644 index 000000000..6265087ff --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/LISTING @@ -0,0 +1,34 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/classic/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +44692fd01ae15a4d7edaa1813241e55e8b2367180dc700b0c780d2d4014a4f95 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +c248bdd322d9cb6cf15e104002f324a0c42b8a823067d7592922259c31a16128 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +ca399bc8839a9f7d89409338489886f98b0cf8e9d2f21c21eedb208e4b5fa82f policy.json +8469efd694ddf0de476dc32f2106d016855a4076a98a609814e34de72c820807 policy.toml +bd137a26dabe325292d1984e80c2a7abf6e5cb84958d829b1082aec801aca4f2 stages/classic.json +4a264050dfbf4118dad95d9fa05e644db2160a047a95dc577fc4bb8a12ed2cea stages/mobile.json +698b0085ae2913bfcd2e66cf71d24fbd03c54c2374de8b0176479709d1cc3439 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/class-named-classic/hls/classic/master.m3u8 b/wai/pack-conformance/package-mutations/class-named-classic/hls/classic/master.m3u8 new file mode 100644 index 000000000..5495adf2c --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/class-named-classic/index-v.wsix b/wai/pack-conformance/package-mutations/class-named-classic/index-v.wsix new file mode 100644 index 000000000..0092ad472 Binary files /dev/null and b/wai/pack-conformance/package-mutations/class-named-classic/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/class-named-classic/package.receipt.json b/wai/pack-conformance/package-mutations/class-named-classic/package.receipt.json new file mode 100644 index 000000000..4d5931cbb --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"e988ea17bdbd17dfdcfda9c4de206f466f1fcbd38a5420247ab8569b78804743","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"8d83fd4c9ece4d9ef9fb4f05799234fd2c0c0bbc85d0b4887fd8845cb7cc53d1","receipt_hash":"e836878fc3aa7d62d5f1e7513b703c9876b199a601f92ee012ed83a3ef9c2b50","sig":"11a431dc0c0097b3034b6c24521b2866af7648fe35e695597be51d5880cf99e241beeebd100b7636009cc77a259ada8e9c03d0761c1b34d4006c1e6d6932c30b","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/class-named-classic/policy.json b/wai/pack-conformance/package-mutations/class-named-classic/policy.json new file mode 100644 index 000000000..77890ddc7 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"classic","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/class-named-classic/policy.toml b/wai/pack-conformance/package-mutations/class-named-classic/policy.toml new file mode 100644 index 000000000..55f2c3664 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/policy.toml @@ -0,0 +1,47 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "classic" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/class-named-classic/stages/classic.json b/wai/pack-conformance/package-mutations/class-named-classic/stages/classic.json new file mode 100644 index 000000000..bdb0a2f32 --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/stages/classic.json @@ -0,0 +1 @@ +{"class":"classic","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8d83fd4c9ece4d9ef9fb4f05799234fd2c0c0bbc85d0b4887fd8845cb7cc53d1"} diff --git a/wai/pack-conformance/package-mutations/class-named-classic/stages/mobile.json b/wai/pack-conformance/package-mutations/class-named-classic/stages/mobile.json new file mode 100644 index 000000000..094dbc82f --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8d83fd4c9ece4d9ef9fb4f05799234fd2c0c0bbc85d0b4887fd8845cb7cc53d1"} diff --git a/wai/pack-conformance/package-mutations/class-named-classic/stages/tv.json b/wai/pack-conformance/package-mutations/class-named-classic/stages/tv.json new file mode 100644 index 000000000..fe75850de --- /dev/null +++ b/wai/pack-conformance/package-mutations/class-named-classic/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"8d83fd4c9ece4d9ef9fb4f05799234fd2c0c0bbc85d0b4887fd8845cb7cc53d1"} diff --git a/wai/pack-conformance/package-mutations/component-medium/LISTING b/wai/pack-conformance/package-mutations/component-medium/LISTING new file mode 100644 index 000000000..83cc051c0 --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +1cfc2a0d1b54f1a6ec47ebec95de9bc1cba3a9df40dbc3868d684ffa1f33db8d index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +525299655740d52443a7dc6fe458b919d6dccff5839f558af9613ba033126f4f package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +3b3d12c696539b1fe8099ec0dc611b9d0c3c8a1451c874c1db32d5a1b395e29d policy.json +5c346838b58b5ec1f2b63df839cfb8c2f614ca6f793195313048db69a313e27d policy.toml +0883e453d1e03e220182e479b88e0a8b89e9db77e1c51ed913cb13644a19b6d6 stages/legacy.json +45076638940f5e4c9990a2c9e49654ab574567ca71df2fbecc3fe967659ea022 stages/mobile.json +5d4a3354c58daf39fdb64b24f2b6400ac135ccaf1e2c6a2e480675a828843cd5 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/component-medium/index-v.wsix b/wai/pack-conformance/package-mutations/component-medium/index-v.wsix new file mode 100644 index 000000000..a066f62ca Binary files /dev/null and b/wai/pack-conformance/package-mutations/component-medium/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/component-medium/package.receipt.json b/wai/pack-conformance/package-mutations/component-medium/package.receipt.json new file mode 100644 index 000000000..f48c60962 --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5b5a5b592e7429474d5d05be9ec587e2beaffb14c8573cae34d8876531515e52","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"c87330dff332bf86f7c5abc8f4455c11de2bc7adddaff0e124fad96c77cf7c28","receipt_hash":"730f4249fbc5bb567869ab40b5ffe9ff5fd2a867771cb815b4bfce1dc0007079","sig":"874590c9e961dae68fd9294688c2146773abdd74da398eb349810b6fecee97e73cb121276fd2bdb3200f54f2c97de93631c50a557df2b41f7470da547d9e730c","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/component-medium/policy.json b/wai/pack-conformance/package-mutations/component-medium/policy.json new file mode 100644 index 000000000..2de6f2a11 --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"image","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/component-medium/policy.toml b/wai/pack-conformance/package-mutations/component-medium/policy.toml new file mode 100644 index 000000000..1a165ed59 --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/policy.toml @@ -0,0 +1,47 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "image" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/component-medium/stages/legacy.json b/wai/pack-conformance/package-mutations/component-medium/stages/legacy.json new file mode 100644 index 000000000..6aa30ad6a --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"c87330dff332bf86f7c5abc8f4455c11de2bc7adddaff0e124fad96c77cf7c28"} diff --git a/wai/pack-conformance/package-mutations/component-medium/stages/mobile.json b/wai/pack-conformance/package-mutations/component-medium/stages/mobile.json new file mode 100644 index 000000000..ebfa9a4c2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"c87330dff332bf86f7c5abc8f4455c11de2bc7adddaff0e124fad96c77cf7c28"} diff --git a/wai/pack-conformance/package-mutations/component-medium/stages/tv.json b/wai/pack-conformance/package-mutations/component-medium/stages/tv.json new file mode 100644 index 000000000..ab61498df --- /dev/null +++ b/wai/pack-conformance/package-mutations/component-medium/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"c87330dff332bf86f7c5abc8f4455c11de2bc7adddaff0e124fad96c77cf7c28"} diff --git a/wai/pack-conformance/package-mutations/dot-directory/.cache/blob b/wai/pack-conformance/package-mutations/dot-directory/.cache/blob new file mode 100644 index 000000000..67aed78ea --- /dev/null +++ b/wai/pack-conformance/package-mutations/dot-directory/.cache/blob @@ -0,0 +1 @@ +anything \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/dot-file/.hidden b/wai/pack-conformance/package-mutations/dot-file/.hidden new file mode 100644 index 000000000..67aed78ea --- /dev/null +++ b/wai/pack-conformance/package-mutations/dot-file/.hidden @@ -0,0 +1 @@ +anything \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/dot-object/objects/.x.wai b/wai/pack-conformance/package-mutations/dot-object/objects/.x.wai new file mode 100644 index 000000000..67aed78ea --- /dev/null +++ b/wai/pack-conformance/package-mutations/dot-object/objects/.x.wai @@ -0,0 +1 @@ +anything \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/expected.json b/wai/pack-conformance/package-mutations/expected.json new file mode 100644 index 000000000..e1842869e --- /dev/null +++ b/wai/pack-conformance/package-mutations/expected.json @@ -0,0 +1,310 @@ +{ + "pack": "video-base-01", + "cases": [ + { + "name": "no-component", + "reason": "policy-component-mismatch", + "remove": [ + "dash/classic.mpd", + "dash/mobile.mpd", + "dash/tv.mpd", + "floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s", + "floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s", + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s", + "floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s", + "floor/v/init.mp4", + "hls/classic/floor-v.m3u8", + "hls/classic/master.m3u8", + "hls/legacy/master.m3u8", + "hls/mobile/master.m3u8", + "hls/mobile/wai-v.m3u8", + "hls/mobile/wai.m3u8", + "hls/tv/master.m3u8", + "hls/tv/wai-v.m3u8", + "hls/tv/wai.m3u8", + "index-v.wsix", + "moq/catalog.json", + "objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai", + "objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai", + "objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai", + "objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai", + "objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai", + "objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai", + "objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai", + "plan-v.txt", + "stages/legacy.json", + "stages/mobile.json", + "stages/tv.json" + ] + }, + { + "name": "no-component-live", + "reason": "policy-component-mismatch", + "remove": [ + "dash/classic.mpd", + "dash/mobile.mpd", + "dash/tv.mpd", + "floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s", + "floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s", + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s", + "floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s", + "floor/v/init.mp4", + "hls/classic/floor-v.m3u8", + "hls/classic/master.m3u8", + "hls/legacy/master.m3u8", + "hls/mobile/master.m3u8", + "hls/mobile/wai-v.m3u8", + "hls/mobile/wai.m3u8", + "hls/tv/master.m3u8", + "hls/tv/wai-v.m3u8", + "hls/tv/wai.m3u8", + "index-v.wsix", + "moq/catalog.json", + "objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai", + "objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai", + "objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai", + "objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai", + "objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai", + "objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai", + "objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai", + "plan-v.txt", + "stages/legacy.json", + "stages/mobile.json", + "stages/tv.json" + ] + }, + { + "name": "index-class-records", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "index-class-rate", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "index-capability", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "index-string-order", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "index-class-plan", + "reason": "index-policy-mismatch", + "remove": [] + }, + { + "name": "class-named-classic", + "reason": "pack-id", + "remove": [ + "hls/legacy/master.m3u8", + "stages/legacy.json" + ] + }, + { + "name": "class-named-classic-in-capitals", + "reason": "pack-id", + "remove": [ + "hls/legacy/master.m3u8", + "stages/legacy.json" + ] + }, + { + "name": "patent-pool-floor", + "reason": "license-refused:wai.video.avc:patent-pool", + "remove": [] + }, + { + "name": "patent-pool-floor-by-override", + "reason": "license-refused:wai.video.avc:patent-pool", + "remove": [] + }, + { + "name": "policy-json-relaxes-royalty-free", + "reason": "policy-json-mismatch", + "remove": [ + "objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai", + "objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai", + "objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "component-medium", + "reason": "policy-component-mismatch", + "remove": [] + }, + { + "name": "floor-object-missing", + "reason": "object-missing", + "remove": [ + "objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai" + ] + }, + { + "name": "floor-object-corrupt", + "reason": "object-hash", + "remove": [] + }, + { + "name": "live-window-past-2-53", + "reason": "catalog-mismatch", + "remove": [ + "dash/classic.mpd", + "dash/mobile.mpd", + "dash/tv.mpd", + "floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s", + "floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s", + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s", + "floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s", + "hls/classic/floor-v.m3u8", + "hls/classic/master.m3u8", + "hls/legacy/master.m3u8", + "hls/mobile/master.m3u8", + "hls/mobile/wai-v.m3u8", + "hls/mobile/wai.m3u8", + "hls/tv/master.m3u8", + "hls/tv/wai-v.m3u8", + "hls/tv/wai.m3u8", + "moq/catalog.json", + "stages/legacy.json", + "stages/mobile.json", + "stages/tv.json" + ] + }, + { + "name": "floor-opens-on-inter", + "reason": "floor-keyframe-mismatch", + "remove": [ + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-extra-key-frame", + "reason": "floor-keyframe-mismatch", + "remove": [ + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-extra-key-frame-allowed", + "reason": "ok", + "remove": [ + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-sequence-header", + "reason": "floor-av1-malformed", + "remove": [ + "floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s", + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-framing-header", + "reason": "floor-object-mismatch", + "remove": [ + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-renderer-settings", + "reason": "floor-object-mismatch", + "remove": [ + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-target", + "reason": "floor-object-mismatch", + "remove": [ + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-motif", + "reason": "floor-motif-mismatch", + "remove": [ + "objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai" + ] + }, + { + "name": "floor-cmaf", + "reason": "floor-cmaf-mismatch", + "remove": [] + }, + { + "name": "dot-file", + "reason": "unexpected-file", + "remove": [] + }, + { + "name": "dot-directory", + "reason": "unexpected-file", + "remove": [] + }, + { + "name": "dot-object", + "reason": "unexpected-file", + "remove": [] + }, + { + "name": "sha256-file", + "reason": "unexpected-file", + "remove": [] + }, + { + "name": "receipt-pretty", + "reason": "receipt-json", + "remove": [] + }, + { + "name": "receipt-newline", + "reason": "receipt-json", + "remove": [] + }, + { + "name": "receipt-duplicate-member", + "reason": "receipt-json", + "remove": [] + }, + { + "name": "empty-directory", + "reason": "unexpected-file", + "remove": [], + "empty_dirs": [ + "hls/empty" + ] + }, + { + "name": "empty-dot-directory", + "reason": "unexpected-file", + "remove": [], + "empty_dirs": [ + ".git/refs" + ] + }, + { + "name": "unreadable-directory", + "reason": "unexpected-file", + "remove": [], + "unreadable_dirs": [ + "objects/locked" + ] + } + ] +} diff --git a/wai/pack-conformance/package-mutations/floor-cmaf/LISTING b/wai/pack-conformance/package-mutations/floor-cmaf/LISTING new file mode 100644 index 000000000..a1cf58bea --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-cmaf/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +1006595531d62d47577c530d97cf2d1bb70bc3929b03cc56ed28b696ef19c1de floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-cmaf/floor/v/init.mp4 b/wai/pack-conformance/package-mutations/floor-cmaf/floor/v/init.mp4 new file mode 100644 index 000000000..7f51f9b0c Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-cmaf/floor/v/init.mp4 differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING new file mode 100644 index 000000000..202ed4fa2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING @@ -0,0 +1,35 @@ +46f912e567259b494c5fbfe95a8918b9bf033371a2cd091fa39417141c18537f dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +9fc20344d9ea0374ba8e279550b9417e27b78ce74efb608389083afd21404b95 hls/classic/floor-v.m3u8 +be828879b46523f0f9ae0a2fc5f313db01bacfcfc193bdab58a816e73abca5d2 hls/classic/master.m3u8 +3d40bd5551e9ab33121e6c67e6b5b910584acae002963b42f643f52f493c5137 hls/legacy/master.m3u8 +9cdc245c6f4236246a62d40814037641ed6e154187e12ebd48bc4e23fb5a0f04 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +9cdc245c6f4236246a62d40814037641ed6e154187e12ebd48bc4e23fb5a0f04 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +b1ebb904e14be6aa830e400b4d732bc46ac47986f5b20202be0a261ace2369f0 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +08fecb5c36522a2b9cd51f169395dee95b2cba71af391ea20da9a4d0e44b059b objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +372f6296559f9b41ba1f0e85e3b2a25d4a5ce907db1835d0e1e0f7741173e65c package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +146ed2964738e1ed36dcb71ed4602e5e667ba7d2a4a6d4c454d7524e70743597 policy.json +2d39de550a81413e0861cd1bcc2a9f644e1a69dd0d23248b08d092dcb10ed436 policy.toml +12de1cef7e3da675237ea470999adeb1a99fec4536564170517dcbd7d51a49b6 stages/legacy.json +38ac4decfc59e1679a927cc1cd25b22fcd056646d648676504a7b0c5f86d48a9 stages/mobile.json +53735243c45c44150537cca0d7d8fc9b0b485ede0237bd60db1cd225c5ad406b stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/dash/classic.mpd b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/dash/classic.mpd new file mode 100644 index 000000000..5eb61784c --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s new file mode 100644 index 000000000..4d172a018 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/floor-v.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..ebf831e78 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.400, +../../floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s +#EXTINF:1.000, +../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/master.m3u8 new file mode 100644 index 000000000..ee7fac241 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/legacy/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/legacy/master.m3u8 new file mode 100644 index 000000000..925b3fc6d --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/mobile/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/mobile/master.m3u8 new file mode 100644 index 000000000..073e87187 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/mobile/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/tv/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/tv/master.m3u8 new file mode 100644 index 000000000..073e87187 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/tv/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/index-v.wsix b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/index-v.wsix new file mode 100644 index 000000000..a5aa0f86d Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai new file mode 100644 index 000000000..c194a74db Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/package.receipt.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/package.receipt.json new file mode 100644 index 000000000..6fae2f026 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"7edf4cd2035939ef679f916cfed20a2cc6b9adf869d28167b8a324f661e9a6e1","media_class":"video","stage_root":"06cb0ab45b37b2861764872ab1206f72ebdde5ed18f52fd0a98f919c2f31c32e"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"d05c7716108a3c7bab5b77e0f16bde995e59f7aa90a59b299123d3399fecb153","receipt_hash":"519bff67e7a8c21375423d1b407f18308050a126d21b43cae548ba9f9c097fd6","sig":"60bc9fdf0eb48818dafa2d9aa2e5977a8f395800f9239dc0269b64db070bf59f1d7a130c3dd582285344e6399561ad9e3be3923c6b918be33e85bfe0464ef704","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":120308,"total_bytes":131492,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.json new file mode 100644 index 000000000..286bb944e --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":true,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.toml b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.toml new file mode 100644 index 000000000..80d2546de --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.toml @@ -0,0 +1,48 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +extra_keyframes = true +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/legacy.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/legacy.json new file mode 100644 index 000000000..1d0f00cc4 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"d05c7716108a3c7bab5b77e0f16bde995e59f7aa90a59b299123d3399fecb153"} diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/mobile.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/mobile.json new file mode 100644 index 000000000..891ba7dac --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"d05c7716108a3c7bab5b77e0f16bde995e59f7aa90a59b299123d3399fecb153"} diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/tv.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/tv.json new file mode 100644 index 000000000..83563e003 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"d05c7716108a3c7bab5b77e0f16bde995e59f7aa90a59b299123d3399fecb153"} diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING b/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING new file mode 100644 index 000000000..1a3af594f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING @@ -0,0 +1,35 @@ +46f912e567259b494c5fbfe95a8918b9bf033371a2cd091fa39417141c18537f dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +9fc20344d9ea0374ba8e279550b9417e27b78ce74efb608389083afd21404b95 hls/classic/floor-v.m3u8 +be828879b46523f0f9ae0a2fc5f313db01bacfcfc193bdab58a816e73abca5d2 hls/classic/master.m3u8 +3d40bd5551e9ab33121e6c67e6b5b910584acae002963b42f643f52f493c5137 hls/legacy/master.m3u8 +9cdc245c6f4236246a62d40814037641ed6e154187e12ebd48bc4e23fb5a0f04 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +9cdc245c6f4236246a62d40814037641ed6e154187e12ebd48bc4e23fb5a0f04 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +216c7a0baa5505fb92e3fe5cd2101962f220c6b188fe4ec3a5f655703373738a index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +08fecb5c36522a2b9cd51f169395dee95b2cba71af391ea20da9a4d0e44b059b objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +ef01aaca4190e762b90c3d7c9368fc8ff5d379edab1555158eb5c4906bc8ef3d package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/dash/classic.mpd b/wai/pack-conformance/package-mutations/floor-extra-key-frame/dash/classic.mpd new file mode 100644 index 000000000..5eb61784c --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s b/wai/pack-conformance/package-mutations/floor-extra-key-frame/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s new file mode 100644 index 000000000..4d172a018 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/floor-v.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..ebf831e78 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.400, +../../floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s +#EXTINF:1.000, +../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/master.m3u8 new file mode 100644 index 000000000..ee7fac241 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/legacy/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/legacy/master.m3u8 new file mode 100644 index 000000000..925b3fc6d --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/mobile/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/mobile/master.m3u8 new file mode 100644 index 000000000..073e87187 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/mobile/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/tv/master.m3u8 b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/tv/master.m3u8 new file mode 100644 index 000000000..073e87187 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/tv/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=11716,AVERAGE-BANDWIDTH=9555,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/index-v.wsix b/wai/pack-conformance/package-mutations/floor-extra-key-frame/index-v.wsix new file mode 100644 index 000000000..72565ab50 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai b/wai/pack-conformance/package-mutations/floor-extra-key-frame/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai new file mode 100644 index 000000000..c194a74db Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-extra-key-frame/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-extra-key-frame/package.receipt.json b/wai/pack-conformance/package-mutations/floor-extra-key-frame/package.receipt.json new file mode 100644 index 000000000..85c57eee9 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-extra-key-frame/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"d87e0de5dd2ef30a9eef3f079664e01060ddf243b7b4a8c2ea32b6bec6b6c0c7","media_class":"video","stage_root":"06cb0ab45b37b2861764872ab1206f72ebdde5ed18f52fd0a98f919c2f31c32e"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"8efb72fa4112d9497cb0f523ee0b32ac2e5664e986cb6548ad93361edac5d745","sig":"33a05cc7492631032ffb5149ce92cf923b36acd23bebf37b9b735e3d5ca4e1b6f942a0075cc5be7667f0c50bc837203ad2588f3fc89adc5a2560cb610b6d1e03","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":120308,"total_bytes":131492,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-framing-header/LISTING b/wai/pack-conformance/package-mutations/floor-framing-header/LISTING new file mode 100644 index 000000000..c330bbb81 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-framing-header/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +e1ff9d513a5576dd32236a21b4ad00d1e97b3d9247c8a0b05f44ff2ee3080594 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +4dacc52e68b4fd119cb6da1387961aeabaa7dda4ed75ea9df881e92bfb2552d3 objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +be3bc1f624e41d30b08f35740a212f75b395489833e75283299fdc565f8fe7c7 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-framing-header/index-v.wsix b/wai/pack-conformance/package-mutations/floor-framing-header/index-v.wsix new file mode 100644 index 000000000..4802a6aec Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-framing-header/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-framing-header/objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai b/wai/pack-conformance/package-mutations/floor-framing-header/objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai new file mode 100644 index 000000000..0ab0a5c2e Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-framing-header/objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-framing-header/package.receipt.json b/wai/pack-conformance/package-mutations/floor-framing-header/package.receipt.json new file mode 100644 index 000000000..78e419609 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-framing-header/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"51c1c2702e8156068c33a00b7a873e1f37e4b621f3b9da96cba3b4f81d733205","media_class":"video","stage_root":"725b6eb6c89fcc253be2d27673327308649816b71489bd935d0cc510cffbe051"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"54b85b2e7855f9ca67b66a3b104cb525e51df4fe18db37fc93e50b08913ab50d","sig":"ea191ac9e55e2f57eba6e127c92a0d0dd5583815757bed4223b8307c416c9c7dc695cb70b05be9c09e60f0c6093b18aca998e68eeb6adf514d2a00f2db084903","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-motif/LISTING b/wai/pack-conformance/package-mutations/floor-motif/LISTING new file mode 100644 index 000000000..44af90b9f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-motif/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +44b744229777bd582f45bf670e2dd873798cb21c205f55efaea164acb3a6128f index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +3ba33185bdcba397875e354354a5f54b06b561da6fa98f98584458752eddd41e objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +5272f222d45116baf95dc077d1a2c7d6a647acb1cdba97afdc5c5d2d39faa075 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-motif/index-v.wsix b/wai/pack-conformance/package-mutations/floor-motif/index-v.wsix new file mode 100644 index 000000000..e5ae74a72 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-motif/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai b/wai/pack-conformance/package-mutations/floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai new file mode 100644 index 000000000..6fa04d28a Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-motif/package.receipt.json b/wai/pack-conformance/package-mutations/floor-motif/package.receipt.json new file mode 100644 index 000000000..b9548c8c1 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-motif/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"19cf4094324fa0035cecc08d033ec909800d3abd3993ac213f0bdb2b21225a3b","media_class":"video","stage_root":"4a4e3add7f538c175fbeeefeab33c39458a36c6951c8aa28bcdebb8c59239c84"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"f77642867144ddede7ebba8fa491a3af4a1dde0b172a0d2e51b842670292d8cf","sig":"b0b9f9791f3e4ed02c32083f055cb2095df7b7df83b464819b561326164f0bfe1dd33ec4a6d50f790a63bf68fea8325b627dab496dfdf54b12667b723a92e008","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING b/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING new file mode 100644 index 000000000..a76792fcc --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +0628379d21c859665aad7bd384c6773d63658f2d49a77b781e97d923f8546b37 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-object-corrupt/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai b/wai/pack-conformance/package-mutations/floor-object-corrupt/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai new file mode 100644 index 000000000..3616c1385 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-object-corrupt/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-object-missing/LISTING b/wai/pack-conformance/package-mutations/floor-object-missing/LISTING new file mode 100644 index 000000000..1db5c9f32 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-object-missing/LISTING @@ -0,0 +1,34 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING b/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING new file mode 100644 index 000000000..61f52179c --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING @@ -0,0 +1,35 @@ +c7b6cd01fc6c73bd4663b0587af4b7f8597d3fa1d24f412e535f8bc3567a9df9 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +74a45cf535c3dc3a34b8ecdd81e6309cefb91960a32b7d2a1b174c64c245b376 hls/classic/floor-v.m3u8 +94fe25e5a5860078103f06504feb1d5e6693a9a964c735f4f9192cfcc16fc240 hls/classic/master.m3u8 +852d535942efb87feb4f0b0a69220142d300db1dbda7783ca87fdcd0b801d348 hls/legacy/master.m3u8 +fa991c3f0242f452b02f5c70b72e10e864db388a40b654bfe64dcca8423b077e hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +fa991c3f0242f452b02f5c70b72e10e864db388a40b654bfe64dcca8423b077e hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +f2f570cba26269e220795992dd224078b6c05473940ab1f6a900f2c8d1b220b1 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b89bf554c66c823470d13694ad207313d47946ab2a62017f40bd72f0d2e9abd8 objects/a3ff3effd00c6c5eaf8af992493df833e28d8bf88a7ad829d3f3c5ed5e623227.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +4e6574c817cacfafcb5b32f8c1bbd6fa09cd67d202caa86650ebd48ed1057732 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/dash/classic.mpd b/wai/pack-conformance/package-mutations/floor-opens-on-inter/dash/classic.mpd new file mode 100644 index 000000000..91ca8c44a --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s b/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s new file mode 100644 index 000000000..09980c384 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s differ diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/floor-v.m3u8 b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..55d12301e --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.400, +../../floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s +#EXTINF:1.000, +../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/master.m3u8 b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/master.m3u8 new file mode 100644 index 000000000..282f12af2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=8498,CODECS="av01.0.00M.08",RESOLUTION=64x36 +floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/legacy/master.m3u8 b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/legacy/master.m3u8 new file mode 100644 index 000000000..783798c0f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=8498,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/mobile/master.m3u8 b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/mobile/master.m3u8 new file mode 100644 index 000000000..886380ef5 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/mobile/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=8498,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/tv/master.m3u8 b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/tv/master.m3u8 new file mode 100644 index 000000000..886380ef5 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/tv/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=8498,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/index-v.wsix b/wai/pack-conformance/package-mutations/floor-opens-on-inter/index-v.wsix new file mode 100644 index 000000000..5fed22595 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-opens-on-inter/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/a3ff3effd00c6c5eaf8af992493df833e28d8bf88a7ad829d3f3c5ed5e623227.wai b/wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/a3ff3effd00c6c5eaf8af992493df833e28d8bf88a7ad829d3f3c5ed5e623227.wai new file mode 100644 index 000000000..f108cfd0a Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/a3ff3effd00c6c5eaf8af992493df833e28d8bf88a7ad829d3f3c5ed5e623227.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/package.receipt.json b/wai/pack-conformance/package-mutations/floor-opens-on-inter/package.receipt.json new file mode 100644 index 000000000..f72610e5d --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-opens-on-inter/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"473b6fd1b64743d9d1df955781d86938fe9f47aea516959949f66ddcbc6ca833","media_class":"video","stage_root":"8963046ded4126ec03a4479e8a2d01e1e56227deaf76a2e392614c7ea98328a0"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"82155027500496350ef0dbc6ebf84516906d07021e681e673ed7a741648556e5","sig":"551fe5c6bf5ff0def4672f54f8e23db9b2b7d65e0e39d290c9bfd846cddcc9aac746f34bf6b5a0afac924219655dabc34ee91c101804cf193cca18969ec9460d","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119251,"total_bytes":130435,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING b/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING new file mode 100644 index 000000000..1c6f33f0f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +ed10f43afaa754358e36b19a8fe5a9b614a272d5741bfaebe5fb24ea2a457086 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +ae5b80fc2e0062e560a63d188726619cfc32769f71a30f715ea59cee2dd9e1d5 objects/ac1051ad64db4dc790f34d5a2525bfe3b538d1d6d814e01d3cacde80c38de31d.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +6890249084072ff9ecb97bd87ead89f0926893beb9c91921b99233ae7d2caa4a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-renderer-settings/index-v.wsix b/wai/pack-conformance/package-mutations/floor-renderer-settings/index-v.wsix new file mode 100644 index 000000000..241761897 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-renderer-settings/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-renderer-settings/objects/ac1051ad64db4dc790f34d5a2525bfe3b538d1d6d814e01d3cacde80c38de31d.wai b/wai/pack-conformance/package-mutations/floor-renderer-settings/objects/ac1051ad64db4dc790f34d5a2525bfe3b538d1d6d814e01d3cacde80c38de31d.wai new file mode 100644 index 000000000..a64fd6605 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-renderer-settings/objects/ac1051ad64db4dc790f34d5a2525bfe3b538d1d6d814e01d3cacde80c38de31d.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-renderer-settings/package.receipt.json b/wai/pack-conformance/package-mutations/floor-renderer-settings/package.receipt.json new file mode 100644 index 000000000..3439cbaf7 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-renderer-settings/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"b9785abbac066f26b5317ee19621f56f98b29c75de74cf1792f2440ef7d7fbd9","media_class":"video","stage_root":"3153739e8ead560707e971ac1655cc6d692f2b4b777d731c97ea37c7ba18dfea"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"ebf0fc9dccf04e7a2fc0653696a1b8df81724746fb530acd9fcf531eaa24a98e","sig":"36699d674a0309427340270638bc05ee49a69d7a6c04698d52a35219c89c903937c314f4a40afdc32b5fa6a3d24983d24f54c9207b465cd2d8332fedb13f7e01","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119716,"total_bytes":130900,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING b/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING new file mode 100644 index 000000000..263ffbdee --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-sequence-header/LISTING @@ -0,0 +1,35 @@ +92787231d671a343d959bd1de9d1c4730788f6d3b712db2f53fd529b5e69eaef dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61 floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +1efa2b5ad044916ad7fef2b5af1549d09be62666eaa685b24c751efc10e9bf59 hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +752666c9b5af4bcf8d7eb7a09e43b368069828d2b9848dd882660e64025c74a2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +59c59bd437e2dac861a3b442f36b2387faa5942315c2b9fc0ffadd4163431973 objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +135c6eaa711e7ad9616cb13201c7791c21148e48b10380fc1ebf4060b2514801 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/dash/classic.mpd b/wai/pack-conformance/package-mutations/floor-sequence-header/dash/classic.mpd new file mode 100644 index 000000000..c93a9217a --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-sequence-header/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s b/wai/pack-conformance/package-mutations/floor-sequence-header/floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s new file mode 100644 index 000000000..c587b3380 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-sequence-header/floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s differ diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/hls/classic/floor-v.m3u8 b/wai/pack-conformance/package-mutations/floor-sequence-header/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..2d133d99f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-sequence-header/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.400, +../../floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s +#EXTINF:1.000, +../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/index-v.wsix b/wai/pack-conformance/package-mutations/floor-sequence-header/index-v.wsix new file mode 100644 index 000000000..928f4e521 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-sequence-header/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai b/wai/pack-conformance/package-mutations/floor-sequence-header/objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai new file mode 100644 index 000000000..c1e84c060 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-sequence-header/objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-sequence-header/package.receipt.json b/wai/pack-conformance/package-mutations/floor-sequence-header/package.receipt.json new file mode 100644 index 000000000..1b7bcb85c --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-sequence-header/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"954524b2e52c283ab2a92d2fb4e5015700a0a122ac9f501c23ac5ced35279aea","media_class":"video","stage_root":"4baab3844539ca69c2896649b2c1b45d3660d6825c1326722080b84e1180969e"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"15b12c100ed4d57ec77ed004759c7a6fe5be5e133864b5d2e07c74e25ca1f17e","sig":"1574bf16d089996d18dbe881aa6a7cc891ed46cf525eda00839067e365fa07956f03702a689dea96b77d58ee943c24aae6ff096b8b99af092ad613a04bf4fe0d","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/floor-target/LISTING b/wai/pack-conformance/package-mutations/floor-target/LISTING new file mode 100644 index 000000000..149f2ae9f --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-target/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +11eee7726f33e9ca6e1d16eddb0a01bdd4cff18cfe6caa625585f19429173bbf index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +6cb6a761558f48f6957d33c8c7c4775f6ec421e58a705e75ba03e1ae7e67c219 objects/c252c46801c1e52aaedfe1d4aec422dc0b288c29a1e54c19993546752bdef5a5.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +4d875e5b6d3ff3102c25b5131ea776ebdc69489efdd3a01f19eaf94cfb9b7e10 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/floor-target/index-v.wsix b/wai/pack-conformance/package-mutations/floor-target/index-v.wsix new file mode 100644 index 000000000..b7971efd4 Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-target/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/floor-target/objects/c252c46801c1e52aaedfe1d4aec422dc0b288c29a1e54c19993546752bdef5a5.wai b/wai/pack-conformance/package-mutations/floor-target/objects/c252c46801c1e52aaedfe1d4aec422dc0b288c29a1e54c19993546752bdef5a5.wai new file mode 100644 index 000000000..a34bdb22e Binary files /dev/null and b/wai/pack-conformance/package-mutations/floor-target/objects/c252c46801c1e52aaedfe1d4aec422dc0b288c29a1e54c19993546752bdef5a5.wai differ diff --git a/wai/pack-conformance/package-mutations/floor-target/package.receipt.json b/wai/pack-conformance/package-mutations/floor-target/package.receipt.json new file mode 100644 index 000000000..d3604ecb4 --- /dev/null +++ b/wai/pack-conformance/package-mutations/floor-target/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"1bd9485582cdc5960a36a6b17a07fbdb804cbb5a955811b52354ac1ed07d5305","media_class":"video","stage_root":"3ac7c02012a51fccdd5de519eec297acacf6bec9e3332bc1ca6d060f2d8d162a"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"92c708424407b92c0d28ed7e37e27531b8cf0db4923ee5b02aafee0587461ff1","sig":"191e5439feebc72d3de8b0a728e93f7a6c9ed012ffecbf4aa4d5081e4b5f3b44358217dbb04e366708e72451dcbb517734686971c6a630ce0e69d65ad959af05","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119776,"total_bytes":130960,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/index-capability/LISTING b/wai/pack-conformance/package-mutations/index-capability/LISTING new file mode 100644 index 000000000..3ef6758d9 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-capability/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +baabd81a9cad6e8afacbb18311924557956a0e37f5f271e8d458d651921707c4 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +e05ea0c3345dfbfb076465b65fbb004890906560ce4ccf7ceb3d376231adf35e package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/index-capability/index-v.wsix b/wai/pack-conformance/package-mutations/index-capability/index-v.wsix new file mode 100644 index 000000000..7d27fc9af Binary files /dev/null and b/wai/pack-conformance/package-mutations/index-capability/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/index-capability/package.receipt.json b/wai/pack-conformance/package-mutations/index-capability/package.receipt.json new file mode 100644 index 000000000..f8e6071fc --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-capability/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"ad50d9f462bbaf7fbb808c168bf6ac26d13ebd39da09413d48ba44927a66f7c2","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"40e41a2b3a31b977673d9d1a21ea7f8745cd93fb4ed5519e6c0e0a90bf764909","sig":"ae45bc039b89c420e5a1b6db4d97f5fb9ab3c40b2b2617978a77820f3a3638854ef4a8afc989aab41c05c0939aaa6686ec1ad5d04bf3e1a4fac1db92e997330f","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/index-class-plan/LISTING b/wai/pack-conformance/package-mutations/index-class-plan/LISTING new file mode 100644 index 000000000..14ef8f8e5 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +b71531a5dfbefbda78f25d3c5a6fa2b3e8ef28f3a1c11ad2d9d408d6acf016f5 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +d47a2a0bb43d9bd55d6ff3aa1849b4811e7265e2302d343e95656bb5ae7a426b hls/mobile/wai-v.m3u8 +d06d4523d12c983b815c3f85efa985c0b43fd3633dd54c8e7736e2ff5d8b05c1 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +3c481132f733d1076cad2742c8a4b4667dad83397c1a57abfeb8333ab9944cbb index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +de66e7c71a5bfef57c42bec85a1430082b56f7c6b4b5aad54a4364f10425124a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +f9b02093cd740e442cd254ae6d02ac59f42e2d4b57acfc5f1273ccdc2937a7cb stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/index-class-plan/dash/mobile.mpd b/wai/pack-conformance/package-mutations/index-class-plan/dash/mobile.mpd new file mode 100644 index 000000000..97c930011 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/dash/mobile.mpd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..d09ba8c2b --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai-v.m3u8 @@ -0,0 +1,19 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai.m3u8 b/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..c0d292c60 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="mobile" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/mobile.json" +#EXT-X-STREAM-INF:BANDWIDTH=132754,AVERAGE-BANDWIDTH=108593,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/index-class-plan/index-v.wsix b/wai/pack-conformance/package-mutations/index-class-plan/index-v.wsix new file mode 100644 index 000000000..f6c3878a9 Binary files /dev/null and b/wai/pack-conformance/package-mutations/index-class-plan/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/index-class-plan/package.receipt.json b/wai/pack-conformance/package-mutations/index-class-plan/package.receipt.json new file mode 100644 index 000000000..9a708c7aa --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"a78202afc1b3658a70f2b3ed589cd014b13a0068a60b6a9de0b38b4d346434c5","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"cb55e30d4bfd80f9626d810e8022a453f008f78c91b79dcfa37ef626cadfacc0","sig":"581db97265d7e26df652621d880b3e13def1c0803e471ba9e6ade7c2c7bcc1bd1cc3914f30d866f3ba9ba2266fb77eef2518b5a877eed293d73fccf4bbd1060c","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/index-class-plan/stages/mobile.json b/wai/pack-conformance/package-mutations/index-class-plan/stages/mobile.json new file mode 100644 index 000000000..fa6ec36da --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-plan/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e"} diff --git a/wai/pack-conformance/package-mutations/index-class-rate/LISTING b/wai/pack-conformance/package-mutations/index-class-rate/LISTING new file mode 100644 index 000000000..cb5916f1a --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-rate/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +8bfd45971ced265bfc01d043d53359c2d23aa8ac4c6512b0a421f78e1bdfe476 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +2a967b891402509016f0eb869f48561e119d208923351c264b188a3ba0e7e0d5 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/index-class-rate/index-v.wsix b/wai/pack-conformance/package-mutations/index-class-rate/index-v.wsix new file mode 100644 index 000000000..515c472ff Binary files /dev/null and b/wai/pack-conformance/package-mutations/index-class-rate/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/index-class-rate/package.receipt.json b/wai/pack-conformance/package-mutations/index-class-rate/package.receipt.json new file mode 100644 index 000000000..f6d5e69e5 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-rate/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"d1a14e2b95937df5248058cc868d573f001564b06a4791abe550cd3c6ce0ae90","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"0dcf1104a04734f71eac096340ef585fef4d9a090aa51cfe35ef56648986d33a","sig":"b688e9c8bf2a1b15bc82db0ea389efffed70b99d5326a1b992ae62e7f40213a775b5811d46ecf616ea6eb8fd47e581294bbbb2241f70f3798c6158c78e8f2202","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/index-class-records/LISTING b/wai/pack-conformance/package-mutations/index-class-records/LISTING new file mode 100644 index 000000000..7cc9b75e1 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/LISTING @@ -0,0 +1,38 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +aeb175e4c7c19272ada83f2cca9a7daa44732403d3f3f0744d8010739dd55b3b dash/legacy.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/legacy/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/legacy/wai-v.m3u8 +cf8389a3cb6ffd211c98dd193f1352745f6adf9eab45b1ae744e94e78b396b81 hls/legacy/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +00641a12e923f55c78bdc6d1262a0d4dc218a7c35904590e3dbaf16a7607f745 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +3a9348172de01e2b6ee5d38cfd145688f6deebf15aa451cc043ea9ac889dd499 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +b8621d219fbe2d50074705f6a246b65a7a3978d42b913280707d3f1d251b1f4c stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/index-class-records/dash/legacy.mpd b/wai/pack-conformance/package-mutations/index-class-records/dash/legacy.mpd new file mode 100644 index 000000000..c224caa63 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/dash/legacy.mpd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/master.m3u8 b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/master.m3u8 new file mode 100644 index 000000000..07647e470 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai-v.m3u8 b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai-v.m3u8 new file mode 100644 index 000000000..8d1c2efe8 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai.m3u8 b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai.m3u8 new file mode 100644 index 000000000..eb236d423 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="legacy" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/legacy.json" +#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/package-mutations/index-class-records/index-v.wsix b/wai/pack-conformance/package-mutations/index-class-records/index-v.wsix new file mode 100644 index 000000000..2fb58ef78 Binary files /dev/null and b/wai/pack-conformance/package-mutations/index-class-records/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/index-class-records/package.receipt.json b/wai/pack-conformance/package-mutations/index-class-records/package.receipt.json new file mode 100644 index 000000000..282d31e47 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"2332913ab494656240a915e9f582d6c1395b786c7286263c876ce87f00ad7234","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"4c12c4358f6d2547ea7f1e6c2310fe0964514ff46b35736f8fa6e1cb78c29a97","sig":"c1b266ce3244d9d9262c7116ef1439ebe96c08516edf6a3216c0fdb2c7072895b5321ebc9b2bd8733d2d02e724973fe9d1e16ccba89ac0b5bf5090013483eb08","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/index-class-records/stages/legacy.json b/wai/pack-conformance/package-mutations/index-class-records/stages/legacy.json new file mode 100644 index 000000000..70f95f411 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-class-records/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"media_playlist":"../hls/legacy/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e"} diff --git a/wai/pack-conformance/package-mutations/index-string-order/LISTING b/wai/pack-conformance/package-mutations/index-string-order/LISTING new file mode 100644 index 000000000..9204bb2d2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-string-order/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +c3a5e23418c1ff2cc8e12281df47a82bebe61b31260997a71311f1275d104334 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +47b4118829afd2a82210855f64d7e80c94ce50de66a4b5e646f9858c33afe86f package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/index-string-order/index-v.wsix b/wai/pack-conformance/package-mutations/index-string-order/index-v.wsix new file mode 100644 index 000000000..0a53b9632 Binary files /dev/null and b/wai/pack-conformance/package-mutations/index-string-order/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/index-string-order/package.receipt.json b/wai/pack-conformance/package-mutations/index-string-order/package.receipt.json new file mode 100644 index 000000000..6d6c379fd --- /dev/null +++ b/wai/pack-conformance/package-mutations/index-string-order/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"c821b79bdb894262d7aca31d88370e57aaf18e0fc9d3442955d440a6c02deb7b","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"482e0b6785816934c9d9311109bb993b9e469f710d308a97c78f3f8109ca6f67","sig":"7c2138ae6137950ba0d13941cf86a3dde8ed3b823acb50524b975fb8903b250dc09ea33329f07de62a381b2b3e7a8054353171132094ef2a5ee0d8523db03f06","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/LISTING b/wai/pack-conformance/package-mutations/live-window-past-2-53/LISTING new file mode 100644 index 000000000..88b73ee4b --- /dev/null +++ b/wai/pack-conformance/package-mutations/live-window-past-2-53/LISTING @@ -0,0 +1,19 @@ +033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e floor/v/033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e.m4s +1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c floor/v/1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s +a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a floor/v/a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s +d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60 floor/v/d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s +e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8 floor/v/e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +c08ab65657da08c94b08b66e6124760e2815d882e0ba9e6b1677d04c3e4ebeb5 index-v.wsix +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +17b67eb2c495b48ff011ec183f85af009617e506790d7d9d1f0b6fc2db45e14e package.receipt.json +3942e4a483260dcdd713df1724b92ebe874dbfb6cd21428a11bc5182cc79d0c1 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e.m4s b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e.m4s new file mode 100644 index 000000000..5e3a1fc1e Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e.m4s differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s new file mode 100644 index 000000000..7acbe4036 Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s new file mode 100644 index 000000000..b5246b957 Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s new file mode 100644 index 000000000..7213822ae Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8.m4s b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8.m4s new file mode 100644 index 000000000..c69605cc5 Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8.m4s differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/index-v.wsix b/wai/pack-conformance/package-mutations/live-window-past-2-53/index-v.wsix new file mode 100644 index 000000000..b62ad5ea9 Binary files /dev/null and b/wai/pack-conformance/package-mutations/live-window-past-2-53/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/package.receipt.json b/wai/pack-conformance/package-mutations/live-window-past-2-53/package.receipt.json new file mode 100644 index 000000000..b50ee908c --- /dev/null +++ b/wai/pack-conformance/package-mutations/live-window-past-2-53/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"d04ab03bf63effbcf809b0f9a417b336a709d1fe45a56e24a3c9677ea6653806","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"live-window","parent_receipt_hash":"0707070707070707070707070707070707070707070707070707070707070707","policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"0efedbbd31a1aa913eff5c232bee2030129ef2c7334ff50fa85b806fab79332e","sig":"48848c3959fc0fe64c081816e33d6b69e4d1ce71b80cf34e2de2e36e4f85b48e60b578ec82a6873b85bc61387a50eb5e9c6a78952d0358cce233ca20c2f0e705","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/live-window-past-2-53/plan-v.txt b/wai/pack-conformance/package-mutations/live-window-past-2-53/plan-v.txt new file mode 100644 index 000000000..9d48dd9fb --- /dev/null +++ b/wai/pack-conformance/package-mutations/live-window-past-2-53/plan-v.txt @@ -0,0 +1,5 @@ +9007199254740992 +9007199254741052 +9007199254741077 +9007199254741127 +9007199254741152 diff --git a/wai/pack-conformance/package-mutations/no-component-live/LISTING b/wai/pack-conformance/package-mutations/no-component-live/LISTING new file mode 100644 index 000000000..4515c81f7 --- /dev/null +++ b/wai/pack-conformance/package-mutations/no-component-live/LISTING @@ -0,0 +1,3 @@ +0d8cfb4e9fdce26c652c9c9e63f34285201d373c24870d75628b1ae715e489ff package.receipt.json +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml diff --git a/wai/pack-conformance/package-mutations/no-component-live/package.receipt.json b/wai/pack-conformance/package-mutations/no-component-live/package.receipt.json new file mode 100644 index 000000000..3fe43077b --- /dev/null +++ b/wai/pack-conformance/package-mutations/no-component-live/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"live-window","parent_receipt_hash":"0909090909090909090909090909090909090909090909090909090909090909","policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"fd1454e98b74e5b8193dd59dd55e60a5035e3fdcba4a50fcd06eedd69342c667","sig":"56e1c4ba101a46e020d3eb7b3abcc9d5bb5d28401ddd2001cb676bd280046362d8e12cdbe3937573e9e6dbbfd2f189129186d657307f0a60c19b62f718137107","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":0,"total_bytes":0,"total_entries":0,"unique_objects":0,"version":2,"work_units":0} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/no-component/LISTING b/wai/pack-conformance/package-mutations/no-component/LISTING new file mode 100644 index 000000000..160ebbf80 --- /dev/null +++ b/wai/pack-conformance/package-mutations/no-component/LISTING @@ -0,0 +1,3 @@ +0a15f980f155100adcc0a8691894c03a01aeed7339ffdfac3febfa43231b6284 package.receipt.json +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml diff --git a/wai/pack-conformance/package-mutations/no-component/package.receipt.json b/wai/pack-conformance/package-mutations/no-component/package.receipt.json new file mode 100644 index 000000000..1db49964b --- /dev/null +++ b/wai/pack-conformance/package-mutations/no-component/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"98310d163850b333fd853c4ee758bdaa837a3fe3a05621820b060d9f2c0a45f6","sig":"df751974feda4ba45d2b04df698a064a024b8149591d269718ee5f9f5f9dd1086fb8c742d4571ebdd0d61681ca7c4bd271b9c118e81a836c9195a4e9bdc3a70c","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":0,"total_bytes":0,"total_entries":0,"unique_objects":0,"version":2,"work_units":0} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING new file mode 100644 index 000000000..0800ca91f --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +ad140ff82229d3f47f4b2b67be03006ae5a86f89444b4db2b1265ed32766b965 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +2607763ae9cdd7e3ae0217fe4f924305da3edadf8d4c91fd2a3192e9b334ed70 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +b601f45d393a0a8629a94ddfe96a418ed48447efc4977d85999ac47b08b227f8 policy.json +5cc6535d288878f477e376fe24f4b2d0f22971d229fc9a1d5d6d595b9d814d37 policy.toml +5d32285384950d3bfd11418750457a2361c1c8c5d5969d04d4d06e6db7ac7b44 stages/legacy.json +ebd9080703cb1d7fcdf6465ee986f7c49d5de18f0fb17be432f7ba9d615bab15 stages/mobile.json +ceef47b18a2ab5feb8e96a9b6d6e8d3bad850c64749c9efbe38dbeeba4bdda07 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/index-v.wsix b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/index-v.wsix new file mode 100644 index 000000000..279be0031 Binary files /dev/null and b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/package.receipt.json b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/package.receipt.json new file mode 100644 index 000000000..925f6de5c --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"15b505b570be4162043c8470488cc32a0633e02a77bf0ac9d804c65ae965f84a","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89","receipt_hash":"713f17ae18ddcae38d571c8d934cae9b0c42fdf6fc548447e1b6ff624f922467","sig":"5074e7da8d128407360de8b9c97130a16cc3ceafce19963ffad01e903b20cbc5c3adf8b56c023735e905d98957f160420eff2235913590f49e8bf23fbe809804","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.json b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.json new file mode 100644 index 000000000..c98698975 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.avc","component":"v","id":"f0","params":{},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.toml b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.toml new file mode 100644 index 000000000..4fb88b6f3 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.toml @@ -0,0 +1,45 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.avc" +role = "floor" + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/legacy.json b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/legacy.json new file mode 100644 index 000000000..30b09a460 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/mobile.json b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/mobile.json new file mode 100644 index 000000000..ad3709e07 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/tv.json b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/tv.json new file mode 100644 index 000000000..be153da36 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING b/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING new file mode 100644 index 000000000..e59d903d7 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +ad140ff82229d3f47f4b2b67be03006ae5a86f89444b4db2b1265ed32766b965 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +2607763ae9cdd7e3ae0217fe4f924305da3edadf8d4c91fd2a3192e9b334ed70 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +b601f45d393a0a8629a94ddfe96a418ed48447efc4977d85999ac47b08b227f8 policy.json +426a1e8cbaf7acfab22f87c8832bc39238e73ae899b9abe92ed2b4356c640238 policy.toml +5d32285384950d3bfd11418750457a2361c1c8c5d5969d04d4d06e6db7ac7b44 stages/legacy.json +ebd9080703cb1d7fcdf6465ee986f7c49d5de18f0fb17be432f7ba9d615bab15 stages/mobile.json +ceef47b18a2ab5feb8e96a9b6d6e8d3bad850c64749c9efbe38dbeeba4bdda07 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/index-v.wsix b/wai/pack-conformance/package-mutations/patent-pool-floor/index-v.wsix new file mode 100644 index 000000000..279be0031 Binary files /dev/null and b/wai/pack-conformance/package-mutations/patent-pool-floor/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/package.receipt.json b/wai/pack-conformance/package-mutations/patent-pool-floor/package.receipt.json new file mode 100644 index 000000000..925f6de5c --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"15b505b570be4162043c8470488cc32a0633e02a77bf0ac9d804c65ae965f84a","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89","receipt_hash":"713f17ae18ddcae38d571c8d934cae9b0c42fdf6fc548447e1b6ff624f922467","sig":"5074e7da8d128407360de8b9c97130a16cc3ceafce19963ffad01e903b20cbc5c3adf8b56c023735e905d98957f160420eff2235913590f49e8bf23fbe809804","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/policy.json b/wai/pack-conformance/package-mutations/patent-pool-floor/policy.json new file mode 100644 index 000000000..c98698975 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.avc","component":"v","id":"f0","params":{},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/policy.toml b/wai/pack-conformance/package-mutations/patent-pool-floor/policy.toml new file mode 100644 index 000000000..15040c638 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/policy.toml @@ -0,0 +1,46 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.avc" +role = "floor" + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/stages/legacy.json b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/legacy.json new file mode 100644 index 000000000..30b09a460 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/stages/mobile.json b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/mobile.json new file mode 100644 index 000000000..ad3709e07 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/patent-pool-floor/stages/tv.json b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/tv.json new file mode 100644 index 000000000..be153da36 --- /dev/null +++ b/wai/pack-conformance/package-mutations/patent-pool-floor/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"8e3819cd851ac047ff8d7e8012892ad3f878ebcdc8c5a51751948b721314cd89"} diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING new file mode 100644 index 000000000..6c2f5c5df --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +108469a87ed44e9a7ce517eae4507fc789b7a3ffa926ded766ca463d6443e161 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +a48f00494e0742a3efe861fcdd419d46f240f6edcf2cb82474e07992adf2bead objects/625a28c3601bc4f6806179f6247c07efba99f455fc48b40a36f9a8303e10b428.wai +a653ee6ecf5257a1f053dd92f37ff8280ca300d3703b1d2470a91af577718555 objects/63ce2c64c49f7e10935c342b6141e1eebb9b7565cb1615ea1457936a974ebeed.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +8fca32e4dca1851138d152170ee8d2be07d8f73c3465c09b55274a3cb6058636 objects/cd24dd001a473e1c8925393968a7e7cac75709acc00d304e5d42633d85aaf2ec.wai +7c9e0281525adccc8680809e04ebc2cdd3dbc00c2beeaad75bdfca7442c8f151 objects/fc49fcf2bd8fcd8da7cdc095b0fa7c36576c8228ae1a631a08d9c766bbd0e355.wai +d5a65fcf5c42dfcfc8c5643e505a143b7cdab7fdb994f2365b05714ad636ad69 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +f52ed9f10e7e854a8e3c518fe0c86fe8b69daea079924819a87f20468dce3100 policy.json +426a1e8cbaf7acfab22f87c8832bc39238e73ae899b9abe92ed2b4356c640238 policy.toml +b97dd352e6726b02544cfd75a4ef0aeae36dcf402804f79de3678825a2aeb4b1 stages/legacy.json +5de066fe39c0bf46262e8404dd2290a331bcd0e7c663caa57a0fe6c4fd5f4c9e stages/mobile.json +ddeb15326c1c980f36733b2979457a845857c14bc4c3402605c92e82f20afd27 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/index-v.wsix b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/index-v.wsix new file mode 100644 index 000000000..f026dea48 Binary files /dev/null and b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/index-v.wsix differ diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/625a28c3601bc4f6806179f6247c07efba99f455fc48b40a36f9a8303e10b428.wai b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/625a28c3601bc4f6806179f6247c07efba99f455fc48b40a36f9a8303e10b428.wai new file mode 100644 index 000000000..7fb82a295 Binary files /dev/null and b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/625a28c3601bc4f6806179f6247c07efba99f455fc48b40a36f9a8303e10b428.wai differ diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/63ce2c64c49f7e10935c342b6141e1eebb9b7565cb1615ea1457936a974ebeed.wai b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/63ce2c64c49f7e10935c342b6141e1eebb9b7565cb1615ea1457936a974ebeed.wai new file mode 100644 index 000000000..097ddd162 Binary files /dev/null and b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/63ce2c64c49f7e10935c342b6141e1eebb9b7565cb1615ea1457936a974ebeed.wai differ diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/cd24dd001a473e1c8925393968a7e7cac75709acc00d304e5d42633d85aaf2ec.wai b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/cd24dd001a473e1c8925393968a7e7cac75709acc00d304e5d42633d85aaf2ec.wai new file mode 100644 index 000000000..939e478dd Binary files /dev/null and b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/cd24dd001a473e1c8925393968a7e7cac75709acc00d304e5d42633d85aaf2ec.wai differ diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/fc49fcf2bd8fcd8da7cdc095b0fa7c36576c8228ae1a631a08d9c766bbd0e355.wai b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/fc49fcf2bd8fcd8da7cdc095b0fa7c36576c8228ae1a631a08d9c766bbd0e355.wai new file mode 100644 index 000000000..237924bbd Binary files /dev/null and b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/fc49fcf2bd8fcd8da7cdc095b0fa7c36576c8228ae1a631a08d9c766bbd0e355.wai differ diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/package.receipt.json b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/package.receipt.json new file mode 100644 index 000000000..80d9eb819 --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"3a43f31c6f7069f6a250a60b5e312e1f76686b33133492ff6920f8181c891328","media_class":"video","stage_root":"7d5a45a29a639b6b1441b8eb5998417eb90f87dd0affe06776ccdf53004f1200"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"f3aa3e42518096bc871be46b7f8ae04ccae35efe6f166df4c1ffe381ccd27610","receipt_hash":"c4d55d04f97c395ccb75aceec1149b0bfb236e15da6ed7be61f6270b806edba5","sig":"307753a61b5ef0638c290197f698755046c4d29429ef38b39fdebd4e13b858a0a261972b704c0acca723a98cf4516f528b99b4c9d10dd190a912ada93f8fe30f","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.json b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.json new file mode 100644 index 000000000..4d638f041 --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.avc","component":"v","id":"f0","params":{},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":false,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.toml b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.toml new file mode 100644 index 000000000..15040c638 --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.toml @@ -0,0 +1,46 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.avc" +role = "floor" + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/legacy.json b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/legacy.json new file mode 100644 index 000000000..8ba0f50ac --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"f3aa3e42518096bc871be46b7f8ae04ccae35efe6f166df4c1ffe381ccd27610"} diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/mobile.json b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/mobile.json new file mode 100644 index 000000000..a2450ea90 --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"f3aa3e42518096bc871be46b7f8ae04ccae35efe6f166df4c1ffe381ccd27610"} diff --git a/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/tv.json b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/tv.json new file mode 100644 index 000000000..fddb4614a --- /dev/null +++ b/wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"f3aa3e42518096bc871be46b7f8ae04ccae35efe6f166df4c1ffe381ccd27610"} diff --git a/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING b/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING new file mode 100644 index 000000000..589f292b5 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +8c6eb5e9fb7692c3e5f77dd64dbec22f5608276f7128ed222d30108feed11cda package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/receipt-duplicate-member/package.receipt.json b/wai/pack-conformance/package-mutations/receipt-duplicate-member/package.receipt.json new file mode 100644 index 000000000..a116d6a71 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-duplicate-member/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"22c6f558063499040127d58b062f08f9ec2a6cee54b51c97d42e8741d76ad728","sig":"4554bd1c09aaa4af8799e37e2379cde9d341c7982a6d31a050162ee075c07f93c507abfda9c8223e70b9c56a4dbf9cee626ddaa680bd807fb4eda1fd2f885b06","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/receipt-newline/LISTING b/wai/pack-conformance/package-mutations/receipt-newline/LISTING new file mode 100644 index 000000000..449f766d8 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-newline/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +cd5f3710152fdfefc261331ae7312db56b6bb9c630369034f5cb03b179128043 package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/receipt-newline/package.receipt.json b/wai/pack-conformance/package-mutations/receipt-newline/package.receipt.json new file mode 100644 index 000000000..169edbca6 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-newline/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"22c6f558063499040127d58b062f08f9ec2a6cee54b51c97d42e8741d76ad728","sig":"4554bd1c09aaa4af8799e37e2379cde9d341c7982a6d31a050162ee075c07f93c507abfda9c8223e70b9c56a4dbf9cee626ddaa680bd807fb4eda1fd2f885b06","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} diff --git a/wai/pack-conformance/package-mutations/receipt-pretty/LISTING b/wai/pack-conformance/package-mutations/receipt-pretty/LISTING new file mode 100644 index 000000000..60d61b2f1 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-pretty/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +c5ab78bdda64206e3db9cf1c6c6f76e26d40f6db68aff7b19954f680ab19fcae package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/receipt-pretty/package.receipt.json b/wai/pack-conformance/package-mutations/receipt-pretty/package.receipt.json new file mode 100644 index 000000000..6f7dc6024 --- /dev/null +++ b/wai/pack-conformance/package-mutations/receipt-pretty/package.receipt.json @@ -0,0 +1,35 @@ +{ + "asset_id": "video-base-01", + "components": [ + { + "id": "v", + "index": "index-v.wsix", + "index_digest": "5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0", + "media_class": "video", + "stage_root": "bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6" + } + ], + "energy_provenance": null, + "grant": { + "capability": "wai.video.package", + "funds_ceiling": null, + "grant_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "joule_ceiling_micro": null + }, + "joules_micro": 0, + "kind": "package", + "packager": "builtin:wai-pack@0.1.0", + "parent_kind": "none", + "parent_receipt_hash": null, + "policy_digest": "e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e", + "receipt_hash": "22c6f558063499040127d58b062f08f9ec2a6cee54b51c97d42e8741d76ad728", + "sig": "4554bd1c09aaa4af8799e37e2379cde9d341c7982a6d31a050162ee075c07f93c507abfda9c8223e70b9c56a4dbf9cee626ddaa680bd807fb4eda1fd2f885b06", + "signer_id": "did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD", + "signer_pubkey": "0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0", + "stored_bytes": 119779, + "total_bytes": 130963, + "total_entries": 10, + "unique_objects": 8, + "version": 2, + "work_units": 10 +} \ No newline at end of file diff --git a/wai/pack-conformance/package-mutations/sha256-file/LISTING b/wai/pack-conformance/package-mutations/sha256-file/LISTING new file mode 100644 index 000000000..62b2d47b2 --- /dev/null +++ b/wai/pack-conformance/package-mutations/sha256-file/LISTING @@ -0,0 +1,36 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json +ee0874170b7f6f32b8c2ac9573c428d35b575270a66b757c2c0185d2bd09718d payload.sha256 +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/package-mutations/sha256-file/payload.sha256 b/wai/pack-conformance/package-mutations/sha256-file/payload.sha256 new file mode 100644 index 000000000..67aed78ea --- /dev/null +++ b/wai/pack-conformance/package-mutations/sha256-file/payload.sha256 @@ -0,0 +1 @@ +anything \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-base-01/floor/v.obu b/wai/pack-conformance/packs/video-base-01/floor/v.obu new file mode 100644 index 000000000..40171fdd0 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/floor/v.obu differ diff --git a/wai/pack-conformance/packs/video-base-01/key.hex b/wai/pack-conformance/packs/video-base-01/key.hex new file mode 100644 index 000000000..bfac84fc5 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/key.hex @@ -0,0 +1 @@ +5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a diff --git a/wai/pack-conformance/packs/video-base-01/package/LISTING b/wai/pack-conformance/packs/video-base-01/package/LISTING new file mode 100644 index 000000000..b8ce14807 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/LISTING @@ -0,0 +1,35 @@ +91b8aba2292191f2046eb841fe6fcb7b214826cecd9e06a3c193a68e7089e144 dash/classic.mpd +a3aa7658db9b85a9d9a1fed977825189f75c5adad0f5fdfcd70ebd604824c138 dash/mobile.mpd +3f520749a6d2f231cddfd17804ee3629439f3678515e424c44e36cff54d89de9 dash/tv.mpd +04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6 floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15 floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +58c598174eed554cd0d1bc8abedca8c9d51d992c8e3b963bcb7574da4983ed9f floor/v/init.mp4 +289bfcb09eb25124ac19597bc624371c34bea00aa1011ced9372d96b5e14b2bd hls/classic/floor-v.m3u8 +15a96a7b9a5d5608ebaaf5976cd6ad88bed032ca60c4ecd246e142022e071be5 hls/classic/master.m3u8 +3026d7f11005e34ff66298175777ee6806d649cf9d5c1467aaa4fb00ac5987e6 hls/legacy/master.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/mobile/master.m3u8 +c4044eca76959c9f87230484a2b207fb12c0bcaedb8d921eab806014fca53cb8 hls/mobile/wai-v.m3u8 +60a6378f46afc9308dc4ac4d138602b16568577616ea0bf4526b142d86899151 hls/mobile/wai.m3u8 +c5902f61ffaf77f940c4d3dc6e850340aaaeb3f433e4541f935c32443c50b341 hls/tv/master.m3u8 +ff4edf2929e380d205c1c74156621185feec53d593ffa968b8dfaf0d05448bcd hls/tv/wai-v.m3u8 +5d42147b308330913c3d9c8a868ca83ce4d3f3e460da8d84c84014eb24aaf9d7 hls/tv/wai.m3u8 +0b65400d0bbde57ed6a2c19d90cb72ec753de0e76165e4f286acfd5a7bda14d2 index-v.wsix +57ecdf226998f26618121279f42ebe674cb5467c489f6e9aa12feba24989d9ab moq/catalog.json +07e98fb9ef9ff3fa6bf04f9078da2a3d2423e586feea1061256131a1d1fbb943 objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai +4d583d03292b77f9d46b02f2c80a774b1cda1b07df38243d18fe586b82eaca60 objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai +cfb71256ef44758ea279846a8938823994d321abce83ef73afd2ed269a8e0259 objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai +f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai +9fdf29f5c9536d1f7701c791328d656ef725ba9b8cb3760bdb00f5a503c69338 objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai +a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai +3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai +b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json +517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt +a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json +d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json +b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json +33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json diff --git a/wai/pack-conformance/packs/video-base-01/package/dash/classic.mpd b/wai/pack-conformance/packs/video-base-01/package/dash/classic.mpd new file mode 100644 index 000000000..aff186979 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/dash/classic.mpd @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-base-01/package/dash/mobile.mpd b/wai/pack-conformance/packs/video-base-01/package/dash/mobile.mpd new file mode 100644 index 000000000..d3340daa2 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/dash/mobile.mpd @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-base-01/package/dash/tv.mpd b/wai/pack-conformance/packs/video-base-01/package/dash/tv.mpd new file mode 100644 index 000000000..f0817bb2c --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/dash/tv.mpd @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s b/wai/pack-conformance/packs/video-base-01/package/floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s new file mode 100644 index 000000000..7e5a29203 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s differ diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s b/wai/pack-conformance/packs/video-base-01/package/floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s new file mode 100644 index 000000000..3e21af5bf Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s differ diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s b/wai/pack-conformance/packs/video-base-01/package/floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s new file mode 100644 index 000000000..ec55da7e8 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s differ diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s b/wai/pack-conformance/packs/video-base-01/package/floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s new file mode 100644 index 000000000..a246e72be Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s differ diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s b/wai/pack-conformance/packs/video-base-01/package/floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s new file mode 100644 index 000000000..f619273b0 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s differ diff --git a/wai/pack-conformance/packs/video-base-01/package/floor/v/init.mp4 b/wai/pack-conformance/packs/video-base-01/package/floor/v/init.mp4 new file mode 100644 index 000000000..0b3ade258 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/floor/v/init.mp4 differ diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/classic/floor-v.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/classic/floor-v.m3u8 new file mode 100644 index 000000000..dcd36c10d --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/classic/floor-v.m3u8 @@ -0,0 +1,18 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-MAP:URI="../../floor/v/init.mp4" +#EXTINF:2.400, +../../floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s +#EXTINF:1.000, +../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s +#EXTINF:2.000, +../../floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s +#EXTINF:1.000, +../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s +#EXTINF:1.600, +../../floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/classic/master.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/classic/master.m3u8 new file mode 100644 index 000000000..caa181b24 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/classic/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/legacy/master.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/legacy/master.m3u8 new file mode 100644 index 000000000..5495adf2c --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/legacy/master.m3u8 @@ -0,0 +1,5 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/mobile/master.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/master.m3u8 new file mode 100644 index 000000000..07647e470 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai-v.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai-v.m3u8 new file mode 100644 index 000000000..949e945a2 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai-v.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-GAP +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXT-X-GAP +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai.m3u8 new file mode 100644 index 000000000..66fa8dc50 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="mobile" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/mobile.json" +#EXT-X-STREAM-INF:BANDWIDTH=79600,AVERAGE-BANDWIDTH=79600,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/tv/master.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/tv/master.m3u8 new file mode 100644 index 000000000..07647e470 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/tv/master.m3u8 @@ -0,0 +1,6 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.multivariant",VALUE="wai.m3u8" +#EXT-X-STREAM-INF:BANDWIDTH=10295,AVERAGE-BANDWIDTH=9026,CODECS="av01.0.00M.08",RESOLUTION=64x36 +../classic/floor-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai-v.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai-v.m3u8 new file mode 100644 index 000000000..8d1c2efe8 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai-v.m3u8 @@ -0,0 +1,17 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-TARGETDURATION:3 +#EXT-X-MEDIA-SEQUENCE:0 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-INDEPENDENT-SEGMENTS +#EXTINF:2.400, +../../objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai#n=39826 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:2.000, +../../objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai#n=35882 +#EXTINF:1.000, +../../objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai#n=9950 +#EXTINF:1.600, +../../objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai#n=25514 +#EXT-X-ENDLIST diff --git a/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai.m3u8 b/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai.m3u8 new file mode 100644 index 000000000..47679d0e9 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/hls/tv/wai.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +#EXT-X-VERSION:7 +#EXT-X-INDEPENDENT-SEGMENTS +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.class",VALUE="tv" +#EXT-X-SESSION-DATA:DATA-ID="science.transaction.wai.stages",URI="../../stages/tv.json" +#EXT-X-STREAM-INF:BANDWIDTH=143528,AVERAGE-BANDWIDTH=121122,CODECS="wai1.video.int_motion",X-WAI-FLOOR="wai1.video.int_motion",RESOLUTION=64x36 +wai-v.m3u8 diff --git a/wai/pack-conformance/packs/video-base-01/package/index-v.wsix b/wai/pack-conformance/packs/video-base-01/package/index-v.wsix new file mode 100644 index 000000000..a5370aa2a Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/index-v.wsix differ diff --git a/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json b/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json new file mode 100644 index 000000000..40bccfcac --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/moq/catalog.json @@ -0,0 +1 @@ +{"tracks":[{"altGroup":1,"name":"v/l0","packaging":"wai","priority":64,"renderGroup":1,"wai_capabilities":["wai.video.int_motion"],"wai_determinism":"decode-equivalence","wai_layer":0,"wai_receipts":false},{"altGroup":1,"codec":"av01.0.00M.08","height":36,"name":"v/floor","packaging":"cmaf","renderGroup":1,"width":64}],"version":1} diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai b/wai/pack-conformance/packs/video-base-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai new file mode 100644 index 000000000..b15b53108 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai b/wai/pack-conformance/packs/video-base-01/package/objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai new file mode 100644 index 000000000..c79ffdded Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai b/wai/pack-conformance/packs/video-base-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai new file mode 100644 index 000000000..04de3f188 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai b/wai/pack-conformance/packs/video-base-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai new file mode 100644 index 000000000..622209937 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai b/wai/pack-conformance/packs/video-base-01/package/objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai new file mode 100644 index 000000000..91e94a1c7 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai b/wai/pack-conformance/packs/video-base-01/package/objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai new file mode 100644 index 000000000..95467d8ee Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai b/wai/pack-conformance/packs/video-base-01/package/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai new file mode 100644 index 000000000..b6c59ed67 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai b/wai/pack-conformance/packs/video-base-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai new file mode 100644 index 000000000..2ab2cbd49 Binary files /dev/null and b/wai/pack-conformance/packs/video-base-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai differ diff --git a/wai/pack-conformance/packs/video-base-01/package/package.receipt.json b/wai/pack-conformance/packs/video-base-01/package/package.receipt.json new file mode 100644 index 000000000..e7594dd7a --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/package.receipt.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:wai-pack@0.1.0","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e","receipt_hash":"22c6f558063499040127d58b062f08f9ec2a6cee54b51c97d42e8741d76ad728","sig":"4554bd1c09aaa4af8799e37e2379cde9d341c7982a6d31a050162ee075c07f93c507abfda9c8223e70b9c56a4dbf9cee626ddaa680bd807fb4eda1fd2f885b06","signer_id":"did:key:z6MkfMo6gxqdBhaHMNnmfhgZFBjpCDTkmJMJLoypsBZS9PwD","signer_pubkey":"0d7550754e0800a5d237eef5826035766b9b3e5a15868a940ab289958788e3b0","stored_bytes":119779,"total_bytes":130963,"total_entries":10,"unique_objects":8,"version":2,"work_units":10} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-base-01/package/plan-v.txt b/wai/pack-conformance/packs/video-base-01/package/plan-v.txt new file mode 100644 index 000000000..c7196c34c --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/plan-v.txt @@ -0,0 +1,5 @@ +0 +60 +85 +135 +160 diff --git a/wai/pack-conformance/packs/video-base-01/package/policy.json b/wai/pack-conformance/packs/video-base-01/package/policy.json new file mode 100644 index 000000000..025b4a5f8 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/policy.json @@ -0,0 +1 @@ +{"asset_id":"video-base-01","class":[{"floor_only":false,"id":"tv","max_layer":0},{"floor_only":false,"id":"mobile","max_kbps":100,"max_layer":0},{"floor_only":true,"id":"legacy","max_layer":0}],"classify":{"motion_high_milli":9000,"static_mad_milli":300,"static_permille":5,"synthetic_bins":12},"component":[{"id":"v","media":"video","source":"video-base-01.y4m","timebase":{"den":1,"num":25}}],"defaults":{"deadline_ms":1500},"floor":{"extra_keyframes":false,"supplied_listing":"6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab"},"ladder":[{"capability":"wai.video.int_motion","component":"v","id":"b0","params":{"block":16,"max_sse_px_milli":30000,"mv_precision":1,"q_candidates":[16,12,8],"search":4},"role":"base"},{"capability":"wai.video.av1","component":"v","id":"f0","params":{"source":"supplied"},"role":"floor"}],"lead":"v","live":{"layer_lag_ms":[0]},"moment":[],"royalty_free_only":true,"rule":[],"schema":"wai.pack.policy/1","segmentation":{"closed":true,"cut_abs_permille":300,"cut_rel_permille":3000,"cut_window":8,"lookahead_frames":6,"max_ms":4000,"min_ms":1000,"scene_cut":"wai.pack.scenecut/1","target_ms":2000}} \ No newline at end of file diff --git a/wai/pack-conformance/packs/video-base-01/package/policy.toml b/wai/pack-conformance/packs/video-base-01/package/policy.toml new file mode 100644 index 000000000..a452db7ba --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/policy.toml @@ -0,0 +1,47 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json b/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json new file mode 100644 index 000000000..5ffa239e8 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"floor_only":true,"p":1,"stages":[],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"floor_only":true,"p":3,"stages":[],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e"} diff --git a/wai/pack-conformance/packs/video-base-01/package/stages/mobile.json b/wai/pack-conformance/packs/video-base-01/package/stages/mobile.json new file mode 100644 index 000000000..8b9f0f489 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/stages/mobile.json @@ -0,0 +1 @@ +{"class":"mobile","components":{"v":{"media_playlist":"../hls/mobile/wai-v.m3u8","pieces":[{"dur":60,"floor_only":true,"p":0,"stages":[],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"floor_only":true,"p":2,"stages":[],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"floor_only":true,"p":4,"stages":[],"start":160}]}},"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e"} diff --git a/wai/pack-conformance/packs/video-base-01/package/stages/tv.json b/wai/pack-conformance/packs/video-base-01/package/stages/tv.json new file mode 100644 index 000000000..bd9100d72 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/stages/tv.json @@ -0,0 +1 @@ +{"class":"tv","components":{"v":{"media_playlist":"../hls/tv/wai-v.m3u8","pieces":[{"dur":60,"p":0,"stages":[{"bytes":39826,"h":"24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d","layer":0}],"start":0},{"dur":25,"p":1,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":60},{"dur":50,"p":2,"stages":[{"bytes":35882,"h":"c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2","layer":0}],"start":85},{"dur":25,"p":3,"stages":[{"bytes":9950,"h":"61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365","layer":0}],"start":135},{"dur":40,"p":4,"stages":[{"bytes":25514,"h":"618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481","layer":0}],"start":160}]}},"policy_digest":"e1c131d56798eb840e22736076d74908bca2b7585aeae7e4bb1698218c01a35e"} diff --git a/wai/pack-conformance/packs/video-base-01/policy.toml b/wai/pack-conformance/packs/video-base-01/policy.toml new file mode 100644 index 000000000..a452db7ba --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/policy.toml @@ -0,0 +1,47 @@ +schema = "wai.pack.policy/1" +asset_id = "video-base-01" +royalty_free_only = true +lead = "v" + +[[component]] +id = "v" +media = "video" +source = "video-base-01.y4m" + +[component.timebase] +num = 25 +den = 1 + +[segmentation] +target_ms = 2000 +min_ms = 1000 +max_ms = 4000 + +[[ladder]] +id = "b0" +component = "v" +capability = "wai.video.int_motion" +role = "base" +params = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 } + +[[ladder]] +id = "f0" +component = "v" +capability = "wai.video.av1" +role = "floor" +params = { source = "supplied" } + +[floor] +supplied_listing = "6b5ed967f771b936390b4101743a6f6fec8bf0fa2abd1165add3e750b0b86dab" + +[[class]] +id = "tv" +max_layer = 0 + +[[class]] +id = "mobile" +max_kbps = 100 + +[[class]] +id = "legacy" +floor_only = true diff --git a/wai/pack-conformance/policy/expected.json b/wai/pack-conformance/policy/expected.json index af918f9d2..bcf864418 100644 --- a/wai/pack-conformance/policy/expected.json +++ b/wai/pack-conformance/policy/expected.json @@ -26,11 +26,11 @@ "digest": "1997e19e06f6bf191d5de605ba1a4719a3372d5e7a168b5364cc82638d9dd0e9" }, { - "file": "example.toml", - "canonical": "{\"asset_id\":\"pack-fixture-01\",\"class\":[{\"floor_only\":false,\"id\":\"mobile\",\"max_kbps\":900,\"max_layer\":1},{\"floor_only\":false,\"id\":\"tv\",\"max_layer\":2},{\"floor_only\":true,\"id\":\"legacy\",\"max_layer\":0}],\"classify\":{\"motion_high_milli\":9000,\"static_mad_milli\":300,\"static_permille\":5,\"synthetic_bins\":12},\"component\":[{\"id\":\"v\",\"media\":\"video\",\"source\":\"src/video.y4m\",\"timebase\":{\"den\":1,\"num\":25}},{\"id\":\"a\",\"media\":\"audio\",\"source\":\"src/audio.wav\",\"timebase\":{\"den\":1,\"num\":48000}}],\"defaults\":{\"deadline_ms\":1500},\"floor\":{\"extra_keyframes\":false,\"supplied_listing\":\"0000000000000000000000000000000000000000000000000000000000000000\"},\"ladder\":[{\"capability\":\"wai.video.int_motion\",\"component\":\"v\",\"id\":\"b0\",\"params\":{\"block\":16,\"max_sse_px_milli\":40000,\"mv_precision\":2,\"q_candidates\":[16,12,8],\"search\":7},\"role\":\"base\"},{\"capability\":\"wai.video.int_refine\",\"component\":\"v\",\"id\":\"r1\",\"params\":{\"q\":6},\"refines\":\"b0\",\"role\":\"refinement\"},{\"capability\":\"wai.video.int_refine\",\"component\":\"v\",\"id\":\"r2\",\"params\":{\"q\":3},\"refines\":\"r1\",\"role\":\"refinement\"},{\"capability\":\"wai.image.png\",\"component\":\"v\",\"id\":\"still\",\"params\":{},\"role\":\"base\"},{\"capability\":\"wai.video.av1\",\"component\":\"v\",\"id\":\"f0\",\"params\":{\"source\":\"supplied\"},\"role\":\"floor\"},{\"capability\":\"wai.audio.flac\",\"component\":\"a\",\"id\":\"ab\",\"params\":{\"source\":\"encode\"},\"role\":\"base\"}],\"lead\":\"v\",\"live\":{\"layer_lag_ms\":[0,200,400]},\"moment\":[{\"from_ms\":30000,\"id\":\"m-goal\",\"tags\":[\"event\"],\"to_ms\":34000}],\"royalty_free_only\":false,\"rule\":[{\"id\":\"static\",\"then\":{\"ladder\":[\"still\",\"f0\"]},\"when\":{\"piece_kind\":[\"static\"]}},{\"id\":\"motion\",\"then\":{\"max_layer\":1},\"when\":{\"piece_kind\":[\"motion_high\"]}},{\"id\":\"event\",\"then\":{\"deadline_ms\":800,\"max_layer\":2},\"when\":{\"moment_tag\":[\"event\"]}}],\"schema\":\"wai.pack.policy/1\",\"segmentation\":{\"closed\":true,\"cut_abs_permille\":300,\"cut_rel_permille\":3000,\"cut_window\":8,\"lookahead_frames\":6,\"max_ms\":4000,\"min_ms\":1000,\"scene_cut\":\"wai.pack.scenecut/1\",\"target_ms\":2000}}", - "digest": "53d2fb3b7ad5cfb211e86baaf4faec205b1332b0fe6875d9525c04ff021187bd", + "file": "eq-defaults-a.toml", + "canonical": "{\"asset_id\":\"r\",\"class\":[],\"classify\":{\"motion_high_milli\":9000,\"static_mad_milli\":300,\"static_permille\":5,\"synthetic_bins\":12},\"component\":[{\"id\":\"v\",\"media\":\"video\",\"timebase\":{\"den\":1,\"num\":25}}],\"defaults\":{\"deadline_ms\":1500},\"floor\":{\"extra_keyframes\":false},\"ladder\":[{\"capability\":\"wai.video.int_motion\",\"component\":\"v\",\"id\":\"b0\",\"params\":{\"block\":16,\"max_sse_px_milli\":0,\"mv_precision\":2,\"q_candidates\":[8],\"search\":7},\"role\":\"base\"}],\"lead\":\"v\",\"live\":{\"layer_lag_ms\":[0]},\"moment\":[],\"royalty_free_only\":true,\"rule\":[],\"schema\":\"wai.pack.policy/1\",\"segmentation\":{\"closed\":true,\"cut_abs_permille\":300,\"cut_rel_permille\":3000,\"cut_window\":8,\"lookahead_frames\":6,\"max_ms\":4000,\"min_ms\":1000,\"scene_cut\":\"wai.pack.scenecut/1\",\"target_ms\":2000}}", + "digest": "0cd5b7f95eeed90c49e2592bac4f9a93bd2c8e9c1f5e2be61a4764ac3bbd3511", "overrides": { - "royalty_free_only": false + "royalty_free_only": true } } ], diff --git a/wai/pack-conformance/receipts/expected.json b/wai/pack-conformance/receipts/expected.json new file mode 100644 index 000000000..0ee9c2404 --- /dev/null +++ b/wai/pack-conformance/receipts/expected.json @@ -0,0 +1,12 @@ +{ + "cases": [ + { + "file": "v1-legacy.json", + "receipt_hash": "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b" + }, + { + "file": "v1-unmetered.json", + "receipt_hash": "2e9a0a221436ae3b7161b67c0d3215d8bed8f4c2f1a0b27ad786762120f71f31" + } + ] +} diff --git a/wai/pack-conformance/receipts/v1-legacy.json b/wai/pack-conformance/receipts/v1-legacy.json new file mode 100644 index 000000000..5900b0129 --- /dev/null +++ b/wai/pack-conformance/receipts/v1-legacy.json @@ -0,0 +1 @@ +{"asset_id":"asset-abc","energy_provenance":"HwShunt","grant":{"capability":"video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":12000,"kind":"package","parent_receipt_hash":null,"receipt_hash":"71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b","segment_map_root":"941704ebc6c0adc3394ebda28f50b3d6cf38cfc0fcd265a861d002365294829f","segments":[{"byte_len":16,"content_hash":"9f63cd63fcfb34a53f2113045123c0309bef7f8eb74dc1ccda2c17c1369558ab","duration_ms":2000,"index":0},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":1},{"byte_len":16,"content_hash":"864d8e31f5b44bde825e01963d73d6047b7acfae6ddaa64aa85ff0a47995e176","duration_ms":2000,"index":2},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":3}],"sig":"ad47c3af1dec7cac2d23259d6b389c923a897e82741f0748a0a437810a7c5a32dd30ad2c99970d53dafd0938af8a8e7d296d87931a95ac489b7b5eace40c7f00","signer_id":"did:key:pkg","signer_pubkey":"8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c","stored_bytes":48,"total_bytes":64,"total_segments":4,"unique_segments":3,"version":1,"work_segment_ops":4} \ No newline at end of file diff --git a/wai/pack-conformance/receipts/v1-unmetered.json b/wai/pack-conformance/receipts/v1-unmetered.json new file mode 100644 index 000000000..be6ea565a --- /dev/null +++ b/wai/pack-conformance/receipts/v1-unmetered.json @@ -0,0 +1 @@ +{"asset_id":"asset","energy_provenance":null,"grant":{"capability":"video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","parent_receipt_hash":"0303030303030303030303030303030303030303030303030303030303030303","receipt_hash":"2e9a0a221436ae3b7161b67c0d3215d8bed8f4c2f1a0b27ad786762120f71f31","segment_map_root":"941704ebc6c0adc3394ebda28f50b3d6cf38cfc0fcd265a861d002365294829f","segments":[{"byte_len":16,"content_hash":"9f63cd63fcfb34a53f2113045123c0309bef7f8eb74dc1ccda2c17c1369558ab","duration_ms":2000,"index":0},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":1},{"byte_len":16,"content_hash":"864d8e31f5b44bde825e01963d73d6047b7acfae6ddaa64aa85ff0a47995e176","duration_ms":2000,"index":2},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":3}],"sig":"97feb1bef70dff2abfa56a3a9968400ab446263f4bfc66db09ddbeb8c435eba4bf7d4136833acca27a4697f28ca4d2d25422aba26ad34a08ded1e4c6025f0804","signer_id":"o","signer_pubkey":"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394","stored_bytes":48,"total_bytes":64,"total_segments":4,"unique_segments":3,"version":1,"work_segment_ops":4} \ No newline at end of file diff --git a/wai/pack-conformance/requirements-verify.txt b/wai/pack-conformance/requirements-verify.txt new file mode 100644 index 000000000..7ad01bf82 --- /dev/null +++ b/wai/pack-conformance/requirements-verify.txt @@ -0,0 +1,170 @@ +# The Python packages pack_verify.py needs beyond the standard library: Ed25519 through +# `cryptography`, and what it depends on. Installed in CI with +# python -m pip install --require-hashes -r pack-conformance/requirements-verify.txt +# so every file pip fetches is the one these SHA-256 digests name (every distribution +# file of each pinned release, from the package index's own listing). + +cryptography==50.0.2 \ + --hash=sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602 \ + --hash=sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2 \ + --hash=sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047 \ + --hash=sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c \ + --hash=sha256:1ba34f04897fcdaa73f74145c25f3ec146fbd56593853e88adc2e811303c5f42 \ + --hash=sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18 \ + --hash=sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51 \ + --hash=sha256:3dc4fd8058cea1644971207d530e1a03a184a805ffc8ebdddf0599d78a331b81 \ + --hash=sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856 \ + --hash=sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2 \ + --hash=sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de \ + --hash=sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7 \ + --hash=sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd \ + --hash=sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2 \ + --hash=sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be \ + --hash=sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45 \ + --hash=sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0 \ + --hash=sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e \ + --hash=sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c \ + --hash=sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5 \ + --hash=sha256:7b75de3c8b3be1cdb1052747c929440c3eea46c1bc2cb8a6e3a48388e9b7b452 \ + --hash=sha256:7c6d0330c472d96f6a6afe24d80dfdf15176c33096f0a4397ae4c60f3dd3be48 \ + --hash=sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05 \ + --hash=sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1 \ + --hash=sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93 \ + --hash=sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04 \ + --hash=sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e \ + --hash=sha256:92e665960f25fcdc73725b9cec7a3824f279ba97a98653afe9ffac2e43668f67 \ + --hash=sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7 \ + --hash=sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107 \ + --hash=sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079 \ + --hash=sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134 \ + --hash=sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227 \ + --hash=sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1 \ + --hash=sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539 \ + --hash=sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e \ + --hash=sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d \ + --hash=sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c \ + --hash=sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd \ + --hash=sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020 \ + --hash=sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd \ + --hash=sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94 \ + --hash=sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a \ + --hash=sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408 \ + --hash=sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37 \ + --hash=sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e \ + --hash=sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454 \ + --hash=sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c \ + --hash=sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc \ + --hash=sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37 \ + --hash=sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767 \ + --hash=sha256:eef4c2f3423810b3070ab391f85436d2f8bbfcb286ac15cbc73190b3563b1f1a \ + --hash=sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5 \ + --hash=sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc \ + --hash=sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67 \ + --hash=sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8 \ + --hash=sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480 \ + --hash=sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb \ + --hash=sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b +cffi==2.1.1 ; platform_python_implementation != "PyPy" \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 +pycparser==3.0 ; implementation_name != "PyPy" \ + --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ + --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index 20182f378..630c1f138 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -1553,6 +1553,13 @@ "section": "§6.2", "status": "defined" }, + { + "name": "WSIX", + "kind": "magic", + "document": "extensions/packaging.md", + "section": "§6", + "status": "defined" + }, { "name": "X-WAI-FLOOR", "kind": "playlist-attribute", @@ -1581,6 +1588,13 @@ "section": "§8.1", "status": "defined" }, + { + "name": "science.transaction.wai.class", + "kind": "data-id", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, { "name": "science.transaction.wai.multivariant", "kind": "data-id", @@ -1588,6 +1602,20 @@ "section": "§11.1", "status": "defined" }, + { + "name": "science.transaction.wai.stages", + "kind": "data-id", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, + { + "name": "tag:transaction.science,2026:wai/class", + "kind": "scheme", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, { "name": "tag:transaction.science,2026:wai/floor", "kind": "scheme", @@ -1602,6 +1630,13 @@ "section": "§11.1", "status": "defined" }, + { + "name": "tag:transaction.science,2026:wai/stages", + "kind": "scheme", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, { "name": "wai", "kind": "catalog", @@ -1679,6 +1714,13 @@ "section": "§4", "status": "defined" }, + { + "name": "wai_delivery_timeout_ms", + "kind": "catalog", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, { "name": "wai_determinism", "kind": "catalog", @@ -1686,6 +1728,13 @@ "section": "§4", "status": "defined" }, + { + "name": "wai_layer", + "kind": "catalog", + "document": "extensions/packaging.md", + "section": "§8", + "status": "defined" + }, { "name": "wai_receipts", "kind": "catalog", diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index bbccb4b60..e0160c91d 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -8,6 +8,18 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bitflags" version = "2.13.2" @@ -70,6 +82,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -104,6 +122,43 @@ dependencies = [ "typenum", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "digest" version = "0.10.7" @@ -114,6 +169,30 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -136,6 +215,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.14" @@ -152,6 +237,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -215,12 +311,50 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.34" @@ -251,6 +385,24 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "1.1.5" @@ -264,6 +416,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -343,6 +501,31 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "rand_core", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -372,7 +555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys", @@ -475,10 +658,14 @@ version = "0.3.0" dependencies = [ "blake3", "cbindgen", + "curve25519-dalek", + "ed25519-dalek", + "num-bigint", "pkg-config", "serde", "serde_json", "sha2", + "wai-quantum", ] [[package]] @@ -486,12 +673,28 @@ name = "wai-pack" version = "0.1.0" dependencies = [ "blake3", + "ed25519-dalek", "serde_json", "sha2", "toml 0.8.23", "wai", ] +[[package]] +name = "wai-quantum" +version = "0.3.36" +dependencies = [ + "blake3", + "ed25519-dalek", + "serde_json", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "windows-link" version = "0.2.1" @@ -522,6 +725,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zmij" version = "1.0.23" diff --git a/wai/wai-pack/Cargo.toml b/wai/wai-pack/Cargo.toml index 6a96e5a27..e9d59370f 100644 --- a/wai/wai-pack/Cargo.toml +++ b/wai/wai-pack/Cargo.toml @@ -19,9 +19,13 @@ name = "wai-pack" path = "src/bin/wai_pack.rs" [dependencies] -# The standard's crate: canonical JSON and the capability registry (ungated), and the -# integer video encoder and decoder (video_int). No codec library is linked. -wai = { path = "../wai-rs", default-features = false, features = ["video_int"] } +# The standard's crate: canonical JSON and the capability registry (ungated); the +# integer video encoder and decoder (video_int); the stage index and the package receipt +# (video_package); HTTP delivery's codecs union and floor (http); staged measurement's +# counted bytes and output digest (stage_measure); the measurement claim and its figure +# view (receipt_claims); and the model meter (energy_meter). No codec library is linked. +wai = { path = "../wai-rs", default-features = false, features = ["video_int", "video_package", "http", "stage_measure", "receipt_claims", "energy_meter"] } +ed25519-dalek = "2" # TOML 1.0 (the 0.8 line; later lines read TOML 1.1, which a policy must not use). toml = { version = "0.8", default-features = false, features = ["parse"] } serde_json = "1" diff --git a/wai/wai-pack/src/av1.rs b/wai/wai-pack/src/av1.rs new file mode 100644 index 000000000..2b13e87fd --- /dev/null +++ b/wai/wai-pack/src/av1.rs @@ -0,0 +1,419 @@ +//! The parts of an AV1 bitstream ([AV1] §5, §7.5, Annex B is not used) a packager reads +//! from a supplied floor: the low-overhead OBU format split into temporal units, which +//! temporal units start at a shown key frame, the sequence header's fields that the +//! ISOBMFF sample entry and the codecs string carry, and the codecs string itself. +//! +//! Nothing here decodes a picture. Every read is bounds-checked and integer. +//! +//! [AV1]: the AV1 Bitstream & Decoding Process Specification + +/// OBU types ([AV1] §6.2.2). +pub const OBU_SEQUENCE_HEADER: u8 = 1; +pub const OBU_TEMPORAL_DELIMITER: u8 = 2; +pub const OBU_FRAME_HEADER: u8 = 3; +pub const OBU_FRAME: u8 = 6; + +/// Why a supplied AV1 stream is refused: `floor-av1-malformed`, with what failed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Av1Error(pub String); + +impl std::fmt::Display for Av1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "floor-av1-malformed: {}", self.0) + } +} + +impl std::error::Error for Av1Error {} + +fn bad(why: impl Into) -> Result { + Err(Av1Error(why.into())) +} + +/// One OBU: its type, its header's length (1, or 2 with an extension), and its bytes +/// (header, size field and payload). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Obu<'a> { + pub obu_type: u8, + pub bytes: &'a [u8], + pub payload: &'a [u8], +} + +/// `leb128()` ([AV1] §4.10.5): at most 8 bytes, a value below `2^32`. +fn leb128(b: &[u8]) -> Result<(u64, usize), Av1Error> { + let mut v = 0u64; + for i in 0..8 { + let byte = *b.get(i).ok_or_else(|| Av1Error("a size field runs past the end".into()))?; + v |= ((byte & 0x7f) as u64) << (7 * i); + if byte & 0x80 == 0 { + if v > u32::MAX as u64 { + return bad("a size field past 2^32 - 1"); + } + return Ok((v, i + 1)); + } + } + bad("a size field longer than 8 bytes") +} + +/// Split a low-overhead OBU stream ([AV1] §5.2) into OBUs. Every OBU carries its size +/// field (`obu_has_size_field` = 1), as the format requires; the forbidden bit is 0. +pub fn obus(b: &[u8]) -> Result>, Av1Error> { + let mut out = Vec::new(); + let mut at = 0usize; + while at < b.len() { + let h = b[at]; + if h & 0x80 != 0 { + return bad(format!("the forbidden bit is set at byte {at}")); + } + let ext = h & 0x04 != 0; + if h & 0x02 == 0 { + return bad(format!("an OBU at byte {at} has no size field")); + } + let hl = if ext { 2 } else { 1 }; + if at + hl > b.len() { + return bad("an OBU header runs past the end"); + } + let (size, sl) = leb128(&b[at + hl..])?; + let start = at + hl + sl; + let end = start.checked_add(size as usize).filter(|&e| e <= b.len()).ok_or_else(|| Av1Error(format!("the OBU at byte {at} runs past the end")))?; + out.push(Obu { obu_type: (h >> 3) & 0x0f, bytes: &b[at..end], payload: &b[start..end] }); + at = end; + } + Ok(out) +} + +/// A temporal unit: its OBUs, from its temporal delimiter up to the next. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemporalUnit<'a> { + pub obus: Vec>, +} + +impl TemporalUnit<'_> { + /// The unit's bytes without its temporal delimiter: an ISOBMFF sample ([AV1-ISOBMFF] + /// §2.4). + pub fn sample(&self) -> Vec { + self.obus.iter().filter(|o| o.obu_type != OBU_TEMPORAL_DELIMITER).flat_map(|o| o.bytes.iter().copied()).collect() + } + + /// The unit's bytes as they appear in the stream, temporal delimiter included. + pub fn bytes(&self) -> Vec { + self.obus.iter().flat_map(|o| o.bytes.iter().copied()).collect() + } + + /// The unit's sequence header OBU, if it carries one. + pub fn sequence_header(&self) -> Option<&Obu<'_>> { + self.obus.iter().find(|o| o.obu_type == OBU_SEQUENCE_HEADER) + } +} + +/// Split a stream into temporal units ([AV1] §7.5): it begins with a temporal delimiter, +/// and each delimiter starts a unit. A unit holds at least one OBU beside its delimiter. +pub fn temporal_units(b: &[u8]) -> Result>, Av1Error> { + let mut out: Vec = Vec::new(); + for o in obus(b)? { + if o.obu_type == OBU_TEMPORAL_DELIMITER { + if !o.payload.is_empty() { + return bad("a temporal delimiter with a payload"); + } + out.push(TemporalUnit { obus: vec![o] }); + } else { + match out.last_mut() { + Some(tu) => tu.obus.push(o), + None => return bad("the stream does not begin with a temporal delimiter"), + } + } + } + if out.iter().any(|t| t.obus.len() < 2) { + return bad("a temporal unit holds only its delimiter"); + } + if out.is_empty() { + return bad("no temporal unit"); + } + Ok(out) +} + +struct Bits<'a> { + b: &'a [u8], + at: usize, +} + +impl Bits<'_> { + fn f(&mut self, n: u32) -> Result { + let mut v = 0u32; + for _ in 0..n { + let byte = *self.b.get(self.at >> 3).ok_or_else(|| Av1Error("a header runs past its OBU".into()))?; + v = (v << 1) | ((byte >> (7 - (self.at & 7))) & 1) as u32; + self.at += 1; + } + Ok(v) + } + + fn flag(&mut self) -> Result { + Ok(self.f(1)? == 1) + } + + /// `uvlc()` ([AV1] §4.10.3). + fn uvlc(&mut self) -> Result { + let mut lz = 0u32; + while !self.flag()? { + lz += 1; + if lz > 32 { + return bad("a uvlc() past 32 leading zeros"); + } + } + if lz >= 32 { + return Ok(u32::MAX); + } + Ok(self.f(lz)? + ((1u32 << lz) - 1)) + } +} + +/// The fields of a sequence header ([AV1] §5.5) a sample entry and a codecs string carry. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SequenceHeader { + pub seq_profile: u8, + pub still_picture: bool, + pub reduced_still_picture_header: bool, + pub seq_level_idx_0: u8, + pub seq_tier_0: u8, + pub max_frame_width: u32, + pub max_frame_height: u32, + pub high_bitdepth: bool, + pub twelve_bit: bool, + pub mono_chrome: bool, + pub chroma_subsampling_x: bool, + pub chroma_subsampling_y: bool, + pub chroma_sample_position: u8, +} + +impl SequenceHeader { + /// 8, 10 or 12. + pub fn bit_depth(&self) -> u8 { + match (self.high_bitdepth, self.twelve_bit) { + (false, _) => 8, + (true, false) => 10, + (true, true) => 12, + } + } + + /// Read a sequence header OBU's payload through `color_config()`. + pub fn parse(payload: &[u8]) -> Result { + let mut r = Bits { b: payload, at: 0 }; + let seq_profile = r.f(3)? as u8; + if seq_profile > 2 { + return bad(format!("seq_profile {seq_profile}")); + } + let still_picture = r.flag()?; + let reduced = r.flag()?; + let (seq_level_idx_0, seq_tier_0); + if reduced { + seq_level_idx_0 = r.f(5)? as u8; + seq_tier_0 = 0; + } else { + let timing_info_present = r.flag()?; + let mut decoder_model_info_present = false; + let mut buffer_delay_length = 0; + if timing_info_present { + r.f(32)?; + r.f(32)?; + if r.flag()? { + r.uvlc()?; + } + decoder_model_info_present = r.flag()?; + if decoder_model_info_present { + buffer_delay_length = r.f(5)? + 1; + r.f(32)?; + r.f(5)?; + r.f(5)?; + } + } + let initial_display_delay_present = r.flag()?; + let cnt = r.f(5)? + 1; + let (mut lvl0, mut tier0) = (0u8, 0u8); + for i in 0..cnt { + r.f(12)?; + let lvl = r.f(5)? as u8; + let tier = if lvl > 7 { r.f(1)? as u8 } else { 0 }; + if decoder_model_info_present && r.flag()? { + r.f(buffer_delay_length)?; + r.f(buffer_delay_length)?; + r.f(1)?; + } + if initial_display_delay_present && r.flag()? { + r.f(4)?; + } + if i == 0 { + (lvl0, tier0) = (lvl, tier); + } + } + seq_level_idx_0 = lvl0; + seq_tier_0 = tier0; + } + let wbits = r.f(4)? + 1; + let hbits = r.f(4)? + 1; + let max_frame_width = r.f(wbits)? + 1; + let max_frame_height = r.f(hbits)? + 1; + if !reduced && r.flag()? { + r.f(4)?; + r.f(3)?; + } + r.f(3)?; // use_128x128_superblock, enable_filter_intra, enable_intra_edge_filter + if !reduced { + r.f(4)?; // interintra, masked compound, warped motion, dual filter + let order_hint = r.flag()?; + if order_hint { + r.f(2)?; // jnt_comp, ref_frame_mvs + } + let force_sct = if r.flag()? { 2 } else { r.f(1)? }; + if force_sct > 0 && !r.flag()? { + r.f(1)?; + } + if order_hint { + r.f(3)?; + } + } + r.f(3)?; // superres, cdef, restoration + // color_config() + let high_bitdepth = r.flag()?; + let twelve_bit = seq_profile == 2 && high_bitdepth && r.flag()?; + let mono_chrome = seq_profile != 1 && r.flag()?; + let (mut cp, mut tc, mut mc) = (2u32, 2u32, 2u32); + if r.flag()? { + cp = r.f(8)?; + tc = r.f(8)?; + mc = r.f(8)?; + } + let (ssx, ssy, mut csp); + csp = 0u8; + if mono_chrome { + r.f(1)?; + (ssx, ssy) = (true, true); + } else if cp == 1 && tc == 13 && mc == 0 { + (ssx, ssy) = (false, false); + } else { + r.f(1)?; // color_range + if seq_profile == 0 { + (ssx, ssy) = (true, true); + } else if seq_profile == 1 { + (ssx, ssy) = (false, false); + } else if high_bitdepth && twelve_bit { + let x = r.flag()?; + let y = if x { r.flag()? } else { false }; + (ssx, ssy) = (x, y); + } else { + (ssx, ssy) = (true, false); + } + if ssx && ssy { + csp = r.f(2)? as u8; + } + } + Ok(SequenceHeader { + seq_profile, + still_picture, + reduced_still_picture_header: reduced, + seq_level_idx_0, + seq_tier_0, + max_frame_width, + max_frame_height, + high_bitdepth, + twelve_bit, + mono_chrome, + chroma_subsampling_x: ssx, + chroma_subsampling_y: ssy, + chroma_sample_position: csp, + }) + } + + /// The codecs string ([AV1-ISOBMFF] Codecs Parameter String), its short form: + /// `av01...`. + pub fn codec_string(&self) -> String { + format!("av01.{}.{:02}{}.{:02}", self.seq_profile, self.seq_level_idx_0, if self.seq_tier_0 == 1 { 'H' } else { 'M' }, self.bit_depth()) + } + + /// The four bytes of an `AV1CodecConfigurationRecord` before its `configOBUs` + /// ([AV1-ISOBMFF] §2.3.3): marker and version 1, profile and level, the tier, depth, + /// monochrome and chroma fields, and no initial presentation delay. + pub fn av1c_head(&self) -> [u8; 4] { + let b = |x: bool| x as u8; + [ + 0x81, + (self.seq_profile << 5) | (self.seq_level_idx_0 & 0x1f), + (self.seq_tier_0 << 7) + | (b(self.high_bitdepth) << 6) + | (b(self.twelve_bit) << 5) + | (b(self.mono_chrome) << 4) + | (b(self.chroma_subsampling_x) << 3) + | (b(self.chroma_subsampling_y) << 2) + | (self.chroma_sample_position & 3), + 0, + ] + } +} + +/// Whether a temporal unit starts at a shown key frame ([AV1] §5.9.2): its first frame +/// header (of a frame header or frame OBU) has `frame_type` KEY_FRAME and `show_frame` +/// 1, or the sequence uses the reduced still-picture header. A unit that shows an +/// existing frame is not one. +pub fn starts_with_shown_key_frame(tu: &TemporalUnit<'_>, seq: &SequenceHeader) -> Result { + let fh = tu.obus.iter().find(|o| o.obu_type == OBU_FRAME_HEADER || o.obu_type == OBU_FRAME).ok_or_else(|| Av1Error("a temporal unit with no frame".into()))?; + if seq.reduced_still_picture_header { + return Ok(true); + } + let mut r = Bits { b: fh.payload, at: 0 }; + if r.flag()? { + return Ok(false); + } + let frame_type = r.f(2)?; + let show_frame = r.flag()?; + Ok(frame_type == 0 && show_frame) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn leb128_reads_and_bounds() { + assert_eq!(leb128(&[0x05]).unwrap(), (5, 1)); + assert_eq!(leb128(&[0x80, 0x01]).unwrap(), (128, 2)); + assert!(leb128(&[0x80]).is_err()); + assert!(leb128(&[0xff, 0xff, 0xff, 0xff, 0x7f]).is_err(), "past 2^32 - 1"); + assert!(leb128(&[0x80; 9]).is_err()); + } + + #[test] + fn a_stream_splits_into_temporal_units() { + // TD, seq hdr (2 bytes), frame (1 byte); TD, frame. + let s = [0x12, 0x00, 0x0a, 0x02, 0xaa, 0xbb, 0x32, 0x01, 0x10, 0x12, 0x00, 0x32, 0x01, 0x20]; + let tus = temporal_units(&s).unwrap(); + assert_eq!(tus.len(), 2); + assert_eq!(tus[0].sample(), vec![0x0a, 0x02, 0xaa, 0xbb, 0x32, 0x01, 0x10]); + assert_eq!(tus[1].bytes(), vec![0x12, 0x00, 0x32, 0x01, 0x20]); + assert!(tus[0].sequence_header().is_some() && tus[1].sequence_header().is_none()); + assert!(temporal_units(&s[2..]).is_err(), "no leading delimiter"); + assert!(temporal_units(&[0x12, 0x00, 0x12, 0x00, 0x32, 0x00]).is_err(), "a unit of a delimiter alone"); + assert!(obus(&[0x30, 0x10]).is_err(), "no size field"); + assert!(obus(&[0x92, 0x00]).is_err(), "the forbidden bit"); + assert!(obus(&[0x32, 0x05, 0x00]).is_err(), "runs past the end"); + } + + /// A reduced still-picture sequence header: profile 0, level 4, 64 × 36, 8-bit 4:2:0. + #[test] + fn a_sequence_header_gives_the_codecs_string_and_record() { + // seq_profile 000, still 1, reduced 1, level 00100, wbits-1 0101 (6 bits), hbits-1 0101, + // width-1 = 63 (111111), height-1 = 35 (100011), sb/filter/edge 000, + // superres/cdef/restoration 000, high_bitdepth 0, mono 0, color_description 0, + // color_range 0, chroma_sample_position 00, then padding. + let bits = "000" .to_string() + "1" + "1" + "00100" + "0101" + "0101" + "111111" + "100011" + "000" + "000" + "0" + "0" + "0" + "0" + "00"; + let mut bytes = vec![0u8; bits.len().div_ceil(8)]; + for (i, c) in bits.chars().enumerate() { + if c == '1' { + bytes[i / 8] |= 0x80 >> (i % 8); + } + } + let s = SequenceHeader::parse(&bytes).unwrap(); + assert_eq!((s.seq_profile, s.seq_level_idx_0, s.max_frame_width, s.max_frame_height, s.bit_depth()), (0, 4, 64, 36, 8)); + assert!(s.reduced_still_picture_header && s.chroma_subsampling_x && s.chroma_subsampling_y); + assert_eq!(s.codec_string(), "av01.0.04M.08"); + assert_eq!(s.av1c_head(), [0x81, 0x04, 0x0c, 0x00]); + } +} diff --git a/wai/wai-pack/src/bin/wai_pack.rs b/wai/wai-pack/src/bin/wai_pack.rs index d434c9a57..6748a5b7c 100644 --- a/wai/wai-pack/src/bin/wai_pack.rs +++ b/wai/wai-pack/src/bin/wai_pack.rs @@ -6,7 +6,23 @@ //! rung whose capability a packager may not emit, prints its reason and exits 1 //! wai-pack plan --policy --component [--source ] [--out ] //! segment a video component with wai.pack.scenecut/1: one piece per line -//! (start, frames, flags, kind), then the plan digest +//! (start, frames, flags, kind), then the plan digest; --out writes the keyframe +//! plan, each piece's first frame one per line (packaging §7.3), for the encoder of a +//! supplied floor +//! wai-pack pack --policy --out --key [--source =]... +//! [--floor ] [--signer-id ] [--royalty-free-only] [--meter none|model:] +//! pack the policy's components into : objects, stage indexes, the floor's CMAF +//! files, catalogs, the receipt (and, metered, its measurement claim) and LISTING; +//! is new or empty (`pack-out-not-empty` otherwise, before any work), and a +//! refusal writes nothing and prints its reason. Sources default to each +//! component's `source`, and the floor to `floor/`, beside the policy; the signer id +//! defaults to the key's did:key. A meter that gives no figure (a pack too short to +//! reach a microjoule at , or a figure past 2^64 - 1) refuses the pack +//! (`pack-meter`); it never writes an unmetered package in its place +//! wai-pack verify +//! verify a package (packaging §9): every reason, or its counts and pack digest +//! wai-pack listing +//! the listing (SPEC §3.1) of every file of but LISTING //! wai-pack policy-verdict ... //! each policy's verdict (`ok`, or the reason a packager refuses it) and, when the //! policy itself parses, its digest (else `-`): `\t\t` @@ -38,6 +54,9 @@ fn usage() -> ExitCode { eprintln!( "usage:\n wai-pack policy [--royalty-free-only]\n \ wai-pack plan --policy --component [--source ] [--out ]\n \ + wai-pack pack --policy --out --key [--source =]... [--floor ] [--signer-id ] [--royalty-free-only] [--meter none|model:]\n \ + wai-pack verify \n \ + wai-pack listing \n \ wai-pack policy-verdict ...\n \ wai-pack source-verdict ...\n \ wai-pack fixture gen [-o ] | fixture list\n \ @@ -53,7 +72,7 @@ fn load_policy(path: &str) -> Result { fn cmd_policy(args: &[String]) -> Result<(), String> { let path = args.first().ok_or("policy: a policy file")?; - let o = Overrides { royalty_free_only: args.iter().any(|a| a == "--royalty-free-only").then_some(true) }; + let o = Overrides { royalty_free_only: args.iter().any(|a| a == "--royalty-free-only") }; let p = load_policy(path)?.effective(&o); println!("{}", p.canonical_json()); println!("{}", hex(&p.digest())); @@ -93,14 +112,92 @@ fn cmd_plan(args: &[String]) -> Result<(), String> { text.push_str(&format!("{} {} {} {}\n", x.start, x.len, x.flags, kind)); } let starts: Vec = pieces.iter().map(|x| x.start).collect(); - match opt(args, "--out") { - Some(o) => std::fs::write(o, &text).map_err(|e| format!("{o}: {e}"))?, - None => print!("{text}"), + print!("{text}"); + if let Some(o) = opt(args, "--out") { + std::fs::write(o, wai_pack::pack::plan_file(&starts)).map_err(|e| format!("{o}: {e}"))?; } println!("{}", hex(&scenecut::plan_digest(&starts))); Ok(()) } +fn read_key(path: &str) -> Result { + let t = std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?; + let t = t.trim(); + if t.len() != 64 || !t.bytes().all(|b| b.is_ascii_hexdigit()) { + return Err(format!("{path}: not a 32-byte seed in 64 hexadecimal digits")); + } + let mut seed = [0u8; 32]; + for (i, s) in seed.iter_mut().enumerate() { + *s = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).map_err(|e| e.to_string())?; + } + Ok(ed25519_dalek::SigningKey::from_bytes(&seed)) +} + +fn cmd_pack(args: &[String]) -> Result<(), String> { + let path = opt(args, "--policy").ok_or("pack: --policy")?; + let out = opt(args, "--out").ok_or("pack: --out")?; + wai_pack::pack::out_dir_ready(Path::new(out)).map_err(|e| e.to_string())?; + let key = read_key(opt(args, "--key").ok_or("pack: --key")?)?; + let bytes = std::fs::read(path).map_err(|e| format!("{path}: {e}"))?; + let base = Path::new(path).parent().unwrap_or(Path::new(".")); + let p = Policy::parse_bytes(&bytes).map_err(|e| e.to_string())?; + let mut sources = std::collections::BTreeMap::new(); + for c in &p.components { + let given = args.iter().zip(args.iter().skip(1)).filter(|(f, _)| *f == "--source").find_map(|(_, v)| v.strip_prefix(&format!("{}=", c.id))); + let file = given.map(std::path::PathBuf::from).or_else(|| c.source.as_ref().map(|s| base.join(s))); + if let Some(f) = file { + sources.insert(c.id.clone(), std::fs::read(&f).map_err(|e| format!("{}: {e}", f.display()))?); + } + } + let floor_dir = opt(args, "--floor").map(std::path::PathBuf::from).unwrap_or_else(|| base.join("floor")); + let floor = if floor_dir.is_dir() { wai::pin::read_dir_set(&floor_dir).map_err(|e| e.to_string())? } else { Default::default() }; + let signer_id = opt(args, "--signer-id").map(str::to_string).unwrap_or_else(|| wai::signer_id::did_key(&key.verifying_key().to_bytes())); + let mut o = wai_pack::pack::PackOpts::new(key, signer_id, floor); + o.overrides = Overrides { royalty_free_only: args.iter().any(|a| a == "--royalty-free-only") }; + match opt(args, "--meter").unwrap_or("none") { + "none" => {} + m => { + let mw: u64 = m.strip_prefix("model:").and_then(|x| x.parse().ok()).ok_or_else(|| format!("pack: --meter {m}: none or model:"))?; + o.meter = Some(Box::new(wai::energy_meter::ModelMeter::wall_clock(mw))); + o.power_reader = format!("model:{mw}mW"); + let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs()); + o.issued_at = wai::claim_receipt::rfc3339_utc(now); + } + } + let pk = wai_pack::pack::pack(&bytes, &sources, &mut o).map_err(|e| e.to_string())?; + pk.write(Path::new(out)).map_err(|e| format!("{out}: {e}"))?; + println!("receipt {}", hex(&pk.receipt.receipt_hash())); + println!("pack digest {}", pk.pack_digest()); + println!("entries {} ({} distinct, {} bytes referenced, {} stored)", pk.receipt.total_entries, pk.receipt.unique_objects, pk.receipt.total_bytes, pk.receipt.stored_bytes); + println!("energy {}", if pk.receipt.energy_provenance.is_some() { format!("{} uJ ({})", pk.receipt.joules_micro, pk.receipt.energy_provenance.as_ref().map(|p| p.label()).unwrap_or("")) } else { "not metered".into() }); + Ok(()) +} + +fn cmd_verify(args: &[String]) -> Result<(), String> { + let dir = args.first().ok_or("verify: a package directory")?; + match wai_pack::verify::verify_dir(Path::new(dir)) { + Ok(r) => { + println!("ok {dir}"); + println!("receipt {}", hex(&r.receipt_hash)); + println!("pack digest {}", r.pack_digest); + println!("entries {} ({} distinct, {} bytes referenced, {} stored)", r.total_entries, r.unique_objects, r.total_bytes, r.stored_bytes); + println!("energy {}", if r.metered { "metered, with its measurement claim" } else { "not metered" }); + Ok(()) + } + Err(v) => Err(v.iter().map(|r| r.to_string()).collect::>().join("\n")), + } +} + +fn cmd_listing(args: &[String]) -> Result<(), String> { + let dir = args.first().ok_or("listing: a directory")?; + let mut files = wai::pin::read_dir_set(Path::new(dir)).map_err(|e| e.to_string())?; + files.remove("LISTING"); + let view: std::collections::BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let l = wai::pin::listing_from(&view).map_err(|e| e.to_string())?; + use std::io::Write as _; + std::io::stdout().write_all(&l).map_err(|e| e.to_string()) +} + fn cmd_policy_verdict(args: &[String]) -> Result<(), String> { for p in args { let b = std::fs::read(p).map_err(|e| format!("{p}: {e}"))?; @@ -214,6 +311,9 @@ fn main() -> ExitCode { let r = match cmd.as_str() { "policy" => cmd_policy(rest), "plan" => cmd_plan(rest), + "pack" => cmd_pack(rest), + "verify" => cmd_verify(rest), + "listing" => cmd_listing(rest), "policy-verdict" => cmd_policy_verdict(rest), "source-verdict" => cmd_source_verdict(rest), "segment-json" => cmd_segment_json(), diff --git a/wai/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs new file mode 100644 index 000000000..870c71f08 --- /dev/null +++ b/wai/wai-pack/src/catalog.rs @@ -0,0 +1,579 @@ +//! A package's catalogs ([packaging] §8): what each class is offered, over HTTP (HLS and +//! DASH, in [http-delivery]'s Mode N) and MoQ, and the stage view a WAI sink reads. Every +//! file is a pure function of the stage index, the objects and the floor, so a verifier +//! renders each again and compares bytes. +//! +//! - `hls/classic/master.m3u8` and `hls/classic/floor-.m3u8`: the classical +//! presentation, the floor alone; it names nothing of WAI. +//! - `hls//master.m3u8`: the classical presentation with, for a class offered WAI +//! objects, the one line http-delivery §11.1 lets a publisher add, naming +//! `wai.m3u8`. +//! - `hls//wai.m3u8`: the class's WAI playlist (http-delivery §11.1): its class and +//! stage view as `EXT-X-SESSION-DATA`, and one variant whose `CODECS` is the union and +//! `X-WAI-FLOOR` the floor (http-delivery §4.2) of the objects it lists, with +//! `BANDWIDTH` the peak and `AVERAGE-BANDWIDTH` the mean of `8 · bytes / duration`. +//! - `hls//wai-.m3u8`: the WAI media playlist, adjacent to the floor's +//! (http-delivery §11.2): a piece's base as its segment, each layer the class lists as a +//! `#WAI-LAYER` line (http-delivery §10.4), and a piece the class takes as floor only +//! marked `#EXT-X-GAP`, so the sink takes the floor's segment there. +//! - `dash/classic.mpd` and `dash/.mpd`: the same as two MPDs (http-delivery +//! §11.1): the classical one, and the class's WAI MPD with `EssentialProperty` and the +//! floor in a `SupplementalProperty`, one Representation per layer, each layer's +//! depending on the one below (`dependencyId`), and a timeline with a gap where the +//! class lists the floor only. +//! - `moq/catalog.json`: one track per (component, layer, deadline) of packaging `wai`, a +//! refinement track depending on the track below, the floor a `cmaf` track sharing the +//! base track's `altGroup`. +//! - `stages/.json`: per piece, the stages the class lists. +//! +//! Durations: a piece of `d` units of `num/den` lasts `floor(d · den · 1000 / num)` +//! milliseconds, written `S.mmm`; a target duration is the longest piece in whole +//! seconds, rounded up. Rates: `ceil(8 · bytes · num / (d · den))` bits per second. +//! +//! [packaging]: ../../../extensions/packaging.md +//! [http-delivery]: ../../../extensions/http-delivery.md + +use crate::policy::Timebase; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::fmt::Write as _; +use wai::http_binding::codecs::{self, CodecsError, SeqObject}; +use wai::stage_index::{EntryRec, Role, StageIndex, FLOOR_ONLY, TIER_DECODE, TIER_ENTROPY}; + +/// The SESSION-DATA identifier of a class's id. +pub const DATA_ID_CLASS: &str = "science.transaction.wai.class"; +/// The SESSION-DATA identifier of a class's stage view. +pub const DATA_ID_STAGES: &str = "science.transaction.wai.stages"; +/// http-delivery §11.1's SESSION-DATA identifier of the WAI playlist. +pub const DATA_ID_MULTIVARIANT: &str = "science.transaction.wai.multivariant"; +/// http-delivery §11.1's DASH schemes. +pub const SCHEME_HTTP: &str = "tag:transaction.science,2026:wai/http-delivery"; +pub const SCHEME_FLOOR: &str = "tag:transaction.science,2026:wai/floor"; +/// The DASH schemes naming a class and its stage view. +pub const SCHEME_CLASS: &str = "tag:transaction.science,2026:wai/class"; +pub const SCHEME_STAGES: &str = "tag:transaction.science,2026:wai/stages"; + +/// Publisher priorities (§8.5): the base track's, and the step per layer. +pub const PRIORITY_BASE: u64 = 64; +pub const PRIORITY_STEP: u64 = 32; + +/// A component's floor as the catalogs name it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FloorView { + pub codec: String, + pub w: u32, + pub h: u32, + /// The initialization segment's path in the package. + pub init: String, + /// Each piece's fragment: its path and length. + pub fragments: Vec<(String, u64)>, +} + +/// A component as the catalogs read it. +pub struct CompView<'a> { + pub index: &'a StageIndex, + pub tb: Timebase, + pub w: u32, + pub h: u32, + /// Every object of the index by its content hash. + pub objects: &'a BTreeMap<[u8; 32], Vec>, + pub floor: Option, +} + +/// Why the catalogs cannot be written. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CatalogError(pub String); + +impl std::fmt::Display for CatalogError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl std::error::Error for CatalogError {} + +impl From for CatalogError { + fn from(e: CodecsError) -> Self { + CatalogError(e.to_string()) + } +} + +/// `floor(units · den · 1000 / num)` milliseconds as `S.mmm`. +pub fn seconds(units: u64, tb: Timebase) -> String { + let ms = crate::plan::units_to_ms(units, tb); + format!("{}.{:03}", ms / 1000, ms % 1000) +} + +fn target_duration(ix: &StageIndex, tb: Timebase) -> u64 { + ix.pieces.iter().map(|p| (p.duration as u128 * tb.den as u128).div_ceil(tb.num as u128) as u64).max().unwrap_or(0) +} + +/// `ceil(8 · bytes · num / (units · den))`: bits per second. +pub fn rate(bytes: u64, units: u64, tb: Timebase) -> u64 { + let d = units as u128 * tb.den as u128; + if d == 0 { + return 0; + } + u64::try_from((8 * bytes as u128 * tb.num as u128).div_ceil(d)).unwrap_or(u64::MAX) +} + +/// The peak and mean rate of a sequence of `(bytes, units)`. +fn rates(items: &[(u64, u64)], tb: Timebase) -> (u64, u64) { + let peak = items.iter().map(|&(b, d)| rate(b, d, tb)).max().unwrap_or(0); + let (b, d) = items.iter().fold((0u64, 0u64), |(b, d), &(x, y)| (b + x, d + y)); + (peak, rate(b, d, tb)) +} + +impl CompView<'_> { + fn id(&self) -> &str { + self.index.component() + } + + /// The entries class `c` lists for piece `p`: the bases and the refinements up to its + /// plan's layer; none for a floor-only piece. + pub fn listed(&self, c: usize, p: usize) -> Vec<&EntryRec> { + let v = self.index.plan[c][p]; + if v == FLOOR_ONLY { + return Vec::new(); + } + self.index.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement) && e.layer <= v).collect() + } + + fn seq_objects(&self, c: usize) -> Result, CatalogError> { + let mut out = Vec::new(); + let mut seen = std::collections::BTreeSet::new(); + for p in 0..self.index.pieces.len() { + for e in self.listed(c, p) { + if seen.insert(e.content_hash) { + let env = self.objects.get(&e.content_hash).ok_or_else(|| CatalogError(format!("object {} is not held", crate::hex(&e.content_hash))))?; + out.push(SeqObject::from_envelope(env).map_err(|r| CatalogError(r.to_string()))?); + } + } + } + Ok(out) + } + + /// The union and floor `codecs` elements of what class `c` lists, or `None` when it + /// lists nothing. + pub fn wai_codecs(&self, c: usize) -> Result, CatalogError> { + let objs = self.seq_objects(c)?; + if objs.is_empty() { + return Ok(None); + } + Ok(Some((codecs::render(&codecs::union_of(&objs)?), codecs::render(&codecs::floor_of(&objs)?)))) + } + + fn wai_rates(&self, c: usize) -> (u64, u64) { + let items: Vec<(u64, u64)> = (0..self.index.pieces.len()) + .filter(|&p| self.index.plan[c][p] != FLOOR_ONLY) + .map(|p| (self.listed(c, p).iter().map(|e| e.byte_len).sum(), self.index.pieces[p].duration as u64)) + .collect(); + rates(&items, self.tb) + } + + fn floor_rates(&self) -> (u64, u64) { + let f = self.floor.as_ref().expect("a floor"); + let items: Vec<(u64, u64)> = f.fragments.iter().zip(&self.index.pieces).map(|((_, len), p)| (*len, p.duration as u64)).collect(); + rates(&items, self.tb) + } + + fn class_id(&self, c: usize) -> &str { + self.index.class_name(&self.index.classes[c]) + } +} + +fn media_head(ix: &StageIndex, tb: Timebase) -> String { + format!("#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-TARGETDURATION:{}\n#EXT-X-MEDIA-SEQUENCE:0\n#EXT-X-PLAYLIST-TYPE:VOD\n#EXT-X-INDEPENDENT-SEGMENTS\n", target_duration(ix, tb)) +} + +/// `hls/classic/floor-.m3u8`. +pub fn render_floor_media(v: &CompView) -> String { + let f = v.floor.as_ref().expect("a floor"); + let mut o = media_head(v.index, v.tb); + let _ = writeln!(o, "#EXT-X-MAP:URI=\"../../{}\"", f.init); + for (p, (path, _)) in v.index.pieces.iter().zip(&f.fragments) { + let _ = writeln!(o, "#EXTINF:{},\n../../{path}", seconds(p.duration as u64, v.tb)); + } + o.push_str("#EXT-X-ENDLIST\n"); + o +} + +fn floor_variant(v: &CompView, uri: &str) -> String { + let f = v.floor.as_ref().expect("a floor"); + let (peak, mean) = v.floor_rates(); + format!("#EXT-X-STREAM-INF:BANDWIDTH={peak},AVERAGE-BANDWIDTH={mean},CODECS=\"{}\",RESOLUTION={}x{}\n{uri}\n", f.codec, f.w, f.h) +} + +const MASTER_HEAD: &str = "#EXTM3U\n#EXT-X-VERSION:7\n#EXT-X-INDEPENDENT-SEGMENTS\n"; + +/// `hls/classic/master.m3u8`. +pub fn render_classic_master(v: &CompView) -> String { + format!("{MASTER_HEAD}{}", floor_variant(v, &format!("floor-{}.m3u8", v.id()))) +} + +/// `hls//master.m3u8`: the classical presentation, and the line naming the class's +/// WAI playlist when it has one. +pub fn render_class_master(v: &CompView, c: usize) -> Result { + let mut o = MASTER_HEAD.to_string(); + if v.wai_codecs(c)?.is_some() { + let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_MULTIVARIANT}\",VALUE=\"wai.m3u8\""); + } + o.push_str(&floor_variant(v, &format!("../classic/floor-{}.m3u8", v.id()))); + Ok(o) +} + +/// `hls//wai.m3u8`, or `None` when the class lists no WAI object. +pub fn render_wai_multivariant(v: &CompView, c: usize) -> Result, CatalogError> { + let Some((union, floor)) = v.wai_codecs(c)? else { return Ok(None) }; + let (peak, mean) = v.wai_rates(c); + let class = v.class_id(c); + let mut o = MASTER_HEAD.to_string(); + let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_CLASS}\",VALUE=\"{class}\""); + let _ = writeln!(o, "#EXT-X-SESSION-DATA:DATA-ID=\"{DATA_ID_STAGES}\",URI=\"../../stages/{class}.json\""); + let _ = writeln!(o, "#EXT-X-STREAM-INF:BANDWIDTH={peak},AVERAGE-BANDWIDTH={mean},CODECS=\"{union}\",X-WAI-FLOOR=\"{floor}\",RESOLUTION={}x{}", v.w, v.h); + let _ = writeln!(o, "wai-{}.m3u8", v.id()); + Ok(Some(o)) +} + +fn object_uri(prefix: &str, e: &EntryRec) -> String { + format!("{prefix}objects/{}.wai#n={}", crate::hex(&e.content_hash), e.byte_len) +} + +/// `hls//wai-.m3u8`. +pub fn render_wai_media(v: &CompView, c: usize) -> String { + let mut o = media_head(v.index, v.tb); + for (p, piece) in v.index.pieces.iter().enumerate() { + let entries = v.index.piece_entries(p); + let base = entries.iter().find(|e| e.role == Role::Base).expect("a piece has a base"); + if v.index.plan[c][p] == FLOOR_ONLY { + o.push_str("#EXT-X-GAP\n"); + } + let _ = writeln!(o, "#EXTINF:{},\n{}", seconds(piece.duration as u64, v.tb), object_uri("../../", base)); + for e in v.listed(c, p).into_iter().filter(|e| e.role == Role::Refinement) { + let _ = writeln!(o, "#WAI-LAYER:N={},URI=\"{}\"", e.layer, object_uri("../../", e)); + } + } + o.push_str("#EXT-X-ENDLIST\n"); + o +} + +fn xml_head(duration: &str, min_buffer: u64) -> String { + format!( + "\n\n \n" + ) +} + +const XML_TAIL: &str = " \n\n"; + +fn total_units(ix: &StageIndex) -> u64 { + ix.pieces.iter().map(|p| p.duration as u64).sum() +} + +/// A `SegmentTimeline` over the pieces `ps`, each with its `t`. A time past `2^64 − 1` +/// has no timeline. +fn timeline(ix: &StageIndex, ps: &[usize], tb: Timebase, indent: &str) -> Result { + let mut o = format!("{indent}\n"); + for &p in ps { + let piece = &ix.pieces[p]; + let t = (piece.start - ix.pieces[0].start).checked_mul(tb.den as u64).ok_or_else(|| CatalogError("a segment time past 2^64 - 1".into()))?; + let _ = writeln!(o, "{indent} ", piece.duration as u64 * tb.den as u64); + } + let _ = writeln!(o, "{indent}"); + Ok(o) +} + +/// `dash/classic.mpd`. +pub fn render_dash_classic(v: &CompView) -> Result { + let f = v.floor.as_ref().expect("a floor"); + let (peak, _) = v.floor_rates(); + let mut o = xml_head(&seconds(total_units(v.index), v.tb), target_duration(v.index, v.tb)); + let _ = writeln!(o, " ", f.codec, f.w, f.h); + let _ = writeln!(o, " ", v.id()); + let _ = writeln!(o, " ", v.tb.num); + let _ = writeln!(o, " ", f.init); + let all: Vec = (0..v.index.pieces.len()).collect(); + o.push_str(&timeline(v.index, &all, v.tb, " ")?); + for (path, _) in &f.fragments { + let _ = writeln!(o, " "); + } + o.push_str(" \n \n \n"); + o.push_str(XML_TAIL); + Ok(o) +} + +/// `dash/.mpd`, or `None` when the class lists no WAI object. +pub fn render_dash_class(v: &CompView, c: usize) -> Result, CatalogError> { + let Some((union, floor)) = v.wai_codecs(c)? else { return Ok(None) }; + let class = v.class_id(c); + let mut o = xml_head(&seconds(total_units(v.index), v.tb), target_duration(v.index, v.tb)); + let _ = writeln!(o, " ", v.w, v.h); + let _ = writeln!(o, " "); + let _ = writeln!(o, " "); + let _ = writeln!(o, " "); + let _ = writeln!(o, " "); + let top = (0..v.index.pieces.len()).filter(|&p| v.index.plan[c][p] != FLOOR_ONLY).map(|p| v.index.plan[c][p]).max().unwrap_or(0); + for k in 0..=top { + let ps: Vec<(usize, &EntryRec)> = (0..v.index.pieces.len()) + .filter_map(|p| v.listed(c, p).into_iter().find(|e| e.layer == k && (k > 0 || e.role == Role::Base)).map(|e| (p, e))) + .collect(); + let (peak, _) = rates(&ps.iter().map(|(p, e)| (e.byte_len, v.index.pieces[*p].duration as u64)).collect::>(), v.tb); + let dep = if k > 0 { format!(" dependencyId=\"{}-l{}\"", v.id(), k - 1) } else { String::new() }; + let _ = writeln!(o, " ", v.id()); + let _ = writeln!(o, " ", v.tb.num); + o.push_str(&timeline(v.index, &ps.iter().map(|(p, _)| *p).collect::>(), v.tb, " ")?); + for (_, e) in &ps { + let _ = writeln!(o, " ", crate::hex(&e.content_hash)); + } + o.push_str(" \n \n"); + } + o.push_str(" \n"); + o.push_str(XML_TAIL); + Ok(Some(o)) +} + +fn tier_label(t: u8) -> Option<&'static str> { + match t { + TIER_DECODE => Some("decode-equivalence"), + TIER_ENTROPY => Some("entropy-consistency"), + _ => None, + } +} + +/// `moq/catalog.json`: RFC 8785 canonical. `layer_lag_ms[k]` is the policy's lag of layer +/// `k`, and `default_deadline` its `defaults.deadline_ms`. +/// +/// One track per (layer, deadline), by layer, then by deadline ascending; a refinement +/// track `depends` on every track that carries an entry its entries refine, in track +/// order: the layer below at the same deadline in a package whose layers share their +/// piece's deadline, and never a track the catalog does not hold. A subscriber receives +/// every track a track depends on (§8.4), so a dependency is all of them. +pub fn render_moq_catalog(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64) -> Result { + let id = v.id(); + let ix = v.index; + let staged = |e: &EntryRec| matches!(e.role, Role::Base | Role::Refinement); + // The tracks, in order, and each staged entry's track. + let mut keys: Vec<(u8, u32)> = ix.entries.iter().filter(|e| staged(e)).map(|e| (e.layer, e.deadline_ms)).collect(); + keys.sort_unstable(); + keys.dedup(); + let name_of = |(k, d): (u8, u32)| if k == 0 || d as u64 == default_deadline { format!("{id}/l{k}") } else { format!("{id}/l{k}@{d}") }; + let track_of = |e: &EntryRec| keys.iter().position(|&x| x == (e.layer, e.deadline_ms)); + let mut tracks: Vec = Vec::new(); + for (ti, &(k, d)) in keys.iter().enumerate() { + let on: Vec<&EntryRec> = ix.entries.iter().filter(|e| staged(e) && (e.layer, e.deadline_ms) == (k, d)).collect(); + let mut caps: Vec = Vec::new(); + for e in &on { + let c = ix.capability(e).to_string(); + if !caps.contains(&c) { + caps.push(c); + } + } + let weakest = on.iter().map(|e| e.prefix_tier).min().unwrap_or(0); + let mut t = json!({ + "name": name_of((k, d)), + "packaging": "wai", + "priority": PRIORITY_BASE + PRIORITY_STEP * k as u64, + "renderGroup": 1, + "wai_capabilities": caps, + "wai_layer": k, + "wai_receipts": false, + }); + if let Some(l) = tier_label(weakest) { + t["wai_determinism"] = json!(l); + } + if k == 0 { + if v.floor.is_some() { + t["altGroup"] = json!(1); + } + } else { + let mut deps: Vec = Vec::new(); + for e in &on { + let below = ix.entries.get(e.refines as usize).ok_or_else(|| CatalogError("a refinement refines no entry".into()))?; + let dt = track_of(below).filter(|&x| x < ti).ok_or_else(|| CatalogError("a refinement refines an entry on no earlier track".into()))?; + if !deps.contains(&dt) { + deps.push(dt); + } + } + deps.sort_unstable(); + t["depends"] = json!(deps.iter().map(|&x| name_of(keys[x])).collect::>()); + let lag = layer_lag_ms.get(k as usize).copied().unwrap_or(0); + t["wai_delivery_timeout_ms"] = json!((d as u64).saturating_sub(lag)); + } + tracks.push(t); + } + if let Some(f) = &v.floor { + tracks.push(json!({"altGroup": 1, "codec": f.codec, "height": f.h, "name": format!("{id}/floor"), "packaging": "cmaf", "renderGroup": 1, "width": f.w})); + } + wai::jcs::jcs(&json!({"tracks": tracks, "version": 1})).map(|s| s + "\n").map_err(|e| CatalogError(format!("moq/catalog.json: {e}"))) +} + +/// `stages/.json`: RFC 8785 canonical. A start, a piece number or a length past +/// `2^53 − 1` has no canonical form, so no stage view. +pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> Result { + let mut pieces = Vec::new(); + for (p, piece) in v.index.pieces.iter().enumerate() { + let stages: Vec = v + .listed(c, p) + .into_iter() + .map(|e| { + let mut s = json!({"bytes": e.byte_len, "h": crate::hex(&e.content_hash), "layer": e.layer}); + if e.role == Role::Refinement { + s["refines"] = json!(crate::hex(&v.index.entries[e.refines as usize].content_hash)); + s["deadline_ms"] = json!(e.deadline_ms); + } + s + }) + .collect(); + let number = v.index.header.first_piece.checked_add(p as u64).ok_or_else(|| CatalogError("a piece number past 2^64 - 1".into()))?; + let mut o = json!({"dur": piece.duration, "p": number, "stages": stages, "start": piece.start}); + if v.index.plan[c][p] == FLOOR_ONLY { + o["floor_only"] = json!(true); + } + pieces.push(o); + } + let class = v.class_id(c); + let mut comp = json!({"pieces": pieces}); + if (0..v.index.pieces.len()).any(|p| !v.listed(c, p).is_empty()) { + comp["media_playlist"] = json!(format!("../hls/{class}/wai-{}.m3u8", v.id())); + } + let mut components = serde_json::Map::new(); + components.insert(v.id().to_string(), comp); + wai::jcs::jcs(&json!({"class": class, "components": components, "policy_digest": crate::hex(policy_digest)})) + .map(|s| s + "\n") + .map_err(|e| CatalogError(format!("stages/{class}.json: {e}"))) +} + +/// Every catalog file of a one-component package, by path. +pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, policy_digest: &[u8; 32]) -> Result>, CatalogError> { + let mut out: BTreeMap> = BTreeMap::new(); + let id = v.id().to_string(); + if v.floor.is_none() { + return Err(CatalogError(format!("component {id} has no floor"))); + } + out.insert("hls/classic/master.m3u8".into(), render_classic_master(v).into_bytes()); + out.insert(format!("hls/classic/floor-{id}.m3u8"), render_floor_media(v).into_bytes()); + out.insert("dash/classic.mpd".into(), render_dash_classic(v)?.into_bytes()); + out.insert("moq/catalog.json".into(), render_moq_catalog(v, layer_lag_ms, default_deadline)?.into_bytes()); + for c in 0..v.index.classes.len() { + let class = v.class_id(c).to_string(); + out.insert(format!("hls/{class}/master.m3u8"), render_class_master(v, c)?.into_bytes()); + if let Some(m) = render_wai_multivariant(v, c)? { + out.insert(format!("hls/{class}/wai.m3u8"), m.into_bytes()); + out.insert(format!("hls/{class}/wai-{id}.m3u8"), render_wai_media(v, c).into_bytes()); + } + if let Some(d) = render_dash_class(v, c)? { + out.insert(format!("dash/{class}.mpd"), d.into_bytes()); + } + out.insert(format!("stages/{class}.json"), render_stage_view(v, c, policy_digest)?.into_bytes()); + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn view<'a>(ix: &'a StageIndex, objects: &'a BTreeMap<[u8; 32], Vec>) -> CompView<'a> { + CompView { + index: ix, + tb: Timebase { num: 25, den: 1 }, + w: 64, + h: 36, + objects, + floor: Some(FloorView { codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: vec![("floor/v/a.m4s".into(), 2000), ("floor/v/b.m4s".into(), 1000)] }), + } + } + + /// A class listing layers: the codecs union carries the refinement after `+`; each + /// listed layer is a `#WAI-LAYER` line http-delivery's parser reads, after its piece's + /// segment; the WAI and floor media playlists are adjacent; and the DASH, MoQ and + /// stage-view forms name the same layers. + #[test] + fn a_layered_class_is_listed_in_every_catalog() { + let (mut ix, objs) = crate::golden::layered(); + ix.classes.push(wai::stage_index::ClassRec { name: 1, max_layer: 1, flags: 0, max_kbps: 0 }); + ix.plan.push(vec![1, FLOOR_ONLY]); + ix.check().unwrap(); + let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (crate::objects::object_hash(o), o.clone())).collect(); + let v = view(&ix, &objects); + assert_eq!(v.wai_codecs(0).unwrap().unwrap(), ("wai1.video.int_motion+video.int_refine".to_string(), "wai1.video.int_motion+video.int_refine".to_string())); + let media = render_wai_media(&v, 0); + let layers: Vec<&str> = media.lines().filter(|l| l.starts_with("#WAI-LAYER")).collect(); + assert_eq!(layers.len(), 2); + for (i, l) in layers.iter().enumerate() { + let t = wai::http_binding::playlist::parse_layer_tag(l).unwrap(); + assert_eq!(t.n as usize, i + 1); + assert!(t.uri.starts_with("../../objects/") && t.uri.contains(".wai#n=")); + } + let m1 = render_wai_media(&v, 1); + assert_eq!(m1.lines().filter(|l| l.starts_with("#WAI-LAYER")).count(), 1, "class 1 lists layer 1 of piece 0 only"); + assert_eq!(m1.lines().filter(|l| *l == "#EXT-X-GAP").count(), 1, "and piece 1 as floor only"); + let extinf = |s: &str| s.lines().filter(|l| l.starts_with("#EXTINF") || l.starts_with("#EXT-X-TARGETDURATION") || l.starts_with("#EXT-X-MEDIA-SEQUENCE")).map(str::to_string).collect::>(); + assert_eq!(extinf(&media), extinf(&render_floor_media(&v)), "adjacent: the same sequence and durations to the millisecond"); + let dash = render_dash_class(&v, 0).unwrap().unwrap(); + assert!(dash.contains(" = moq["tracks"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert_eq!(names, vec!["v/l0", "v/l1", "v/l2", "v/floor"]); + assert_eq!(moq["tracks"][1]["depends"], serde_json::json!(["v/l0"])); + assert_eq!(moq["tracks"][1]["wai_delivery_timeout_ms"], 1300); + assert_eq!(moq["tracks"][2]["wai_delivery_timeout_ms"], 1100); + assert!(moq["tracks"][2]["priority"].as_u64() > moq["tracks"][1]["priority"].as_u64(), "a refinement after the track it depends on"); + let stages: serde_json::Value = serde_json::from_str(&render_stage_view(&v, 1, &[0; 32]).unwrap()).unwrap(); + let p0 = &stages["components"]["v"]["pieces"][0]["stages"]; + assert_eq!(p0.as_array().unwrap().len(), 2); + assert_eq!(p0[1]["refines"], serde_json::json!(crate::hex(&ix.entries[0].content_hash))); + assert_eq!(stages["components"]["v"]["pieces"][1]["floor_only"], true); + } + + /// §8.4: a refinement track depends on the tracks that carry what its entries refine, + /// and never names a track the catalog does not hold: layer 1 of piece 0 on its own + /// deadline track, and layer 2 on the default deadline refining it (the review's + /// multi-deadline input). + #[test] + fn a_dependency_never_dangles() { + let (mut ix, objs) = crate::golden::layered(); + ix.entries[1].deadline_ms = 800; + ix.check().unwrap(); + let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (crate::objects::object_hash(o), o.clone())).collect(); + let v = view(&ix, &objects); + let moq: serde_json::Value = serde_json::from_str(&render_moq_catalog(&v, &[0, 200, 400], 1500).unwrap()).unwrap(); + let tracks = moq["tracks"].as_array().unwrap(); + let names: Vec<&str> = tracks.iter().map(|t| t["name"].as_str().unwrap()).collect(); + assert_eq!(names, vec!["v/l0", "v/l1@800", "v/l2", "v/floor"]); + assert_eq!(tracks[1]["depends"], serde_json::json!(["v/l0"])); + assert_eq!(tracks[2]["depends"], serde_json::json!(["v/l1@800"])); + for t in tracks { + for d in t["depends"].as_array().into_iter().flatten() { + assert!(names.contains(&d.as_str().unwrap()), "{d} is a track of the catalog"); + } + } + assert_eq!(tracks[1]["wai_delivery_timeout_ms"], 600); + } + + /// A start past 2^53 − 1 has no stage view: the renderer refuses it and does not panic. + #[test] + fn a_start_past_the_canonical_range_is_refused() { + let (mut ix, objs) = crate::golden::layered(); + ix.header.prev_index_digest = [1; 32]; + let s = 1u64 << 53; + ix.pieces[0].start = s; + ix.pieces[1].start = s + 50; + ix.check().unwrap(); + let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (crate::objects::object_hash(o), o.clone())).collect(); + let v = view(&ix, &objects); + assert!(render_stage_view(&v, 0, &[0; 32]).is_err()); + assert!(render_all(&v, &[0, 200, 400], 1500, &[0; 32]).is_err()); + } + + #[test] + fn durations_and_rates_are_integers() { + let tb = Timebase { num: 25, den: 1 }; + assert_eq!(seconds(60, tb), "2.400"); + assert_eq!(seconds(1, Timebase { num: 30000, den: 1001 }), "0.033"); + assert_eq!(rate(1000, 25, tb), 8000); + assert_eq!(rate(1001, 25, tb), 8008); + assert_eq!(rate(1, 3, tb), 67, "rounded up"); + assert_eq!(rates(&[(1000, 25), (3000, 25)], tb), (24000, 16000)); + } +} diff --git a/wai/wai-pack/src/classify.rs b/wai/wai-pack/src/classify.rs index 42bb21f2f..4a7016988 100644 --- a/wai/wai-pack/src/classify.rs +++ b/wai/wai-pack/src/classify.rs @@ -23,6 +23,11 @@ pub enum PieceKind { } impl PieceKind { + /// The kind a stage index's `kind` code names, of the four a video piece has. + pub fn from_code(c: u8) -> Option { + [PieceKind::Natural, PieceKind::Static, PieceKind::Synthetic, PieceKind::MotionHigh].get(c as usize).copied() + } + /// The policy's spelling (`rule.when.piece_kind`). pub fn label(self) -> &'static str { match self { diff --git a/wai/wai-pack/src/cmaf.rs b/wai/wai-pack/src/cmaf.rs new file mode 100644 index 000000000..21de86ccc --- /dev/null +++ b/wai/wai-pack/src/cmaf.rs @@ -0,0 +1,210 @@ +//! The floor's CMAF files ([packaging] §7.6): one initialization segment per component +//! and one fragment per piece, each a pure function of the floor's samples, so a +//! verifier rebuilds them byte for byte from the floor objects. +//! +//! - `init.mp4`: `ftyp` (major brand `cmfc`; compatible `cmfc`, `iso6`, `av01`) and +//! `moov`: `mvhd`, one `trak` (`tkhd`, `mdia`: `mdhd`, `hdlr` `vide`, `minf`: `vmhd`, +//! `dinf`, `stbl` with an `av01` sample entry and its `av1C`, and empty sample tables) +//! and `mvex`/`trex`. Every time is 0 and the track is 1. +//! - a fragment: `moof` (`mfhd` with the sequence number, `traf`: `tfhd` with +//! default-base-is-moof, `tfdt` version 1, `trun` with each sample's duration, size and +//! flags) and `mdat`. +//! +//! Box layouts are ISO/IEC 14496-12's; the sample entry and its configuration record are +//! the AV1 ISOBMFF binding's. +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::av1::SequenceHeader; + +fn bx(kind: &[u8; 4], body: &[u8]) -> Vec { + let mut o = Vec::with_capacity(8 + body.len()); + o.extend_from_slice(&((8 + body.len()) as u32).to_be_bytes()); + o.extend_from_slice(kind); + o.extend_from_slice(body); + o +} + +fn full(kind: &[u8; 4], version: u8, flags: u32, body: &[u8]) -> Vec { + let mut b = Vec::with_capacity(4 + body.len()); + b.push(version); + b.extend_from_slice(&flags.to_be_bytes()[1..]); + b.extend_from_slice(body); + bx(kind, &b) +} + +const UNITY: [u32; 9] = [0x0001_0000, 0, 0, 0, 0x0001_0000, 0, 0, 0, 0x4000_0000]; + +fn matrix() -> Vec { + UNITY.iter().flat_map(|v| v.to_be_bytes()).collect() +} + +/// A sample's flags: a sync sample depends on no other (`sample_depends_on` 2); any +/// other depends on others and is not a sync sample. +pub const SYNC: u32 = 0x0200_0000; +pub const NON_SYNC: u32 = 0x0101_0000; + +/// One sample of a fragment. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Sample { + pub data: Vec, + pub duration: u32, + pub sync: bool, +} + +/// The initialization segment of an AV1 track of `w × h` at `timescale`, whose sample +/// entry's configuration record carries `seq_obu` (the sequence header OBU, whole). +pub fn write_init_av1(seq: &SequenceHeader, seq_obu: &[u8], w: u32, h: u32, timescale: u32) -> Vec { + let ftyp = bx(b"ftyp", &[b"cmfc".as_slice(), &0u32.to_be_bytes(), b"cmfc", b"iso6", b"av01"].concat()); + let mut mvhd = Vec::new(); + mvhd.extend_from_slice(&[0u8; 8]); // creation, modification + mvhd.extend_from_slice(×cale.to_be_bytes()); + mvhd.extend_from_slice(&0u32.to_be_bytes()); // duration + mvhd.extend_from_slice(&0x0001_0000u32.to_be_bytes()); // rate + mvhd.extend_from_slice(&0x0100u16.to_be_bytes()); // volume + mvhd.extend_from_slice(&[0u8; 10]); + mvhd.extend_from_slice(&matrix()); + mvhd.extend_from_slice(&[0u8; 24]); // pre_defined + mvhd.extend_from_slice(&2u32.to_be_bytes()); // next_track_ID + let mvhd = full(b"mvhd", 0, 0, &mvhd); + let mut tkhd = Vec::new(); + tkhd.extend_from_slice(&[0u8; 8]); + tkhd.extend_from_slice(&1u32.to_be_bytes()); // track_ID + tkhd.extend_from_slice(&[0u8; 4]); + tkhd.extend_from_slice(&0u32.to_be_bytes()); // duration + tkhd.extend_from_slice(&[0u8; 8]); + tkhd.extend_from_slice(&[0u8; 8]); // layer, alternate_group, volume, reserved + tkhd.extend_from_slice(&matrix()); + tkhd.extend_from_slice(&(w << 16).to_be_bytes()); + tkhd.extend_from_slice(&(h << 16).to_be_bytes()); + let tkhd = full(b"tkhd", 0, 3, &tkhd); + let mut mdhd = Vec::new(); + mdhd.extend_from_slice(&[0u8; 8]); + mdhd.extend_from_slice(×cale.to_be_bytes()); + mdhd.extend_from_slice(&0u32.to_be_bytes()); + mdhd.extend_from_slice(&0x55c4u16.to_be_bytes()); // "und" + mdhd.extend_from_slice(&0u16.to_be_bytes()); + let mdhd = full(b"mdhd", 0, 0, &mdhd); + let hdlr = full(b"hdlr", 0, 0, &[&[0u8; 4][..], b"vide", &[0u8; 12], b"video\0"].concat()); + let vmhd = full(b"vmhd", 0, 1, &[0u8; 8]); + let dref = full(b"dref", 0, 0, &[&1u32.to_be_bytes()[..], &full(b"url ", 0, 1, &[])].concat()); + let dinf = bx(b"dinf", &dref); + let av1c = bx(b"av1C", &[&seq.av1c_head()[..], seq_obu].concat()); + let mut entry = Vec::new(); + entry.extend_from_slice(&[0u8; 6]); + entry.extend_from_slice(&1u16.to_be_bytes()); // data_reference_index + entry.extend_from_slice(&[0u8; 16]); // pre_defined, reserved, pre_defined[3] + entry.extend_from_slice(&(w as u16).to_be_bytes()); + entry.extend_from_slice(&(h as u16).to_be_bytes()); + entry.extend_from_slice(&0x0048_0000u32.to_be_bytes()); + entry.extend_from_slice(&0x0048_0000u32.to_be_bytes()); + entry.extend_from_slice(&[0u8; 4]); + entry.extend_from_slice(&1u16.to_be_bytes()); // frame_count + entry.extend_from_slice(&[0u8; 32]); // compressorname + entry.extend_from_slice(&0x0018u16.to_be_bytes()); // depth + entry.extend_from_slice(&0xffffu16.to_be_bytes()); // pre_defined = -1 + entry.extend_from_slice(&av1c); + let stsd = full(b"stsd", 0, 0, &[&1u32.to_be_bytes()[..], &bx(b"av01", &entry)].concat()); + let empty = |k: &[u8; 4]| full(k, 0, 0, &0u32.to_be_bytes()); + let stsz = full(b"stsz", 0, 0, &[0u8; 8]); + let stbl = bx(b"stbl", &[stsd, empty(b"stts"), empty(b"stsc"), stsz, empty(b"stco")].concat()); + let minf = bx(b"minf", &[vmhd, dinf, stbl].concat()); + let mdia = bx(b"mdia", &[mdhd, hdlr, minf].concat()); + let trak = bx(b"trak", &[tkhd, mdia].concat()); + let trex = full(b"trex", 0, 0, &[&1u32.to_be_bytes()[..], &1u32.to_be_bytes(), &[0u8; 12]].concat()); + let mvex = bx(b"mvex", &trex); + let moov = bx(b"moov", &[mvhd, trak, mvex].concat()); + [ftyp, moov].concat() +} + +/// A fragment of track 1: sequence number `seq`, decode time `tfdt` in the track's +/// timescale, and its samples. +pub fn write_fragment(seq: u32, tfdt: u64, samples: &[Sample]) -> Vec { + let mfhd = full(b"mfhd", 0, 0, &seq.to_be_bytes()); + let tfhd = full(b"tfhd", 0, 0x02_0000, &1u32.to_be_bytes()); + let tfdt = full(b"tfdt", 1, 0, &tfdt.to_be_bytes()); + // trun: data offset, then per sample duration, size and flags. + let trun_len = 8 + 4 + 4 + 4 + 12 * samples.len(); + let traf_len = 8 + tfhd.len() + tfdt.len() + trun_len; + let moof_len = 8 + mfhd.len() + traf_len; + let data_offset = (moof_len + 8) as u32; + let mut trun = Vec::with_capacity(trun_len); + trun.extend_from_slice(&(samples.len() as u32).to_be_bytes()); + trun.extend_from_slice(&data_offset.to_be_bytes()); + for s in samples { + trun.extend_from_slice(&s.duration.to_be_bytes()); + trun.extend_from_slice(&(s.data.len() as u32).to_be_bytes()); + trun.extend_from_slice(&(if s.sync { SYNC } else { NON_SYNC }).to_be_bytes()); + } + let trun = full(b"trun", 0, 0x000701, &trun); + let traf = bx(b"traf", &[tfhd, tfdt, trun].concat()); + let moof = bx(b"moof", &[mfhd, traf].concat()); + debug_assert_eq!(moof.len(), moof_len); + let mdat = bx(b"mdat", &samples.iter().flat_map(|s| s.data.iter().copied()).collect::>()); + [moof, mdat].concat() +} + +/// The top-level boxes of a file, `(type, body)`, or `None` when the sizes do not tile +/// it exactly. +pub fn boxes(b: &[u8]) -> Option> { + let mut out = Vec::new(); + let mut at = 0usize; + while at < b.len() { + let size = u32::from_be_bytes(b.get(at..at + 4)?.try_into().ok()?) as usize; + if size < 8 || at + size > b.len() { + return None; + } + out.push((b[at + 4..at + 8].try_into().ok()?, &b[at + 8..at + size])); + at += size; + } + Some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn seq() -> SequenceHeader { + SequenceHeader { + seq_profile: 0, + still_picture: false, + reduced_still_picture_header: false, + seq_level_idx_0: 0, + seq_tier_0: 0, + max_frame_width: 64, + max_frame_height: 36, + high_bitdepth: false, + twelve_bit: false, + mono_chrome: false, + chroma_subsampling_x: true, + chroma_subsampling_y: true, + chroma_sample_position: 0, + } + } + + #[test] + fn the_init_segment_is_ftyp_and_moov_with_the_sample_entry() { + let init = write_init_av1(&seq(), &[0x0a, 0x01, 0x00], 64, 36, 25); + let top = boxes(&init).unwrap(); + assert_eq!(top.iter().map(|(k, _)| *k).collect::>(), vec![*b"ftyp", *b"moov"]); + let moov = boxes(top[1].1).unwrap(); + assert_eq!(moov.iter().map(|(k, _)| *k).collect::>(), vec![*b"mvhd", *b"trak", *b"mvex"]); + let at = init.windows(4).position(|w| w == b"av1C").unwrap(); + assert_eq!(&init[at + 4..at + 11], &[0x81, 0x00, 0x0c, 0x00, 0x0a, 0x01, 0x00]); + assert_eq!(write_init_av1(&seq(), &[0x0a, 0x01, 0x00], 64, 36, 25), init, "a pure function"); + } + + #[test] + fn a_fragment_points_trun_at_its_mdat() { + let s = vec![Sample { data: vec![1, 2, 3], duration: 1, sync: true }, Sample { data: vec![4, 5], duration: 1, sync: false }]; + let f = write_fragment(3, 50, &s); + let top = boxes(&f).unwrap(); + assert_eq!(top.iter().map(|(k, _)| *k).collect::>(), vec![*b"moof", *b"mdat"]); + assert_eq!(top[1].1, &[1, 2, 3, 4, 5]); + let at = f.windows(4).position(|w| w == b"trun").unwrap(); + let off = u32::from_be_bytes(f[at + 12..at + 16].try_into().unwrap()) as usize; + assert_eq!(&f[off..off + 5], &[1, 2, 3, 4, 5]); + let t = f.windows(4).position(|w| w == b"tfdt").unwrap(); + assert_eq!(u64::from_be_bytes(f[t + 8..t + 16].try_into().unwrap()), 50); + } +} diff --git a/wai/wai-pack/src/conformance.rs b/wai/wai-pack/src/conformance.rs index d0e6cc51a..aee17c136 100644 --- a/wai/wai-pack/src/conformance.rs +++ b/wai/wai-pack/src/conformance.rs @@ -21,6 +21,9 @@ //! - `qchoice/expected.json`: the step the base encode chooses for every piece of every //! fixture ([`crate::encode::choose_q`]): each candidate's SSE and its SSE per sample //! in thousandths, the step chosen, and the chosen payload's BLAKE3. +//! - `packs/`, `mutations/` and `receipts/`: the golden packages, the stage index, +//! object and receipt cases with their reasons, and version-1 receipts as JSON +//! ([`crate::golden`]). use crate::classify::{changed_permille, classify, mad_milli}; use crate::policy::{Overrides, Policy, Timebase}; @@ -62,12 +65,13 @@ fn tomls(dir: &Path) -> std::io::Result> { // ─────────────────────────────── policy ─────────────────────────────── -/// The overrides a policy case applies: `example.toml` is also digested with -/// `royalty_free_only` forced off. +/// The overrides a policy case applies: `eq-defaults-a.toml`, which leaves +/// `royalty_free_only` off, is also digested with it forced on (§3.2: an override only +/// forces it on). fn policy_cases(names: &[String]) -> Vec<(String, Overrides)> { let mut v: Vec<(String, Overrides)> = names.iter().map(|n| (n.clone(), Overrides::default())).collect(); - if names.iter().any(|n| n == "example.toml") { - v.push(("example.toml".into(), Overrides { royalty_free_only: Some(false) })); + if names.iter().any(|n| n == "eq-defaults-a.toml") { + v.push(("eq-defaults-a.toml".into(), Overrides { royalty_free_only: true })); } v } @@ -80,8 +84,8 @@ fn policy_expected(dir: &Path) -> std::io::Result { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{name}: {e}")))? .effective(&o); let mut c = json!({"file": name, "canonical": p.canonical_json(), "digest": hex(&p.digest())}); - if let Some(b) = o.royalty_free_only { - c["overrides"] = json!({"royalty_free_only": b}); + if o.royalty_free_only { + c["overrides"] = json!({"royalty_free_only": true}); } cases.push(c); } @@ -256,7 +260,7 @@ pub fn scenecut_expected(policy: &[u8]) -> Result { let c = p.component("v").ok_or("the scene-cut policy has no component v")?; let sp = SegParams::from_policy(&p.segmentation, c.timebase); let mut out = Vec::new(); - for name in fixture::NAMES { + for name in fixture::SCENECUT_NAMES { let bytes = fixture::generate(name).expect("a listed fixture"); let (hd, frames) = y4m::read_all(&bytes).map_err(|e| e.to_string())?; let (pieces, scores) = segment(sp, hd.w, hd.h, frames.iter().map(|f| &f.rgb)); @@ -308,7 +312,7 @@ pub fn encode_vectors(scenecut_policy: &[u8]) -> Result, Strin let c = p.component("v").ok_or("the scene-cut policy has no component v")?; let sp = SegParams::from_policy(&p.segmentation, c.timebase); let mut out = Vec::new(); - for name in fixture::NAMES { + for name in fixture::SCENECUT_NAMES { let bytes = fixture::generate(name).expect("a listed fixture"); let (hd, frames) = y4m::read_all(&bytes).map_err(|e| e.to_string())?; let (pieces, _) = segment(sp, hd.w, hd.h, frames.iter().map(|f| &f.rgb)); @@ -374,7 +378,7 @@ pub fn qchoice_expected(scenecut_policy: &[u8]) -> Result { let mut sets = Vec::new(); for mp in qchoice_params() { let mut out = Vec::new(); - for name in fixture::NAMES { + for name in fixture::SCENECUT_NAMES { let bytes = fixture::generate(name).expect("a listed fixture"); let (hd, frames) = y4m::read_all(&bytes).map_err(|e| e.to_string())?; let (pieces, _) = segment(sp, hd.w, hd.h, frames.iter().map(|f| &f.rgb)); @@ -431,7 +435,7 @@ pub fn regen(dir: &Path) -> std::io::Result<()> { std::fs::create_dir_all(dir.join("qchoice"))?; let qc = qchoice_expected(&text).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; write_json(&dir.join("qchoice/expected.json"), &qc)?; - Ok(()) + crate::golden::regen(dir) } fn same(what: &str, want: &Value, got: &Value, out: &mut Vec) { @@ -506,5 +510,6 @@ pub fn check(dir: &Path) -> std::io::Result> { Ok(qc) => same("qchoice/expected.json", &read_json(&dir.join("qchoice/expected.json"))?, &qc, &mut out), Err(e) => out.push(format!("qchoice: {e}")), } + out.extend(crate::golden::check(dir)?); Ok(out) } diff --git a/wai/wai-pack/src/fixture.rs b/wai/wai-pack/src/fixture.rs index a74514f2b..931aef49b 100644 --- a/wai/wai-pack/src/fixture.rs +++ b/wai/wai-pack/src/fixture.rs @@ -13,13 +13,18 @@ //! - `noise`: `Y = lo + noise mod (hi − lo + 1)`. //! - A fade of `n` frames from shot `a` to shot `b`, at its `i`-th frame (from 0), is //! `(a·(n − i − 1) + b·(i + 1)) / n` per sample, chroma included. +//! - A span marked *local* reads `t` as the frame's index within the span, so the span +//! is the same frames wherever it appears: a bumper played twice. //! //! `noise(seed, x, y, t) = mix(seed ^ mix(x ^ mix(y ^ mix(t))))`, where `mix` is the //! 32-bit finaliser `v ^= v >> 16; v *= 0x7feb352d; v ^= v >> 15; v *= 0x846ca68b; //! v ^= v >> 16` (wrapping). Chroma is constant per shot. /// The fixtures this module generates. -pub const NAMES: &[&str] = &["cuts-01", "kinds-01"]; +pub const NAMES: &[&str] = &["cuts-01", "kinds-01", "video-base-01"]; + +/// The fixtures the scene-cut, encode and step-choice corpora cover. +pub const SCENECUT_NAMES: &[&str] = &["cuts-01", "kinds-01"]; fn mix(mut v: u32) -> u32 { v ^= v >> 16; @@ -65,6 +70,7 @@ struct Span { frames: u32, shot: Shot, fade_to: Option, + local: bool, } #[allow(clippy::too_many_arguments)] @@ -73,7 +79,8 @@ const fn ramp(lo: u32, hi: u32, dx: u32, dy: u32, speed: u32, jitter: bool, seed } fn spans(name: &str) -> Option<(u32, u32, Vec)> { - let s = |frames, shot| Span { frames, shot, fade_to: None }; + let s = |frames, shot| Span { frames, shot, fade_to: None, local: false }; + let local = |frames, shot| Span { frames, shot, fade_to: None, local: true }; Some(match name { // Hard cuts; a flash of exactly 6 frames (the default lookahead) inside shot B; // a 40-frame fade from C to D; and two shots cut against each other, E and F. @@ -87,7 +94,7 @@ fn spans(name: &str) -> Option<(u32, u32, Vec)> { let f = ramp(140, 200, 1, 0, 1, true, 7, 160, 110); (32, 18, vec![ s(60, a), s(30, b), s(6, flash), s(44, b), s(20, c), - Span { frames: 40, shot: c, fade_to: Some(d) }, + Span { frames: 40, shot: c, fade_to: Some(d), local: false }, s(30, d), s(30, e), s(30, f), s(30, e), s(30, f), ]) } @@ -100,6 +107,18 @@ fn spans(name: &str) -> Option<(u32, u32, Vec)> { s(40, ramp(16, 80, 1, 1, 1, true, 8, 128, 128)), s(130, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)), ]), + // The base-only golden: three shots with a one-second bumper between them, the + // same frames both times, so its pieces are one object. + "video-base-01" => { + let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 }; + (64, 36, vec![ + s(60, ramp(20, 200, 1, 1, 1, true, 11, 110, 140)), + local(25, bumper), + s(50, ramp(30, 220, 2, 1, 1, true, 12, 150, 120)), + local(25, bumper), + s(40, ramp(16, 160, 1, 2, 1, true, 13, 128, 128)), + ]) + } _ => return None, }) } @@ -114,10 +133,11 @@ pub fn generate(name: &str) -> Option> { for i in 0..span.frames { out.extend_from_slice(b"FRAME\n"); let blend = |a: u32, b: u32| (a * (span.frames - i - 1) + b * (i + 1)) / span.frames; + let st = if span.local { i } else { t }; for y in 0..h { for x in 0..w { - let v = span.shot.y(x, y, t); - let v = span.fade_to.map_or(v, |to| blend(v, to.y(x, y, t))); + let v = span.shot.y(x, y, st); + let v = span.fade_to.map_or(v, |to| blend(v, to.y(x, y, st))); out.push(v as u8); } } @@ -153,9 +173,14 @@ mod tests { for n in NAMES { let b = generate(n).unwrap(); let (hd, frames) = crate::source::y4m::read_all(&b).unwrap(); - assert_eq!((hd.w, hd.h, hd.rate), (32, 18, Some((25, 1))), "{n}"); - assert!(frames.len() > 200, "{n}"); + let dims = if *n == "video-base-01" { (64, 36) } else { (32, 18) }; + assert_eq!((hd.w, hd.h, hd.rate), (dims.0, dims.1, Some((25, 1))), "{n}"); + assert!(frames.len() >= 200, "{n}"); } + // The bumper is the same frames both times. + let (_, f) = crate::source::y4m::read_all(&generate("video-base-01").unwrap()).unwrap(); + assert_eq!(f[60..85], f[135..160]); + assert_ne!(f[60..85], f[85..110]); assert!(generate("nope").is_none()); } } diff --git a/wai/wai-pack/src/floor.rs b/wai/wai-pack/src/floor.rs new file mode 100644 index 000000000..b47c1b180 --- /dev/null +++ b/wai/wai-pack/src/floor.rs @@ -0,0 +1,143 @@ +//! A supplied floor ([packaging] §7.3): a directory of AV1 streams, one per video +//! component, named `.obu` in the low-overhead OBU format, whose listing +//! (SPEC §3.1) the policy pins by SHA-256 (`floor.supplied_listing`). The floor is checked +//! against the plan before anything is written: +//! +//! - the directory's listing hashes to `floor.supplied_listing` (`floor-listing-mismatch`); +//! - the component's stream is in it (`floor-stream-missing`) and parses +//! (`floor-av1-malformed`): temporal units, a sequence header in every unit that starts +//! at a key frame, every sequence header the same; +//! - it has one temporal unit per source frame (`floor-frame-count`); +//! - a unit starts at a shown key frame at every piece start of the plan, and at no other +//! frame unless `floor.extra_keyframes` is true (`floor-keyframe-mismatch`). +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::av1::{self, SequenceHeader}; +use std::collections::BTreeMap; + +/// Why a supplied floor is refused. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FloorReason { + pub code: &'static str, + pub detail: String, +} + +impl std::fmt::Display for FloorReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.detail) + } +} + +impl std::error::Error for FloorReason {} + +fn refuse(code: &'static str, detail: impl Into) -> Result { + Err(FloorReason { code, detail: detail.into() }) +} + +/// One piece of a component's floor: its temporal units, each as the stream holds it. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FloorPiece { + pub units: Vec>, + /// Which units start at a shown key frame. + pub key: Vec, +} + +/// A component's checked floor: its sequence header and the OBU that carries it, and its +/// pieces. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Floor { + pub seq: SequenceHeader, + pub seq_obu: Vec, + pub pieces: Vec, +} + +/// The SHA-256 of a set's listing, as 64 lowercase hexadecimal digits. +pub fn listing_sha256(files: &BTreeMap>) -> Result { + let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let listing = wai::pin::listing_from(&view).map_err(|e| FloorReason { code: "floor-listing-mismatch", detail: e.to_string() })?; + Ok(crate::sha256_hex(&listing)) +} + +/// Check component `component`'s supplied floor against its plan: `starts` (the piece +/// starts, in frames) of a source of `frames` frames. +pub fn floor_supplied(files: &BTreeMap>, listing: Option<&str>, component: &str, starts: &[u64], frames: u64, extra_keyframes: bool) -> Result { + let got = listing_sha256(files)?; + if listing != Some(got.as_str()) { + return refuse("floor-listing-mismatch", format!("the supplied floor's listing is {got}, the policy pins {}", listing.unwrap_or("none"))); + } + let name = format!("{component}.obu"); + let stream = files.get(&name).ok_or_else(|| FloorReason { code: "floor-stream-missing", detail: format!("no {name} in the supplied floor") })?; + let malformed = |e: av1::Av1Error| FloorReason { code: "floor-av1-malformed", detail: e.0 }; + let tus = av1::temporal_units(stream).map_err(malformed)?; + let first = tus[0].sequence_header().ok_or_else(|| FloorReason { code: "floor-av1-malformed", detail: "the first temporal unit carries no sequence header".into() })?; + let seq_obu = first.bytes.to_vec(); + let seq = SequenceHeader::parse(first.payload).map_err(malformed)?; + if tus.len() as u64 != frames { + return refuse("floor-frame-count", format!("{} temporal units for {frames} source frames", tus.len())); + } + let mut key = Vec::with_capacity(tus.len()); + for (i, tu) in tus.iter().enumerate() { + if let Some(s) = tu.sequence_header() + && s.bytes != seq_obu.as_slice() + { + return refuse("floor-av1-malformed", format!("temporal unit {i} carries another sequence header")); + } + let k = av1::starts_with_shown_key_frame(tu, &seq).map_err(malformed)?; + if k && tu.sequence_header().is_none() { + return refuse("floor-av1-malformed", format!("temporal unit {i} starts a key frame without a sequence header")); + } + key.push(k); + } + for (i, &k) in key.iter().enumerate() { + let at_start = starts.binary_search(&(i as u64)).is_ok(); + if at_start && !k { + return refuse("floor-keyframe-mismatch", format!("no key frame at piece start {i}")); + } + if !at_start && k && !extra_keyframes { + return refuse("floor-keyframe-mismatch", format!("a key frame at {i}, inside a piece")); + } + } + let mut pieces = Vec::with_capacity(starts.len()); + for (p, &s) in starts.iter().enumerate() { + let e = starts.get(p + 1).copied().unwrap_or(frames); + let (s, e) = (s as usize, e as usize); + pieces.push(FloorPiece { units: tus[s..e].iter().map(av1::TemporalUnit::bytes).collect(), key: key[s..e].to_vec() }); + } + Ok(Floor { seq, seq_obu, pieces }) +} + +/// The payload of a `wai.video.av1` object as the reference sink decodes it +/// (`wai-rs` `codecs::video`): `n_frames`, `h`, `w`, `fps_num`, `fps_den` as u32 +/// little-endian, then each temporal unit as a u32 little-endian length and its bytes. +pub fn av1_payload(units: &[Vec], h: u32, w: u32, fps_num: u32, fps_den: u32) -> Vec { + let mut o = Vec::with_capacity(20 + units.iter().map(|u| 4 + u.len()).sum::()); + for v in [units.len() as u32, h, w, fps_num, fps_den] { + o.extend_from_slice(&v.to_le_bytes()); + } + for u in units { + o.extend_from_slice(&(u.len() as u32).to_le_bytes()); + o.extend_from_slice(u); + } + o +} + +/// A reference-framing payload's header after its unit count: `(h, w, fps_num, fps_den)`. +pub type Av1Header = (u32, u32, u32, u32); + +/// The temporal units of a `wai.video.av1` payload in the reference framing, and its +/// header; `None` when it is not that framing. +pub fn av1_units(payload: &[u8]) -> Option<(Vec>, Av1Header)> { + let u32_at = |i: usize| payload.get(i..i + 4).map(|b| u32::from_le_bytes(b.try_into().expect("four bytes"))); + let n = u32_at(0)? as usize; + let hdr = (u32_at(4)?, u32_at(8)?, u32_at(12)?, u32_at(16)?); + let mut at = 20usize; + let mut units = Vec::new(); + while at < payload.len() { + let len = u32_at(at)? as usize; + let end = (at + 4).checked_add(len).filter(|&e| e <= payload.len())?; + units.push(payload[at + 4..end].to_vec()); + at = end; + } + (units.len() == n).then_some((units, hdr)) +} diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs new file mode 100644 index 000000000..73d0d8f0e --- /dev/null +++ b/wai/wai-pack/src/golden.rs @@ -0,0 +1,1179 @@ +//! The package corpora of `wai/pack-conformance/`, written by [`regen`] and checked by +//! [`check`]: +//! +//! - `packs//`: a golden package. Its inputs are `policy.toml`, the supplied floor +//! `floor/`, the signing key `key.hex` (a test key, committed) and the fixture the +//! policy's component names (written by `wai-pack fixture gen`, not committed); its +//! output, `package/`, is every file the packer writes, compared byte for byte, and +//! verified by [`crate::verify::verify_files`]. +//! - `mutations/`: stage indexes, objects and receipts, each with the reason a verifier +//! gives it (`expected.json`): a well-formed case of each kind, and one case per +//! reason of packaging §12 the index, object and receipt rules give. +//! - `catalogs/`: indexes no golden package has (refinements on more than one deadline) +//! with the MoQ catalog each gives (§8.4). +//! - `receipts/`: version-1 package receipts as JSON (packaging §9.3), each with its +//! receipt hash: the legacy receipt whose hash `wai-rs` pins, and an unmetered one. +//! - `package-mutations/`: packages a verifier must refuse (§11.3), each the golden +//! altered and re-sealed with its key where the receipt or `LISTING` alone would catch +//! it, held as an overlay on the golden (`/`, the files that differ or are new, +//! and in `expected.json` the files it removes and its reason). + +use crate::{hex, objects}; +use ed25519_dalek::{Signer, SigningKey}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; +use std::path::Path; +use wai::container::{Conditioning, DerivedFrom, Manifest, ModelRequirement, Prev, Refines, Wai}; +use wai::package::{AnyPackageReceipt, PackageReceiptV2, Parent}; +use wai::quantum_ops::GrantRef; +use wai::stage_index::{self as wsix, ClassRec, EntryRec, Header, MediaClass, PieceRec, Role, StageIndex}; +use wai::wai_video_manifest::EnergyProvenance; + +/// The golden packs. +pub const PACKS: &[&str] = &["video-base-01"]; + +type Files = BTreeMap>; + +fn io(e: impl std::fmt::Display) -> std::io::Error { + std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()) +} + +fn read_key(p: &Path) -> std::io::Result { + let t = std::fs::read_to_string(p)?; + let t = t.trim(); + let mut seed = [0u8; 32]; + if t.len() != 64 { + return Err(io(format!("{}: not 64 hexadecimal digits", p.display()))); + } + for (i, s) in seed.iter_mut().enumerate() { + *s = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).map_err(io)?; + } + Ok(SigningKey::from_bytes(&seed)) +} + +/// Pack golden `name` from its inputs under `dir/packs/`. +pub fn pack_golden(dir: &Path, name: &str) -> std::io::Result { + let d = dir.join("packs").join(name); + let policy = std::fs::read(d.join("policy.toml"))?; + let floor = wai::pin::read_dir_set(&d.join("floor")).map_err(io)?; + let key = read_key(&d.join("key.hex"))?; + let p = crate::policy::Policy::parse_bytes(&policy).map_err(io)?; + let mut sources = BTreeMap::new(); + for c in &p.components { + let src = c.source.as_deref().and_then(|s| s.strip_suffix(".y4m")).ok_or_else(|| io(format!("component {} names no fixture", c.id)))?; + sources.insert(c.id.clone(), crate::fixture::generate(src).ok_or_else(|| io(format!("no fixture {src}")))?); + } + let signer_id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let mut o = crate::pack::PackOpts::new(key, signer_id, floor); + crate::pack::pack(&policy, &sources, &mut o).map_err(io) +} + +// ─────────────────────────────── mutations ─────────────────────────────── + +fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, payload: &[u8]) -> Vec { + let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x")); + if let Some((base, prev, layer)) = refines { + m = m.with_refines(&Refines { base: hex(base), prev: Prev::One(hex(prev)), layer, span: None }); + } + if derived { + m = m.with_derived_from(&DerivedFrom { motif: "ab".repeat(32), motif_capability: "wai.video.int_motion".into(), renderer: "r@1".into(), settings: "cd".repeat(32) }); + } + Wai::new(m, payload.to_vec()).to_bytes().expect("a manifest serialises") +} + +#[allow(clippy::too_many_arguments)] +fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32, tier: u8, flags: u8) -> EntryRec { + let man = u32::from_le_bytes(bytes[4..8].try_into().expect("an envelope")); + EntryRec { + content_hash: objects::object_hash(bytes), + byte_len: bytes.len() as u64, + piece, + refines, + capability: cap, + role, + layer, + prefix_tier: tier, + flags, + deadline_ms: if role == Role::Refinement { 1500 } else { 0 }, + prefix_digest: if tier == wsix::TIER_DECODE { [7u8; 32] } else { [0u8; 32] }, + sse: wsix::SSE_UNKNOWN, + manifest_len: man, + payload_len: bytes.len() as u64 - 12 - man as u64, + } +} + +/// The payloads of a small staged clip: a `WIV1` base of two 8 × 8 frames at a coarse +/// step, and two `WIR1` layers that each bring every frame closer to the source. +fn staged_payloads() -> (Vec, Vec, Vec) { + use wai::staged::wir1::{encode, Below, Next, Predictor, RefineParams, UnitKind}; + let (h, w) = (8u32, 8u32); + let src: Vec> = (0..2u32).map(|t| (0..h * w * 3).map(|i| ((i * 29 + t * 7) % 251) as u8).collect()).collect(); + let base = wai::int_video_enc::encode(&src, h, w, &wai::int_video_enc::EncParams { block: 4, search: 1, mv_precision: 1, q: 24 }).expect("a base encodes"); + let layer = |below: &[Vec], q: u32| { + let belows: Vec = below.iter().map(|f| Below::U8(f)).collect(); + let targets: Vec = src.iter().map(|f| Below::U8(f)).collect(); + let p = RefineParams { q, predictor: Predictor::Identity, rate: (0, 0), table_per_unit: false, max_symbols: 4095 }; + let (payload, next) = encode(UnitKind::Rgb8, &belows, (h, w, 3), &targets, (h, w, 3), &p).expect("a layer encodes"); + let frames: Vec> = next.into_iter().map(|n| match n { + Next::U8(v) => v, + Next::I16(_) => unreachable!("an RGB8 layer"), + }).collect(); + (payload, frames) + }; + let (l1, f1) = layer(&base.recon, 6); + let (l2, _) = layer(&f1, 1); + (base.wiv1, l1, l2) +} + +/// A two-piece index with a base, two refinements and a floor, then the base again and a +/// floor; and its objects. The base and the layers carry real `WIV1` and `WIR1` payloads, +/// so they are what their capabilities' formats say (§6.2, counted bytes). +pub fn layered() -> (StageIndex, Vec>) { + let (wiv1, wir1_a, wir1_b) = staged_payloads(); + let base = env("wai.video.int_motion", None, false, &wiv1); + let bh = objects::object_hash(&base); + let r1 = env("wai.video.int_refine", Some((&bh, &bh, 1)), false, &wir1_a); + let r1h = objects::object_hash(&r1); + let r2 = env("wai.video.int_refine", Some((&bh, &r1h, 2)), false, &wir1_b); + let f0 = env("wai.video.av1", None, true, b"floor-0"); + let f1 = env("wai.video.av1", None, true, b"floor-1"); + let entries = vec![ + entry(&base, 0, 2, Role::Base, 0, wsix::NO_REFINES, wsix::TIER_DECODE, 0), + entry(&r1, 0, 3, Role::Refinement, 1, 0, wsix::TIER_DECODE, 0), + entry(&r2, 0, 3, Role::Refinement, 2, 1, wsix::TIER_DECODE, 0), + entry(&f0, 0, 4, Role::Floor, 0, wsix::NO_REFINES, wsix::TIER_NONE, wsix::ENTRY_DERIVED_FROM), + entry(&base, 1, 2, Role::Base, 0, wsix::NO_REFINES, wsix::TIER_DECODE, wsix::ENTRY_DUP), + entry(&f1, 1, 4, Role::Floor, 0, wsix::NO_REFINES, wsix::TIER_NONE, wsix::ENTRY_DERIVED_FROM), + ]; + let ix = StageIndex { + header: Header { + media_class: MediaClass::Video, + tb_num: 25, + tb_den: 1, + policy_digest: [1u8; 32], + prev_index_digest: [0u8; 32], + first_piece: 0, + first_entry: 0, + plan_digest: wsix::plan_digest(&[0, 50]), + component: 0, + }, + pieces: vec![ + PieceRec { start: 0, duration: 50, first_entry: 0, n_entries: 4, kind: 0, flags: 0, rules: 0, moments: 0 }, + PieceRec { start: 50, duration: 25, first_entry: 4, n_entries: 2, kind: 1, flags: wsix::PIECE_SCENE_CUT, rules: 1, moments: 0 }, + ], + entries, + classes: vec![ClassRec { name: 1, max_layer: 2, flags: 0, max_kbps: 0 }], + plan: vec![vec![2, 0]], + strings: vec!["v".into(), "tv".into(), "wai.video.int_motion".into(), "wai.video.int_refine".into(), "wai.video.av1".into()], + }; + (ix, vec![base, r1, r2, f0, f1]) +} + +type IndexMutation = (&'static str, &'static str, fn(&mut StageIndex)); + +/// The index cases: a name, the reason, the mutation. +fn index_cases() -> Vec { + vec![ + ("ok", "ok", |_| {}), + ("refines-flipped", "refines-mismatch", |ix| ix.entries[2].refines = 3), + ("layer-gap", "layer-gap", |ix| ix.entries[2].refines = 0), + ("refines-outside-window", "refines-outside-window", |ix| ix.entries[2].refines = 99), + ("tier-entropy-over-entropy", "tier-entropy-passthrough", |ix| { + ix.strings[2] = "wai.image.jpegai".into(); + for k in [0, 1, 4] { + ix.entries[k].prefix_tier = wsix::TIER_ENTROPY; + ix.entries[k].prefix_digest = [0; 32]; + } + }), + ("tier-decode-over-entropy", "tier-weakest", |ix| { + ix.strings[2] = "wai.image.jpegai".into(); + for k in [0, 4] { + ix.entries[k].prefix_tier = wsix::TIER_ENTROPY; + ix.entries[k].prefix_digest = [0; 32]; + } + }), + ("tier-on-floor", "tier-mismatch", |ix| { + ix.entries[3].prefix_tier = wsix::TIER_DECODE; + ix.entries[3].prefix_digest = [7; 32]; + }), + ("digest-without-tier", "digest-without-tier", |ix| ix.entries[3].prefix_digest = [9; 32]), + ("dup-flag-missing", "dup-flag", |ix| ix.entries[4].flags = 0), + ("dup-flag-lie", "dup-flag", |ix| ix.entries[5].flags |= wsix::ENTRY_DUP), + ("dup-length", "dup-length", |ix| ix.entries[4].byte_len += 1), + ("floor-at-layer-1", "layer-role", |ix| ix.entries[3].layer = 1), + ("base-with-refines", "layer-role", |ix| ix.entries[0].refines = 1), + ("floor-before-refinement", "entry-order", |ix| ix.entries.swap(2, 3)), + ("deadline-on-base", "deadline-role", |ix| ix.entries[0].deadline_ms = 5), + ("floor-without-derived-from", "floor-derived-from", |ix| ix.entries[3].flags = 0), + ("entry-flag-unknown", "entry-flags", |ix| ix.entries[0].flags |= 0x80), + ("piece-gap", "piece-order", |ix| ix.pieces[1].start = 51), + ("piece-empty", "piece-order", |ix| ix.pieces[0].duration = 0), + ("piece-entries-overlap", "piece-entries", |ix| ix.pieces[1].first_entry = 3), + ("entry-in-another-piece", "piece-entries", |ix| ix.entries[5].piece = 0), + ("piece-kind-unknown", "piece-kind", |ix| ix.pieces[0].kind = 9), + ("plan-past-piece", "class-plan", |ix| ix.plan[0][1] = 1), + ("plan-past-class", "class-plan", |ix| ix.plan[0][0] = 3), + ("plan-floor-only-class", "class-plan", |ix| ix.classes[0].flags = wsix::CLASS_FLOOR_ONLY), + ("class-flag-unknown", "class-flags", |ix| ix.classes[0].flags = 2), + ("timebase-zero", "index-timebase", |ix| ix.header.tb_den = 0), + ("on-demand-first-piece", "window-start", |ix| ix.header.first_piece = 1), + ("on-demand-first-entry", "window-start", |ix| ix.header.first_entry = 1), + ("on-demand-start", "window-start", |ix| { + for p in ix.pieces.iter_mut() { + p.start += 50; + } + }), + ] +} + +type ByteMutation = (&'static str, &'static str, fn(&mut Vec)); + +/// The byte cases: the good index's bytes changed. +fn byte_cases() -> Vec { + vec![ + ("truncated-table", "index-truncated", |b| b.truncate(wsix::HEADER_LEN + 2 * wsix::PIECE_LEN + 3 * wsix::ENTRY_LEN + 5)), + ("trailing", "index-trailing", |b| b.extend_from_slice(&[0u8; 8])), + ("magic", "index-magic", |b| b[0] = b'X'), + ("version", "index-version", |b| b[4] = 2), + ("media-class", "index-media-class", |b| b[6] = 7), + ("reserved", "index-reserved", |b| b[150] = 1), + ("role", "index-role", |b| b[wsix::HEADER_LEN + 2 * wsix::PIECE_LEN + 54] = 7), + ("tier", "index-tier", |b| b[wsix::HEADER_LEN + 2 * wsix::PIECE_LEN + 56] = 3), + ("string-index", "index-string", |b| b[wsix::HEADER_LEN + 2 * wsix::PIECE_LEN + 52] = 40), + ] +} + +/// The verdict on an index's bytes: `ok`, or the first reason. +pub fn index_verdict(b: &[u8]) -> String { + match StageIndex::parse(b) { + Err(r) => r.code.to_string(), + Ok(ix) => ix.check().map(|_| "ok".to_string()).unwrap_or_else(|r| r.code.to_string()), + } +} + +/// The verdict on an index and the objects its entries name, in entry order: the +/// index's own, `object-missing`, or the first object rule that fails, the counted-bytes +/// rule (§6.2) last. +pub fn objects_verdict(index: &[u8], objects: &Files) -> String { + let v = index_verdict(index); + if v != "ok" { + return v; + } + let ix = StageIndex::parse(index).expect("checked"); + for (k, e) in ix.entries.iter().enumerate() { + let Some(b) = objects.get(&format!("{}.wai", hex(&e.content_hash))) else { return "object-missing".into() }; + if let Err(r) = wsix::check_object(&ix, k, b) { + return r.code.into(); + } + if crate::pack::object_lengths(b).ok() != Some((e.manifest_len, e.payload_len)) { + return "object-lengths".into(); + } + } + "ok".into() +} + +/// The verdict on a receipt's JSON against its indexes. +pub fn receipt_verdict(json: &[u8], indexes: &[&[u8]]) -> String { + let Ok(t) = std::str::from_utf8(json) else { return "receipt-json".into() }; + match AnyPackageReceipt::from_json(t) { + Err(e) => e.code.into(), + Ok(AnyPackageReceipt::V1(_)) => "receipt-version".into(), + Ok(AnyPackageReceipt::V2(r)) => r.verify_detailed(indexes).map(|_| "ok".to_string()).unwrap_or_else(|e| e.code.into()), + } +} + +/// The key the mutation receipts are signed with. +fn mutation_key() -> SigningKey { + SigningKey::from_bytes(&[0x42; 32]) +} + +/// Every file of `mutations/` and its `expected.json`. +pub fn mutations() -> (Files, Value) { + let mut files: Files = BTreeMap::new(); + let mut cases: Vec = Vec::new(); + let (good, objs) = layered(); + let good_bytes = good.to_bytes(); + for (name, reason, m) in index_cases() { + let mut ix = good.clone(); + m(&mut ix); + let path = format!("index/{name}.wsix"); + files.insert(path.clone(), ix.to_bytes()); + cases.push(json!({"name": name, "kind": "index", "index": path, "reason": reason})); + } + for (name, reason, m) in byte_cases() { + let mut b = good_bytes.clone(); + m(&mut b); + let path = format!("index/{name}.wsix"); + files.insert(path.clone(), b); + cases.push(json!({"name": name, "kind": "index", "index": path, "reason": reason})); + } + // Objects: the good objects, a fragmented MP4 file listed as an entry, and objects + // that are not what their entries say. + for o in &objs { + files.insert(format!("objects/{}.wai", hex(&objects::object_hash(o))), o.clone()); + } + let cmaf = b"\x00\x00\x00\x18ftypcmfc\x00\x00\x00\x00cmfciso6\x00\x00\x00\x08mdat".to_vec(); + files.insert(format!("objects/{}.wai", hex(&objects::object_hash(&cmaf))), cmaf.clone()); + // A file named by a content hash its bytes do not have: the base's bytes under a + // name one bit away from the base's. + let mut misnamed = objects::object_hash(&objs[0]); + misnamed[31] ^= 1; + files.insert(format!("objects/{}.wai", hex(&misnamed)), objs[0].clone()); + // The base with a payload that is not a WIV1, at the same lengths. + let mut not_wiv1 = objs[0].clone(); + let n = not_wiv1.len(); + not_wiv1[n - objs[0].len() + 12 + u32::from_le_bytes(objs[0][4..8].try_into().expect("an envelope")) as usize..n].fill(0); + let not_wiv1_hash = objects::object_hash(¬_wiv1); + files.insert(format!("objects/{}.wai", hex(¬_wiv1_hash)), not_wiv1); + type ObjectMutation = (&'static str, &'static str, Box); + let cmaf_hash = objects::object_hash(&cmaf); + let cmaf_len = cmaf.len() as u64; + let object_cases: Vec = vec![ + ("ok", "ok", Box::new(|_| {})), + ("cmaf-listed", "non-envelope-entry", Box::new(move |ix| { + ix.entries[3].content_hash = cmaf_hash; + ix.entries[3].byte_len = cmaf_len; + })), + ("missing", "object-missing", Box::new(|ix| ix.entries[1].content_hash = [0xee; 32])), + ("hash", "object-hash", Box::new(move |ix| { + ix.entries[0].content_hash = misnamed; + ix.entries[4].content_hash = misnamed; + })), + ("not-its-format", "object-lengths", Box::new(move |ix| { + ix.entries[0].content_hash = not_wiv1_hash; + ix.entries[4].content_hash = not_wiv1_hash; + })), + ("length", "object-length", Box::new(|ix| { + ix.entries[0].byte_len += 1; + ix.entries[4].byte_len += 1; + })), + ("lengths", "object-lengths", Box::new(|ix| { + ix.entries[0].payload_len += 1; + ix.entries[4].payload_len += 1; + })), + ("capability", "object-capability", Box::new(|ix| ix.strings[3] = "wai.image.int_refine".into())), + ("derived-from", "object-derived-from", Box::new(|ix| { + ix.entries[3].flags = 0; + ix.entries[3].role = Role::Asset; + })), + ]; + for (name, reason, m) in object_cases { + let mut ix = good.clone(); + m(&mut ix); + let path = format!("objects-index/{name}.wsix"); + files.insert(path.clone(), ix.to_bytes()); + cases.push(json!({"name": format!("objects-{name}"), "kind": "objects", "index": path, "objects": "objects/", "reason": reason})); + } + // A refinement whose refines names another object: swap its object for one that + // refines the floor. Written as its own case so the index stays well formed. + { + let bh = objects::object_hash(&objs[0]); + let fh = objects::object_hash(&objs[3]); + let r2 = env("wai.video.int_refine", Some((&bh, &fh, 2)), false, &staged_payloads().2); + files.insert(format!("objects/{}.wai", hex(&objects::object_hash(&r2))), r2.clone()); + let mut ix = good.clone(); + ix.entries[2] = entry(&r2, 0, 3, Role::Refinement, 2, 1, wsix::TIER_DECODE, 0); + let path = "objects-index/refines-names-another.wsix".to_string(); + files.insert(path.clone(), ix.to_bytes()); + cases.push(json!({"name": "objects-refines-names-another", "kind": "objects", "index": path, "objects": "objects/", "reason": "object-refines"})); + } + // Receipts over the good index. + files.insert("receipt/index-v.wsix".into(), good_bytes.clone()); + let mut live = good.clone(); + live.header.prev_index_digest = [9; 32]; + files.insert("receipt/live/index-v.wsix".into(), live.to_bytes()); + let key = mutation_key(); + let seal = |energy: (u64, Option), parent: Parent, index: &[u8]| { + PackageReceiptV2::seal(&key, &wai::signer_id::did_key(&key.verifying_key().to_bytes()), "mutations", [1u8; 32], &[("v".into(), index.to_vec())], "builtin:wai-pack@0.1.0", energy, GrantRef::unbounded("wai.video.package"), parent) + .expect("the good index seals") + }; + let resign = |mut r: PackageReceiptV2| { + r.sig = key.sign(&r.signing_payload()).to_bytes(); + r + }; + let ok = seal((0, None), Parent::None, &good_bytes); + let labelled = seal((25_000, Some(EnergyProvenance::ModelBased)), Parent::RePack([3; 32]), &good_bytes); + let live_r = seal((0, None), Parent::LiveWindow([4; 32]), &live.to_bytes()); + let mut receipts: Vec<(&str, &str, String, &str)> = vec![ + ("ok", "ok", ok.to_json(), "receipt/index-v.wsix"), + ("ok-labelled", "ok", labelled.to_json(), "receipt/index-v.wsix"), + ("ok-live-window", "ok", live_r.to_json(), "receipt/live/index-v.wsix"), + ]; + let m = |f: &dyn Fn(&mut PackageReceiptV2)| { + let mut r = ok.clone(); + f(&mut r); + resign(r).to_json() + }; + receipts.push(("counts", "receipt-counts", m(&|r| { + r.unique_objects -= 1; + r.stored_bytes -= 10; + }), "receipt/index-v.wsix")); + receipts.push(("parent-kind", "receipt-parent-kind", m(&|r| r.parent = Parent::LiveWindow([4; 32])), "receipt/index-v.wsix")); + receipts.push(("parent-kind-live", "receipt-parent-kind", { + let mut r = live_r.clone(); + r.parent = Parent::None; + resign(r).to_json() + }, "receipt/live/index-v.wsix")); + receipts.push(("stage-root", "receipt-stage-root", m(&|r| r.components[0].stage_root = [0; 32]), "receipt/index-v.wsix")); + receipts.push(("index-digest", "receipt-index-digest", m(&|r| r.components[0].index_digest = [0; 32]), "receipt/index-v.wsix")); + receipts.push(("media-class", "receipt-component", m(&|r| r.components[0].media_class = MediaClass::Audio), "receipt/index-v.wsix")); + receipts.push(("policy-digest", "receipt-policy-digest", m(&|r| r.policy_digest = [2; 32]), "receipt/index-v.wsix")); + receipts.push(("energy", "receipt-energy", m(&|r| r.joules_micro = 12), "receipt/index-v.wsix")); + receipts.push(("signature", "receipt-signature", { + let mut r = ok.clone(); + r.sig[0] ^= 1; + r.to_json() + }, "receipt/index-v.wsix")); + receipts.push(("unknown-member", "receipt-json", ok.to_json().replacen('{', "{\"extra\":1,", 1), "receipt/index-v.wsix")); + // §9.1's reader: a member named twice, -0, a lone surrogate, null in place of a value, + // energy_provenance left out, evidence beside no class. + let oj = ok.to_json(); + receipts.push(("duplicate-member", "receipt-json", oj.replacen("{\"asset_id\"", "{\"asset_id\":\"forged\",\"asset_id\"", 1), "receipt/index-v.wsix")); + receipts.push(("minus-zero", "receipt-json", oj.replacen("\"joules_micro\":0", "\"joules_micro\":-0", 1), "receipt/index-v.wsix")); + receipts.push(("lone-surrogate", "receipt-json", oj.replacen("\"signer_id\":\"", "\"signer_id\":\"\\ud800", 1), "receipt/index-v.wsix")); + receipts.push(("null-value", "receipt-json", oj.replacen("\"packager\":\"builtin:wai-pack@0.1.0\"", "\"packager\":null", 1), "receipt/index-v.wsix")); + receipts.push(("energy-member-missing", "receipt-json", oj.replacen("\"energy_provenance\":null,", "", 1), "receipt/index-v.wsix")); + receipts.push(("evidence-without-class", "receipt-json", oj.replacen("\"energy_provenance\":null,", "\"energy_provenance\":null,\"energy_uncertainty\":null,", 1), "receipt/index-v.wsix")); + // §9.1's verification: no component; two components of one id. + receipts.push(("no-component", "receipt-index-count", m(&|r| { + r.components.clear(); + r.total_entries = 0; + r.unique_objects = 0; + r.total_bytes = 0; + r.stored_bytes = 0; + r.work_units = 0; + }), "receipt/index-v.wsix")); + receipts.push(("receipt-hash", "receipt-json", ok.to_json().replacen(&format!("\"total_bytes\":{}", ok.total_bytes), &format!("\"total_bytes\":{}", ok.total_bytes + 1), 1), "receipt/index-v.wsix")); + for (name, reason, text, index) in receipts { + let path = format!("receipt/{name}.json"); + files.insert(path.clone(), text.into_bytes()); + let indexes: Vec<&str> = if name == "no-component" { Vec::new() } else { vec![index] }; + cases.push(json!({"name": format!("receipt-{name}"), "kind": "receipt", "receipt": path, "indexes": indexes, "reason": reason})); + } + { + let mut two = ok.clone(); + two.components.push(two.components[0].clone()); + two.total_entries *= 2; + two.total_bytes *= 2; + two.work_units *= 2; + let path = "receipt/two-components-one-id.json".to_string(); + files.insert(path.clone(), resign(two).to_json().into_bytes()); + cases.push(json!({"name": "receipt-two-components-one-id", "kind": "receipt", "receipt": path, "indexes": ["receipt/index-v.wsix", "receipt/index-v.wsix"], "reason": "receipt-component"})); + } + (files, json!({"cases": cases})) +} + +/// The verdict a case gets from the files of `mutations/`. +pub fn case_verdict(c: &Value, files: &Files) -> Result { + let get = |k: &str| c[k].as_str().and_then(|p| files.get(p)).ok_or_else(|| format!("{}: {k} is not a file of the corpus", c["name"])); + Ok(match c["kind"].as_str() { + Some("index") => index_verdict(get("index")?), + Some("objects") => { + let objs: Files = files.iter().filter_map(|(k, v)| k.strip_prefix("objects/").map(|n| (n.to_string(), v.clone()))).collect(); + objects_verdict(get("index")?, &objs) + } + Some("receipt") => { + let mut ix: Vec<&[u8]> = Vec::new(); + for p in c["indexes"].as_array().into_iter().flatten() { + ix.push(p.as_str().and_then(|p| files.get(p)).ok_or("an index that is not a file")?); + } + receipt_verdict(get("receipt")?, &ix) + } + _ => return Err(format!("{}: an unknown kind", c["name"])), + }) +} + +// ─────────────────────────────── re-sealing (tests and probes) ─────────────────────────────── + +/// Re-derive a package's floor CMAF files, catalogs, receipt and `LISTING` from its +/// (possibly altered) policy, objects and indexes, signing with `key`, without the +/// verifier's checks: what a dishonest packager holding the key could publish. Tests +/// alter a package, re-seal it, and require the verifier to refuse it for the alteration. +/// `policy.json` and every index's policy digest follow `policy.toml` (with +/// `royalty_free_only` forced on where the old `policy.json` says so); an index whose +/// `prev_index_digest` is not zero is sealed as a live window. +pub fn reseal(files: &mut Files, key: &SigningKey) -> Result<(), String> { + reseal_as(files, key, None) +} + +/// [`reseal`], with `policy.json` the bytes given, when given, and the policy digest +/// theirs: a `policy.json` that is not `policy.toml`'s effective form, sealed. +pub fn reseal_as(files: &mut Files, key: &SigningKey, policy_json: Option<&[u8]>) -> Result<(), String> { + use crate::catalog::{CompView, FloorView}; + files.remove("LISTING"); + // policy.json's royalty_free_only, where it says true, is the override the package + // was packed under (§3.2), as the verifier reads it (§11.3). + let rfo = files.get("policy.json").and_then(|b| serde_json::from_slice::(b).ok()).and_then(|v| v.get("royalty_free_only").and_then(Value::as_bool)) == Some(true); + let policy = crate::policy::Policy::parse_bytes(files.get("policy.toml").ok_or("no policy.toml")?).map_err(|e| e.to_string())?.effective(&crate::policy::Overrides { royalty_free_only: rfo }); + let pj = policy_json.map_or_else(|| policy.canonical_json().into_bytes(), <[u8]>::to_vec); + let policy_digest = crate::policy::Policy::digest_of_json(&pj); + files.insert("policy.json".into(), pj); + let comp = policy.components.first().ok_or("no component")?; + let tb = comp.timebase; + let index_path = wai::package::index_file(&comp.id); + let mut ix = StageIndex::parse(files.get(&index_path).ok_or("no index")?).map_err(|e| e.to_string())?; + ix.header.policy_digest = policy_digest; + let ixb = ix.to_bytes(); + files.insert(index_path, ixb.clone()); + let mut objects: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); + let mut dims = None; + for e in &ix.entries { + if let Some(b) = files.get(&objects::object_path(&e.content_hash)) { + objects.insert(e.content_hash, b.clone()); + if e.role == Role::Base && dims.is_none() { + let w = Wai::from_bytes(b).map_err(|e| e.to_string())?; + if let Ok(wai::int_payload::IntDecoded::Video { width, height, .. }) = wai::int_payload::decode_int(ix.capability(e), &w.payload, &wai::int_payload::IntParams::none()) { + dims = Some((width, height)); + } + } + } + } + let mut pieces = Vec::new(); + let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; + for p in 0..ix.pieces.len() { + let Some(fe) = ix.piece_entries(p).iter().find(|e| e.role == Role::Floor) else { break }; + let w = Wai::from_bytes(objects.get(&fe.content_hash).ok_or("a floor object is not held")?).map_err(|e| e.to_string())?; + let (units, _) = crate::floor::av1_units(&w.payload).ok_or("a floor payload is not the framing")?; + let mut key_frames = Vec::new(); + for u in &units { + let tus = crate::av1::temporal_units(u).map_err(|e| e.0)?; + let tu = tus.first().ok_or("an empty unit")?; + if seq.is_none() { + let s = tu.sequence_header().ok_or("no sequence header")?; + seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| e.0)?, s.bytes.to_vec())); + } + key_frames.push(crate::av1::starts_with_shown_key_frame(tu, &seq.as_ref().expect("set").0).map_err(|e| e.0)?); + } + pieces.push(crate::floor::FloorPiece { units, key: key_frames }); + } + files.retain(|k, _| !k.starts_with("floor/")); + files.retain(|k, _| !["hls/", "dash/", "moq/", "stages/"].iter().any(|d| k.starts_with(d))); + let mut floor_view = None; + if let Some((s, obu)) = seq { + let fl = crate::floor::Floor { seq: s, seq_obu: obu, pieces }; + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + let (ip, init, frags) = crate::pack::cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num)?; + floor_view = Some(FloorView { codec: fl.seq.codec_string(), w: fl.seq.max_frame_width, h: fl.seq.max_frame_height, init: ip.clone(), fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect() }); + files.insert(ip, init); + files.extend(frags); + } + let (w, h) = dims.unwrap_or((0, 0)); + let view = CompView { index: &ix, tb, w, h, objects: &objects, floor: floor_view }; + if let Ok(cats) = crate::catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &policy_digest) { + files.extend(cats); + } + let parent = if ix.header.prev_index_digest != [0u8; 32] { Parent::LiveWindow([7u8; 32]) } else { Parent::None }; + let r = PackageReceiptV2::seal(key, &wai::signer_id::did_key(&key.verifying_key().to_bytes()), &policy.asset_id, policy_digest, &[(comp.id.clone(), ixb)], &crate::pack::packager_id(), wai::energy_meter::EnergyReading::unmetered(), GrantRef::unbounded("wai.video.package"), parent).map_err(|e| e.to_string())?; + files.insert("package.receipt.json".into(), r.to_json().into_bytes()); + relist(files); + Ok(()) +} + +/// Write `LISTING` again over every other file. +pub fn relist(files: &mut Files) { + files.remove("LISTING"); + let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + if let Ok(l) = wai::pin::listing_from(&view) { + files.insert("LISTING".into(), l); + } +} + +// ─────────────────────────────── catalogs ─────────────────────────────── + +/// MoQ catalogs (§8.4) of indexes no golden package has: layers on more than one +/// deadline, so each refinement track's `depends` is read from what its entries refine. +pub fn catalogs() -> (Files, Value) { + use crate::catalog::{render_moq_catalog, CompView, FloorView}; + let mut files = BTreeMap::new(); + let mut cases = Vec::new(); + let lag = [0u64, 200, 400]; + let deadline = 1500u64; + type Shape = (&'static str, fn(&mut StageIndex)); + let shapes: [Shape; 3] = [ + ("one-deadline", |_| {}), + // Layer 1 of piece 0 on its own deadline, layer 2 on the default refining it. + ("layer-1-on-its-own-deadline", |ix| ix.entries[1].deadline_ms = 800), + // Both layers on a deadline that is not the default. + ("both-layers-on-800", |ix| { + ix.entries[1].deadline_ms = 800; + ix.entries[2].deadline_ms = 800; + }), + ]; + for (name, m) in shapes { + let (mut ix, objs) = layered(); + m(&mut ix); + let objects: BTreeMap<[u8; 32], Vec> = objs.iter().map(|o| (objects::object_hash(o), o.clone())).collect(); + let floor = FloorView { codec: "av01.0.00M.08".into(), w: 64, h: 36, init: "floor/v/init.mp4".into(), fragments: Vec::new() }; + let v = CompView { index: &ix, tb: crate::policy::Timebase { num: 25, den: 1 }, w: 8, h: 8, objects: &objects, floor: Some(floor) }; + let moq = render_moq_catalog(&v, &lag, deadline).expect("a catalog"); + let path = format!("catalogs/{name}.wsix"); + files.insert(path.clone(), ix.to_bytes()); + cases.push(json!({"index": path, "layer_lag_ms": lag, "default_deadline": deadline, "floor": {"codec": "av01.0.00M.08", "w": 64, "h": 36}, "moq": moq})); + } + (files, json!({"cases": cases})) +} + +// ─────────────────────────────── package mutations ─────────────────────────────── + +/// The golden whose package the package mutations alter, and the component they alter. +pub const MUTATED_PACK: &str = "video-base-01"; +const MUTATED_INDEX: &str = "index-v.wsix"; + +/// A package altered: its name, the reason a package's verifier gives it (§11.3; `ok` +/// for one it must accept), its files, and the directories that hold nothing it has +/// besides them (`empty_dirs`), and those that cannot be read (`unreadable_dirs`, made +/// so where the platform allows; elsewhere they hold nothing either). +#[derive(Debug, Clone)] +pub struct PackageMutation { + pub name: &'static str, + pub reason: String, + pub files: Files, + pub empty_dirs: Vec, + pub unreadable_dirs: Vec, +} + +fn golden_index(f: &Files) -> StageIndex { + StageIndex::parse(&f[MUTATED_INDEX]).expect("the golden's index parses") +} + +fn set_golden_index(f: &mut Files, ix: &StageIndex) { + f.insert(MUTATED_INDEX.into(), ix.to_bytes()); +} + +fn policy_edit(f: &mut Files, from: &str, to: &str) { + let t = String::from_utf8(f["policy.toml"].clone()).expect("the golden's policy is UTF-8"); + assert!(t.contains(from), "the golden's policy holds {from:?}"); + f.insert("policy.toml".into(), t.replacen(from, to, 1).into_bytes()); +} + +/// The floor object of piece `p`: its entry index and its bytes. +pub fn floor_entry(f: &Files, p: usize) -> (usize, Vec) { + let ix = golden_index(f); + let k = ix.pieces[p].first_entry as usize + ix.piece_entries(p).iter().position(|e| e.role == Role::Floor).expect("a floor entry"); + (k, f[&objects::object_path(&ix.entries[k].content_hash)].clone()) +} + +/// Replace entry `k`'s object with `bytes`, its entry following (hash and lengths) and +/// the duplicate flags following the new content. +pub fn replace_object(f: &mut Files, k: usize, bytes: Vec) { + let mut ix = golden_index(f); + let old = ix.entries[k].content_hash; + let h = objects::object_hash(&bytes); + let man = u32::from_le_bytes(bytes[4..8].try_into().expect("an envelope")); + let e = &mut ix.entries[k]; + e.content_hash = h; + e.byte_len = bytes.len() as u64; + e.manifest_len = man; + e.payload_len = bytes.len() as u64 - 12 - man as u64; + if ix.entries.iter().all(|e| e.content_hash != old) { + f.remove(&objects::object_path(&old)); + } + f.insert(objects::object_path(&h), bytes); + let mut seen = std::collections::BTreeSet::new(); + for e in ix.entries.iter_mut() { + if seen.insert(e.content_hash) { + e.flags &= !wsix::ENTRY_DUP; + } else { + e.flags |= wsix::ENTRY_DUP; + } + } + set_golden_index(f, &ix); +} + +/// A floor object's manifest JSON, its framing header and its temporal units. +pub fn floor_parts(b: &[u8]) -> (Value, crate::floor::Av1Header, Vec>) { + let ml = u32::from_le_bytes(b[4..8].try_into().expect("an envelope")) as usize; + let man: Value = serde_json::from_slice(&b[8..8 + ml]).expect("a manifest"); + let (units, hdr) = crate::floor::av1_units(&b[12 + ml..]).expect("§7.2's framing"); + (man, hdr, units) +} + +/// A floor object from its manifest JSON, framing header and temporal units. +pub fn floor_envelope(man: &Value, hdr: crate::floor::Av1Header, units: &[Vec]) -> Vec { + let m = serde_json::to_vec(man).expect("JSON"); + let p = crate::floor::av1_payload(units, hdr.0, hdr.1, hdr.2, hdr.3); + [b"WAI1".as_slice(), &(m.len() as u32).to_le_bytes(), &m, &(p.len() as u32).to_le_bytes(), &p].concat() +} + +/// Packages a verifier must refuse (§11.3), each the golden altered and, where the +/// receipt or `LISTING` alone would catch the alteration, re-sealed with the package key +/// ([`reseal`]): what a dishonest packager holding the key could publish. One it must +/// accept (an extra key frame the policy allows) is among them. +pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { + let sealed = |mut f: Files| { + reseal(&mut f, key).expect("the alteration re-seals"); + f + }; + let listed = |mut f: Files| { + relist(&mut f); + f + }; + let mut out: Vec = Vec::new(); + let mut add = |name: &'static str, reason: &str, files: Files| out.push(PackageMutation { name, reason: reason.into(), files, empty_dirs: Vec::new(), unreadable_dirs: Vec::new() }); + + // A version-2 receipt over no component at all, signed with the package key, beside + // the policy alone: on demand and as a live window. + let r0 = PackageReceiptV2::from_json(std::str::from_utf8(&g["package.receipt.json"]).expect("UTF-8")).expect("the golden's receipt"); + for (name, parent) in [("no-component", Parent::None), ("no-component-live", Parent::LiveWindow([9; 32]))] { + let mut r = r0.clone(); + r.components.clear(); + (r.total_entries, r.unique_objects, r.total_bytes, r.stored_bytes, r.work_units) = (0, 0, 0, 0, 0); + r.parent = parent; + r.sig = key.sign(&r.signing_payload()).to_bytes(); + let mut f: Files = ["policy.toml", "policy.json"].iter().map(|p| (p.to_string(), g[*p].clone())).collect(); + f.insert("package.receipt.json".into(), r.to_json().into_bytes()); + add(name, "policy-component-mismatch", listed(f)); + } + // The index against the policy: class records (a floor-only class offered WAI, and a + // class's rate alone), an entry's capability no rung's, the string table in another + // order, a class plan the policy does not give. + let mut f = g.clone(); + let mut ix = golden_index(&f); + ix.classes[2].flags = 0; + ix.plan[2] = ix.plan[0].clone(); + set_golden_index(&mut f, &ix); + add("index-class-records", "index-policy-mismatch", sealed(f)); + // A class record alone: the plan, the catalogs and the receipt all follow the index. + let mut f = g.clone(); + let mut ix = golden_index(&f); + ix.classes[1].max_kbps -= 1; + set_golden_index(&mut f, &ix); + add("index-class-rate", "index-policy-mismatch", sealed(f)); + let mut f = g.clone(); + let mut ix = golden_index(&f); + let at = ix.strings.iter().position(|s| s == "wai.video.av1").expect("the floor's capability"); + ix.strings[at] = "wai.video.avc".into(); + set_golden_index(&mut f, &ix); + add("index-capability", "index-policy-mismatch", sealed(f)); + let mut f = g.clone(); + let mut ix = golden_index(&f); + ix.strings.swap(1, 2); + for c in ix.classes.iter_mut() { + c.name = match c.name { + 1 => 2, + 2 => 1, + n => n, + }; + } + set_golden_index(&mut f, &ix); + add("index-string-order", "index-policy-mismatch", sealed(f)); + let mut f = g.clone(); + let mut ix = golden_index(&f); + let p = ix.plan[1].iter().position(|&v| v == wsix::FLOOR_ONLY).expect("a floor-only piece of class 1"); + ix.plan[1][p] = 0; + set_golden_index(&mut f, &ix); + add("index-class-plan", "index-policy-mismatch", sealed(f)); + // The policy's own refusals: a class named classic; under royalty_free_only, a + // patent-pool floor; a component of another medium than the receipt's. + let mut f = g.clone(); + policy_edit(&mut f, "id = \"legacy\"", "id = \"classic\""); + let mut ix = golden_index(&f); + let at = ix.strings.iter().position(|s| s == "legacy").expect("the class legacy"); + ix.strings[at] = "classic".into(); + set_golden_index(&mut f, &ix); + add("class-named-classic", "pack-id", sealed(f)); + // The same in another case: class ids name paths, and paths fold case (§11.1). Its + // master playlist would be the classical master's path folded, which no listing + // carries beside it (and no case-folding file system holds apart), so it is left out: + // the policy is refused before any catalog is read. + let mut f = g.clone(); + policy_edit(&mut f, "id = \"legacy\"", "id = \"Classic\""); + let mut ix = golden_index(&f); + let at = ix.strings.iter().position(|s| s == "legacy").expect("the class legacy"); + ix.strings[at] = "Classic".into(); + set_golden_index(&mut f, &ix); + let mut f = sealed(f); + f.retain(|k, _| !k.starts_with("hls/Classic/")); + add("class-named-classic-in-capitals", "pack-id", listed(f)); + let mut f = g.clone(); + policy_edit(&mut f, "capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.video.avc\"\nrole = \"floor\""); + add("patent-pool-floor", "license-refused:wai.video.avc:patent-pool", sealed(f)); + // A patent-pool floor policy.toml allows, refused under the royalty_free_only the + // package was packed with: policy.json's, the packer's override (§3.2). + let mut f = g.clone(); + policy_edit(&mut f, "royalty_free_only = true\n", ""); + policy_edit(&mut f, "capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.video.avc\"\nrole = \"floor\""); + add("patent-pool-floor-by-override", "license-refused:wai.video.avc:patent-pool", sealed(f)); + // A policy.json that turns off the royalty_free_only policy.toml asks for, over a + // patent-pool floor, the package otherwise whole (its floor objects and index + // relabelled to that capability) and sealed: an override never turns it off (§3.2). + let mut f = g.clone(); + policy_edit(&mut f, "capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.video.avc\"\nrole = \"floor\""); + let mut ix = golden_index(&f); + let at = ix.strings.iter().position(|s| s == "wai.video.av1").expect("the floor's capability"); + ix.strings[at] = "wai.video.avc".into(); + set_golden_index(&mut f, &ix); + for p in 0..ix.pieces.len() { + let (k, b) = floor_entry(&f, p); + let (mut man, hdr, units) = floor_parts(&b); + man["model_requirement"]["capability"] = "wai.video.avc".into(); + man["conditioning"]["kind"] = "avc".into(); + replace_object(&mut f, k, floor_envelope(&man, hdr, &units)); + } + let asked = crate::policy::Policy::parse_bytes(&f["policy.toml"]).expect("the policy parses"); + assert!(asked.royalty_free_only, "the golden asks for royalty-free packaging"); + let relaxed = asked.canonical_json().replace("\"royalty_free_only\":true", "\"royalty_free_only\":false"); + reseal_as(&mut f, key, Some(relaxed.as_bytes())).expect("the alteration re-seals"); + add("policy-json-relaxes-royalty-free", "policy-json-mismatch", f); + let mut f = g.clone(); + policy_edit(&mut f, "media = \"video\"", "media = \"image\""); + add("component-medium", "policy-component-mismatch", sealed(f)); + // Malformed packages, refused and never panicked on: a floor object removed, one + // corrupted, a live window whose pieces start past 2^53 − 1 (no stage view can be + // written). + let mut f = g.clone(); + let (k, _) = floor_entry(&f, 1); + f.remove(&objects::object_path(&golden_index(&f).entries[k].content_hash)); + add("floor-object-missing", "object-missing", listed(f)); + let mut f = g.clone(); + let (k, mut b) = floor_entry(&f, 2); + *b.last_mut().expect("bytes") ^= 1; + f.insert(objects::object_path(&golden_index(&f).entries[k].content_hash), b); + add("floor-object-corrupt", "object-hash", listed(f)); + let mut f = g.clone(); + let mut ix = golden_index(&f); + ix.header.prev_index_digest = [3; 32]; + for p in ix.pieces.iter_mut() { + p.start += 1u64 << 53; + } + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + ix.header.plan_digest = wsix::plan_digest(&starts); + set_golden_index(&mut f, &ix); + f.insert("plan-v.txt".into(), crate::pack::plan_file(&starts).into_bytes()); + add("live-window-past-2-53", "catalog-mismatch", sealed(f)); + // The floor of piece 2: opening on an inter frame, with no key frame anywhere in it; + // a key frame inside it, refused unless the policy allows extra key frames; a + // sequence header not the first's bytes; the framing header, renderer and settings, + // and target not §7.2's; presenting another piece's base. + let (k, b) = floor_entry(g, 2); + let (man, hdr, units) = floor_parts(&b); + let with = |m: &Value, h: crate::floor::Av1Header, u: &[Vec]| { + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(m, h, u)); + f + }; + let mut inter = units.clone(); + inter[0] = units[1].clone(); + add("floor-opens-on-inter", "floor-keyframe-mismatch", sealed(with(&man, hdr, &inter))); + let mut extra = units.clone(); + extra[5] = units[0].clone(); + add("floor-extra-key-frame", "floor-keyframe-mismatch", sealed(with(&man, hdr, &extra))); + let mut f = with(&man, hdr, &extra); + policy_edit(&mut f, "[floor]\n", "[floor]\nextra_keyframes = true\n"); + add("floor-extra-key-frame-allowed", "ok", sealed(f)); + let mut other = units.clone(); + let tu = crate::av1::temporal_units(&units[0]).expect("a unit").remove(0); + let sh = tu.sequence_header().expect("a sequence header").bytes.to_vec(); + let at = other[0].windows(sh.len()).position(|w| w == sh.as_slice()).expect("the header's bytes"); + other[0][at + sh.len() - 1] ^= 0x01; + add("floor-sequence-header", "floor-av1-malformed", sealed(with(&man, hdr, &other))); + add("floor-framing-header", "floor-object-mismatch", sealed(with(&man, (1080, 1920, 30000, 1001), &units))); + let mut m = man.clone(); + m["derived_from"]["renderer"] = "anything@1".into(); + m["derived_from"]["settings"] = "00".repeat(32).into(); + add("floor-renderer-settings", "floor-object-mismatch", sealed(with(&m, hdr, &units))); + let mut m = man.clone(); + m["target"] = json!({"frames": 1, "h": 1, "w": 1}); + add("floor-target", "floor-object-mismatch", sealed(with(&m, hdr, &units))); + let ix = golden_index(g); + let other = ix.piece_entries(0).iter().find(|e| e.role == Role::Base).expect("a base").content_hash; + let mut m = man.clone(); + m["derived_from"]["motif"] = hex(&other).into(); + add("floor-motif", "floor-motif-mismatch", sealed(with(&m, hdr, &units))); + // A CMAF file that is not the one the floor's temporal units give. + let mut f = g.clone(); + let init = f.get_mut("floor/v/init.mp4").expect("the initialization segment"); + let n = init.len(); + init[n - 1] ^= 1; + add("floor-cmaf", "floor-cmaf-mismatch", listed(f)); + // Files read strictly: a dot-name, which no listing carries, at the top, in a + // directory of its own and among the objects; and a `*.sha256` file, listed, which + // no package has. + for (name, path) in [("dot-file", ".hidden"), ("dot-directory", ".cache/blob"), ("dot-object", "objects/.x.wai")] { + let mut f = g.clone(); + f.insert(path.into(), b"anything".to_vec()); + add(name, "unexpected-file", f); + } + let mut f = g.clone(); + f.insert("payload.sha256".into(), b"anything".to_vec()); + add("sha256-file", "unexpected-file", listed(f)); + // The receipt file, not its canonical form; a member named twice. + let text = String::from_utf8(g["package.receipt.json"].clone()).expect("UTF-8"); + let v: Value = serde_json::from_str(&text).expect("JSON"); + let dup = text.replacen("{\"asset_id\"", "{\"asset_id\":\"video-base-01\",\"asset_id\"", 1); + for (name, bad) in [("receipt-pretty", serde_json::to_string_pretty(&v).expect("JSON")), ("receipt-newline", format!("{text}\n")), ("receipt-duplicate-member", dup)] { + let mut f = g.clone(); + f.insert("package.receipt.json".into(), bad.into_bytes()); + add(name, "receipt-json", listed(f)); + } + // Directories (§11.1, "no other"): one that holds nothing, one that holds nothing + // under a dot-name (a version-control directory's), and one that cannot be read. + let dir = |name: &'static str, empty: &[&str], unreadable: &[&str]| PackageMutation { + name, + reason: "unexpected-file".into(), + files: g.clone(), + empty_dirs: empty.iter().map(|d| d.to_string()).collect(), + unreadable_dirs: unreadable.iter().map(|d| d.to_string()).collect(), + }; + out.push(dir("empty-directory", &["hls/empty"], &[])); + out.push(dir("empty-dot-directory", &[".git/refs"], &[])); + out.push(dir("unreadable-directory", &[], &["objects/locked"])); + out +} + +static CASE_DIRS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +fn set_unreadable(path: &Path, unreadable: bool) -> std::io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(path, std::fs::Permissions::from_mode(if unreadable { 0o000 } else { 0o755 }))?; + } + #[cfg(not(unix))] + let _ = (path, unreadable); + Ok(()) +} + +/// The first reason the package verifier gives a package case written out as a +/// directory (its files, its directories that hold nothing, and those it makes +/// unreadable) and read strictly, or `ok`. The directory is removed again. +pub fn package_case_verdict(files: &Files, empty_dirs: &[String], unreadable_dirs: &[String]) -> std::io::Result { + let n = CASE_DIRS.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let d = std::env::temp_dir().join(format!("wai-pack-case-{}-{n}", std::process::id())); + if d.exists() { + std::fs::remove_dir_all(&d)?; + } + write_files(&d, files)?; + for e in empty_dirs.iter().chain(unreadable_dirs) { + std::fs::create_dir_all(d.join(e))?; + } + for u in unreadable_dirs { + set_unreadable(&d.join(u), true)?; + } + let v = match crate::verify::verify_dir(&d) { + Ok(_) => "ok".to_string(), + Err(v) => v.into_iter().next().map_or_else(|| "refused".into(), |r| r.code), + }; + for u in unreadable_dirs { + set_unreadable(&d.join(u), false)?; + } + std::fs::remove_dir_all(&d)?; + Ok(v) +} + +/// The package mutations as overlays on the golden: per case, the files that differ or +/// are new under `package-mutations//`, and in `expected.json` the files it +/// removes, the directories it adds that hold nothing (`empty_dirs`) or cannot be read +/// (`unreadable_dirs`), and its reason. +pub fn package_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { + let mut files = BTreeMap::new(); + let mut cases = Vec::new(); + for m in package_mutations(g, key) { + for (p, b) in &m.files { + if g.get(p) != Some(b) { + files.insert(format!("package-mutations/{}/{p}", m.name), b.clone()); + } + } + let remove: Vec<&String> = g.keys().filter(|p| !m.files.contains_key(*p)).collect(); + let mut c = json!({"name": m.name, "reason": m.reason, "remove": remove}); + if !m.empty_dirs.is_empty() { + c["empty_dirs"] = json!(m.empty_dirs); + } + if !m.unreadable_dirs.is_empty() { + c["unreadable_dirs"] = json!(m.unreadable_dirs); + } + cases.push(c); + } + (files, json!({"pack": MUTATED_PACK, "cases": cases})) +} + +/// A package case's files: the golden, less what it removes, with its overlay. +pub fn apply_overlay(g: &Files, overlay: &Files, name: &str, remove: &[String]) -> Files { + let mut f = g.clone(); + for p in remove { + f.remove(p); + } + let prefix = format!("{name}/"); + for (p, b) in overlay { + if let Some(rest) = p.strip_prefix(&prefix) { + f.insert(rest.to_string(), b.clone()); + } + } + f +} + +// ─────────────────────────────── version-1 receipts ─────────────────────────────── + +/// The version-1 receipts and their JSON: the legacy receipt `wai-rs` pins +/// (`71a9da59…`), and an unmetered one with a parent. +#[allow(deprecated)] // the legacy label is the point of the first +pub fn v1_receipts() -> (Files, Value) { + use wai::wai_video_package::{segment, PackageReceipt}; + let src: Vec<(&[u8], u64)> = vec![(b"content-gop-0000", 2000), (b"AD-creative-0001", 2000), (b"content-gop-0002", 2000), (b"AD-creative-0001", 2000)]; + let g = GrantRef::unbounded("video.package"); + let legacy = PackageReceipt::seal(&SigningKey::from_bytes(&[1; 32]), "did:key:pkg", "asset-abc", segment(&src, 2000), 12_000, Some(EnergyProvenance::HwShunt), g.clone(), None); + let unmetered = PackageReceipt::seal(&SigningKey::from_bytes(&[2; 32]), "o", "asset", segment(&src, 2000), 0, None, g, Some([3; 32])); + let mut files = BTreeMap::new(); + let mut cases = Vec::new(); + for (name, r) in [("v1-legacy", legacy), ("v1-unmetered", unmetered)] { + let path = format!("{name}.json"); + files.insert(path.clone(), r.to_json().expect("a JSON form").into_bytes()); + cases.push(json!({"file": path, "receipt_hash": hex(&r.receipt_hash())})); + } + (files, json!({"cases": cases})) +} + +// ─────────────────────────────── regen and check ─────────────────────────────── + +fn write_files(dir: &Path, files: &Files) -> std::io::Result<()> { + for (p, b) in files { + let path = dir.join(p); + if let Some(d) = path.parent() { + std::fs::create_dir_all(d)?; + } + std::fs::write(path, b)?; + } + Ok(()) +} + +fn write_json(p: &Path, v: &Value) -> std::io::Result<()> { + let mut s = serde_json::to_string_pretty(v).expect("JSON"); + s.push('\n'); + std::fs::write(p, s) +} + +fn read_tree(dir: &Path) -> std::io::Result { + wai::pin::read_dir_set(dir).map_err(io) +} + +/// Write the package corpora under `dir`. +pub fn regen(dir: &Path) -> std::io::Result<()> { + for name in PACKS { + let out = pack_golden(dir, name)?; + let pkg = dir.join("packs").join(name).join("package"); + if pkg.exists() { + std::fs::remove_dir_all(&pkg)?; + } + out.write(&pkg)?; + } + let (files, expected) = mutations(); + let m = dir.join("mutations"); + if m.exists() { + std::fs::remove_dir_all(&m)?; + } + write_files(&m, &files)?; + write_json(&m.join("expected.json"), &expected)?; + let (files, expected) = v1_receipts(); + let r = dir.join("receipts"); + if r.exists() { + std::fs::remove_dir_all(&r)?; + } + write_files(&r, &files)?; + write_json(&r.join("expected.json"), &expected)?; + let (files, expected) = catalogs(); + write_files(dir, &files)?; + write_json(&dir.join("catalogs/expected.json"), &expected)?; + let golden = read_tree(&dir.join("packs").join(MUTATED_PACK).join("package"))?; + let key = read_key(&dir.join("packs").join(MUTATED_PACK).join("key.hex"))?; + let (files, expected) = package_overlays(&golden, &key); + let pm = dir.join("package-mutations"); + if pm.exists() { + std::fs::remove_dir_all(&pm)?; + } + write_files(dir, &files)?; + write_json(&pm.join("expected.json"), &expected)?; + Ok(()) +} + +fn same_tree(what: &str, want: &Files, got: &Files, out: &mut Vec) { + for (p, b) in want { + match got.get(p) { + None => out.push(format!("{what}/{p}: not in the corpus")), + Some(g) if g != b => out.push(format!("{what}/{p}: not the bytes this implementation writes")), + _ => {} + } + } + for p in got.keys() { + if !want.contains_key(p) { + out.push(format!("{what}/{p}: not a file this implementation writes")); + } + } +} + +/// Recompute the package corpora under `dir` and list every difference. +pub fn check(dir: &Path) -> std::io::Result> { + let mut out = Vec::new(); + for name in PACKS { + let pkg = dir.join("packs").join(name).join("package"); + let held = read_tree(&pkg)?; + match pack_golden(dir, name) { + Ok(p) => same_tree(&format!("packs/{name}/package"), &p.files, &held, &mut out), + Err(e) => out.push(format!("packs/{name}: {e}")), + } + if let Err(v) = crate::verify::verify_files(&held) { + for r in v { + out.push(format!("packs/{name}/package: {r}")); + } + } + } + let m = dir.join("mutations"); + let mut held = read_tree(&m)?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, expected) = mutations(); + same_tree("mutations", &files, &held, &mut out); + if expected_held != expected { + out.push("mutations/expected.json: the corpus and this implementation differ".into()); + } + for c in expected["cases"].as_array().into_iter().flatten() { + match case_verdict(c, &held) { + Ok(v) if v == c["reason"] => {} + Ok(v) => out.push(format!("mutations {}: {v}, the corpus says {}", c["name"], c["reason"])), + Err(e) => out.push(e), + } + } + let r = dir.join("receipts"); + let mut held = read_tree(&r)?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, expected) = v1_receipts(); + same_tree("receipts", &files, &held, &mut out); + if expected_held != expected { + out.push("receipts/expected.json: the corpus and this implementation differ".into()); + } + for c in expected["cases"].as_array().into_iter().flatten() { + let f = c["file"].as_str().unwrap_or(""); + let ok = held.get(f).and_then(|b| std::str::from_utf8(b).ok()).and_then(|t| wai::wai_video_package::PackageReceipt::from_json(t).ok()).is_some_and(|r| r.verify() && hex(&r.receipt_hash()) == c["receipt_hash"]); + if !ok { + out.push(format!("receipts/{f}: does not read back to a receipt that verifies with its hash")); + } + } + let mut held = read_tree(&dir.join("catalogs"))?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, want) = catalogs(); + let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches("catalogs/").to_string(), v)).collect(); + same_tree("catalogs", &files, &held, &mut out); + if expected_held != want { + out.push("catalogs/expected.json: the corpus and this implementation differ".into()); + } + // The package mutations, read strictly (their dot-names and `*.sha256` files are + // cases), each refused with its reason. + let golden = read_tree(&dir.join("packs").join(MUTATED_PACK).join("package"))?; + let key = read_key(&dir.join("packs").join(MUTATED_PACK).join("key.hex"))?; + let mut held = crate::verify::read_strict(&dir.join("package-mutations")).map_err(io)?; + let expected_held: Value = serde_json::from_slice(&held.remove("expected.json").unwrap_or_default()).map_err(io)?; + let (files, want) = package_overlays(&golden, &key); + let files: Files = files.into_iter().map(|(k, v)| (k.trim_start_matches("package-mutations/").to_string(), v)).collect(); + same_tree("package-mutations", &files, &held, &mut out); + if expected_held != want { + out.push("package-mutations/expected.json: the corpus and this implementation differ".into()); + } + let strings = |v: &Value| -> Vec { v.as_array().into_iter().flatten().filter_map(|p| p.as_str().map(String::from)).collect() }; + for c in want["cases"].as_array().into_iter().flatten() { + let name = c["name"].as_str().unwrap_or(""); + let files = apply_overlay(&golden, &held, name, &strings(&c["remove"])); + let v = package_case_verdict(&files, &strings(&c["empty_dirs"]), &strings(&c["unreadable_dirs"]))?; + if v != c["reason"] { + out.push(format!("package-mutations {name}: {v}, the corpus says {}", c["reason"])); + } + } + Ok(out) +} diff --git a/wai/wai-pack/src/lib.rs b/wai/wai-pack/src/lib.rs index 7fec099e5..f868e5bcc 100644 --- a/wai/wai-pack/src/lib.rs +++ b/wai/wai-pack/src/lib.rs @@ -13,14 +13,23 @@ #![deny(clippy::float_arithmetic)] +pub mod av1; pub mod capcheck; +pub mod catalog; pub mod classify; +pub mod cmaf; pub mod conformance; pub mod encode; pub mod fixture; +pub mod floor; +pub mod golden; +pub mod objects; +pub mod pack; +pub mod plan; pub mod policy; pub mod scenecut; pub mod source; +pub mod verify; /// Lowercase hex of `b`. pub fn hex(b: &[u8]) -> String { diff --git a/wai/wai-pack/src/objects.rs b/wai/wai-pack/src/objects.rs new file mode 100644 index 000000000..a7226d212 --- /dev/null +++ b/wai/wai-pack/src/objects.rs @@ -0,0 +1,68 @@ +//! The objects a packager writes ([packaging] §5): each exactly one `WAI1` envelope, +//! named by its content hash (jwp-receipts §2.1) as `objects/<64 hex>.wai`. No manifest +//! names a piece, a time, an asset, a class or a component, so two pieces with the same +//! content are one object. +//! +//! - a base: `media` the component's class, `intent` `replicate`, the rung's capability, +//! `conditioning.kind` its short codec id, and `target` the frames, height and width; +//! - a floor: the same for its capability, with `derived_from` naming the base it +//! presents (SPEC §7.1, *Derived renditions*). +//! +//! [packaging]: ../../../extensions/packaging.md + +use serde_json::json; +use wai::container::{Conditioning, DerivedFrom, Manifest, ModelRequirement, Wai}; + +/// The renderer a supplied floor names: `supplied@`. +pub fn supplied_renderer(listing: &str) -> String { + format!("supplied@{listing}") +} + +/// `BLAKE3("wai:render-settings\x01" ‖ JCS(settings))` (§3.6, SPEC §7.1). +pub fn settings_digest(settings: &serde_json::Value) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DerivedFrom::SETTINGS_DOMAIN); + h.update(wai::jcs::jcs(settings).expect("render settings are in the canonical subset").as_bytes()); + *h.finalize().as_bytes() +} + +/// The content hash that names an object. +pub fn object_hash(envelope: &[u8]) -> [u8; 32] { + wai::jwp_hash::content_hash(envelope) +} + +/// The path an object is written at. +pub fn object_path(hash: &[u8; 32]) -> String { + format!("objects/{}.wai", crate::hex(hash)) +} + +fn kind_of(cap: &str) -> &str { + cap.rsplit('.').next().unwrap_or(cap) +} + +/// A base object of `cap` over `frames` frames of `h × w`. +pub fn base_envelope(media: &str, cap: &str, payload: Vec, frames: u64, h: u32, w: u32) -> Vec { + let m = Manifest::new(media, "replicate", ModelRequirement::new(cap), Conditioning::new(kind_of(cap))).with_target(json!({"frames": frames, "h": h, "w": w})); + Wai::new(m, payload).to_bytes().expect("a manifest serialises") +} + +/// A floor object of `cap` presenting `derived_from.motif`. +pub fn floor_envelope(media: &str, cap: &str, payload: Vec, frames: u64, h: u32, w: u32, derived_from: &DerivedFrom) -> Vec { + let m = Manifest::new(media, "replicate", ModelRequirement::new(cap), Conditioning::new(kind_of(cap))) + .with_derived_from(derived_from) + .with_target(json!({"frames": frames, "h": h, "w": w})); + Wai::new(m, payload).to_bytes().expect("a manifest serialises") +} + +/// The `derived_from` of a supplied floor's object: the base it presents, the supplied +/// listing as its renderer, and the settings `{"source": "supplied", "supplied_listing": +/// }`. +pub fn supplied_derived_from(motif: &[u8; 32], motif_capability: &str, listing: &str) -> DerivedFrom { + let settings = json!({"source": "supplied", "supplied_listing": listing}); + DerivedFrom { + motif: crate::hex(motif), + motif_capability: motif_capability.to_string(), + renderer: supplied_renderer(listing), + settings: crate::hex(&settings_digest(&settings)), + } +} diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs new file mode 100644 index 000000000..b0ec71339 --- /dev/null +++ b/wai/wai-pack/src/pack.rs @@ -0,0 +1,451 @@ +//! The packer ([packaging] §4–§9): a policy and its sources in, a package out, every +//! file a pure function of the source bytes, the effective policy, the supplied floor, the +//! signing key and the packager's identity, except the energy figure and the measurement +//! claim of a metered pack. +//! +//! This revision packs one video component with a `wai.video.int_motion` base (its step +//! chosen per piece by [`crate::encode::choose_q`]) and a supplied AV1 floor. What it +//! does not pack yet it refuses by name rather than leaving out: another medium +//! (`pack-media-unsupported`), refinement rungs (`pack-refinement-unsupported`), a rule's +//! ladder subset (`pack-rule-ladder-unsupported`), a floor to encode +//! (`pack-floor-encode-unsupported`), a rung of a capability it does not encode +//! (`pack-capability-unsupported:`). Its own failures are refused by name too +//! (`pack-encode`, `pack-piece-length`, `pack-catalog`, `pack-meter`, +//! `pack-measurement-claim`, `pack-listing`; packaging §11.4), and a package is written +//! only into a new or empty directory ([`out_dir_ready`], `pack-out-not-empty`). +//! +//! The package: +//! +//! ```text +//! policy.toml policy.json plan-.txt index-.wsix package.receipt.json +//! objects/.wai … +//! floor//init.mp4 floor//.m4s … (CMAF; never in an index) +//! hls/classic/… hls//… dash/… moq/catalog.json stages/.json +//! claims/.measurement.json (a metered pack) +//! LISTING (SPEC §3.1, of every other file) +//! ``` +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::catalog::{self, CompView, FloorView}; +use crate::classify::{self, PieceKind}; +use crate::encode::{choose_q, MotionParams}; +use crate::floor::{self, Floor}; +use crate::objects; +use crate::plan::{class_plan, plan_piece}; +use crate::policy::{Media, Overrides, Policy, Role as RungRole, Rung}; +use crate::scenecut::{SegParams, Segmenter}; +use ed25519_dalek::SigningKey; +use std::collections::BTreeMap; +use wai::package::{PackageReceiptV2, Parent}; +use wai::quantum_ops::GrantRef; +use wai::stage_index::{self as wsix, ClassRec, EntryRec, Header, MediaClass, PieceRec, Role, StageIndex}; + +/// The packager's identity, `builtin:@`. +pub fn packager_id() -> String { + format!("builtin:{}@{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")) +} + +/// Why nothing was packed: a reason of §12 and what failed. Nothing is written. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PackError { + pub code: String, + pub detail: String, +} + +impl PackError { + fn new(code: impl Into, detail: impl Into) -> PackError { + PackError { code: code.into(), detail: detail.into() } + } +} + +impl std::fmt::Display for PackError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.detail) + } +} + +impl std::error::Error for PackError {} + +/// What a pack is made with beside the policy and its sources. +pub struct PackOpts { + pub signer: SigningKey, + pub signer_id: String, + /// The supplied floor's files, by listing path. + pub floor: BTreeMap>, + /// The override a command line applies (§3.2). + pub overrides: Overrides, + /// The meter that brackets the pack, or none (the unmetered form). + pub meter: Option>, + /// What a meter's report names as its power reader. + pub power_reader: String, + /// The `issued_at` of a measurement claim (RFC 3339, UTC). + pub issued_at: String, + pub grant: GrantRef, +} + +impl PackOpts { + /// Unmetered options with the grant `wai.video.package` and no ceilings. + pub fn new(signer: SigningKey, signer_id: impl Into, floor: BTreeMap>) -> PackOpts { + PackOpts { + signer, + signer_id: signer_id.into(), + floor, + overrides: Overrides::default(), + meter: None, + power_reader: String::new(), + issued_at: String::new(), + grant: GrantRef::unbounded("wai.video.package"), + } + } +} + +/// A package: its files by path, and its receipt. +#[derive(Debug, Clone)] +pub struct PackOutput { + pub files: BTreeMap>, + pub receipt: PackageReceiptV2, +} + +impl PackOutput { + /// Write every file below `dir`. + pub fn write(&self, dir: &std::path::Path) -> std::io::Result<()> { + for (p, b) in &self.files { + let path = dir.join(p); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, b)?; + } + Ok(()) + } + + /// The SHA-256 of `LISTING`: the pack digest. + pub fn pack_digest(&self) -> String { + crate::sha256_hex(&self.files["LISTING"]) + } +} + +/// Whether `dir` may take a package: a directory that does not exist yet, or one that +/// holds nothing (`pack-out-not-empty` otherwise, §11.4). A package written among another +/// one's files would leave the files it does not write behind (a measurement claim, an +/// object no entry names), and would not verify. +pub fn out_dir_ready(dir: &std::path::Path) -> Result<(), PackError> { + let refuse = |why: String| Err(PackError::new("pack-out-not-empty", format!("{}: {why}", dir.display()))); + match std::fs::read_dir(dir) { + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(e) => refuse(e.to_string()), + Ok(mut entries) => match entries.next() { + None => Ok(()), + Some(_) => refuse("not empty; a package is written only into a new or empty directory".into()), + }, + } +} + +/// Whether `id` can name a file: one listing-path segment (SPEC §3.1). +pub fn id_is_path_segment(id: &str) -> bool { + let b = id.as_bytes(); + let first = |c: u8| c.is_ascii_alphanumeric() || c == b'_' || c == b'-'; + !b.is_empty() && first(b[0]) && b.iter().all(|&c| first(c) || c == b'.') && b[b.len() - 1] != b'.' +} + +/// The SHA-256 clip digest of frames (SPEC §7, *Digests*): the SHA-256 of their RGB8 +/// SHA-256 digests, concatenated in order. +pub fn clip_digest>(frames: &[F]) -> [u8; 32] { + use sha2::Digest as _; + let mut h = sha2::Sha256::new(); + for f in frames { + h.update(wai::output_tree::out_digest(f.as_ref())); + } + h.finalize().into() +} + +/// An object's manifest and payload lengths. Where WAI checks its payload's format, they +/// are its counted bytes (staged-measurement §3, `measure_framing::count_object`), which +/// also checks the payload parses as its capability's pinned format; a floor's payload, +/// whose format WAI does not check, is read from the envelope's length fields. +pub fn object_lengths(env: &[u8]) -> Result<(u32, u64), String> { + let cap = wai::container::Wai::from_bytes(env).map_err(|e| e.to_string())?.manifest.model_requirement.capability; + if wai::measure_framing::has_format_check(&cap) { + let c = wai::measure_framing::count_object(env).map_err(|e| e.to_string())?; + return Ok((c.manifest as u32, c.payload)); + } + let man = u32::from_le_bytes(env[4..8].try_into().expect("an envelope")); + Ok((man, env.len() as u64 - 12 - man as u64)) +} + +fn rungs_of<'a>(p: &'a Policy, comp: &str, role: RungRole) -> Vec<&'a Rung> { + p.ladder.iter().filter(|r| r.component == comp && r.role == role).collect() +} + +/// An entry describing `env`. +#[allow(clippy::too_many_arguments)] +fn entry(env: &[u8], piece: u64, capability: u16, role: Role, prefix_tier: u8, prefix_digest: [u8; 32], sse: u64, flags: u8) -> Result { + let (man, payload_len) = object_lengths(env).map_err(|e| PackError::new("pack-encode", format!("an object the packer wrote is not countable: {e}")))?; + Ok(EntryRec { + content_hash: objects::object_hash(env), + byte_len: env.len() as u64, + piece, + refines: wsix::NO_REFINES, + capability, + role, + layer: 0, + prefix_tier, + flags, + deadline_ms: 0, + prefix_digest, + sse, + manifest_len: man, + payload_len, + }) +} + +/// A component's CMAF files: the initialization segment's path and bytes, and each +/// piece's fragment's. +pub type CmafFiles = (String, Vec, Vec<(String, Vec)>); + +/// The floor's CMAF files of a component: the initialization segment and each piece's +/// fragment, by path. `floor` is the checked floor, `starts` the piece starts. +pub fn cmaf_files(comp: &str, floor: &Floor, starts: &[u64], tb_den: u32, timescale: u32) -> Result { + let init = crate::cmaf::write_init_av1(&floor.seq, &floor.seq_obu, floor.seq.max_frame_width, floor.seq.max_frame_height, timescale); + let mut frags = Vec::with_capacity(floor.pieces.len()); + for (p, piece) in floor.pieces.iter().enumerate() { + let mut samples = Vec::with_capacity(piece.units.len()); + for (u, k) in piece.units.iter().zip(&piece.key) { + let tus = crate::av1::temporal_units(u).map_err(|e| e.to_string())?; + let [tu] = tus.as_slice() else { return Err("a floor unit is not one temporal unit".into()) }; + samples.push(crate::cmaf::Sample { data: tu.sample(), duration: tb_den, sync: *k }); + } + let tfdt = starts[p].checked_mul(tb_den as u64).ok_or("a fragment's decode time past 2^64 - 1")?; + let f = crate::cmaf::write_fragment(p as u32 + 1, tfdt, &samples); + frags.push((format!("floor/{comp}/{}.m4s", crate::sha256_hex(&f)), f)); + } + Ok((format!("floor/{comp}/init.mp4"), init, frags)) +} + +/// The keyframe plan's file: each piece start, one decimal per line. +pub fn plan_file(starts: &[u64]) -> String { + starts.iter().map(|s| format!("{s}\n")).collect() +} + +/// Pack `policy_bytes` over `sources` (each component's source bytes, by id). +pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut PackOpts) -> Result { + let policy = Policy::parse_bytes(policy_bytes).map_err(|e| PackError::new(e.code(), e.to_string()))?.effective(&opts.overrides); + // The policy's own refusals, in the order a package's verifier applies them (§11.3). + if let Some(r) = crate::verify::policy_refusal(&policy) { + return Err(PackError::new(r.code, r.detail)); + } + let [comp] = policy.components.as_slice() else { + return Err(PackError::new("pack-media-unsupported", "this revision packs one component")); + }; + if comp.media != Media::Video { + return Err(PackError::new("pack-media-unsupported", format!("component {} is {}", comp.id, comp.media.label()))); + } + if !rungs_of(&policy, &comp.id, RungRole::Refinement).is_empty() { + return Err(PackError::new("pack-refinement-unsupported", "refinement rungs are packed by a later revision")); + } + if policy.rules.iter().any(|r| r.then.ladder.is_some()) { + return Err(PackError::new("pack-rule-ladder-unsupported", "a rule's ladder subset is packed by a later revision")); + } + let bases = rungs_of(&policy, &comp.id, RungRole::Base); + let [base] = bases.as_slice() else { + let extra = bases.get(1).map_or("", |r| r.capability.as_str()); + return Err(PackError::new(format!("pack-capability-unsupported:{extra}"), "this revision packs one base rung")); + }; + let mp = MotionParams::of(base).ok_or_else(|| PackError::new(format!("pack-capability-unsupported:{}", base.capability), "the base rung"))?; + let floors = rungs_of(&policy, &comp.id, RungRole::Floor); + let [floor_rung] = floors.as_slice() else { + return Err(PackError::new("floor-missing", format!("component {} needs one floor rung (§7.1)", comp.id))); + }; + if !matches!(floor_rung.capability.as_str(), "wai.video.av1" | "wai.video.av1.lossless") { + return Err(PackError::new(format!("pack-capability-unsupported:{}", floor_rung.capability), "the floor rung")); + } + if floor_rung.params.get("source").and_then(|v| v.as_str()) != Some("supplied") { + return Err(PackError::new("pack-floor-encode-unsupported", "this revision packs a supplied floor")); + } + let tb = comp.timebase; + let src = sources.get(&comp.id).ok_or_else(|| PackError::new("pack-source-missing", format!("no source for component {}", comp.id)))?; + let (hd, frames) = crate::source::y4m::read_all(src).map_err(|e| PackError::new(e.code(), e.to_string()))?; + if hd.rate.is_some_and(|(n, d)| (n, d) != (tb.num, tb.den)) { + return Err(PackError::new("pack-source-rate", "the source's frame rate is not the component's timebase")); + } + if frames.is_empty() { + return Err(PackError::new("y4m-truncated", "a source of no frames")); + } + let (w, h) = (hd.w, hd.h); + let mut seg = Segmenter::new(SegParams::from_policy(&policy.segmentation, tb), w, h); + let mut pieces = Vec::new(); + for f in &frames { + pieces.extend(seg.push(&f.rgb)); + } + pieces.extend(seg.finish()); + let starts: Vec = pieces.iter().map(|p| p.start).collect(); + let floor_listing = policy.floor.supplied_listing.clone(); + let fl = floor::floor_supplied(&opts.floor, floor_listing.as_deref(), &comp.id, &starts, frames.len() as u64, policy.floor.extra_keyframes) + .map_err(|r| PackError::new(r.code, r.detail))?; + let listing = floor_listing.expect("checked by floor_supplied"); + if let Some(m) = opts.meter.as_mut() { + m.start(); + } + + // The string table: the component, the classes, then capabilities by first use. + let mut strings: Vec = vec![comp.id.clone()]; + strings.extend(policy.classes.iter().map(|c| c.id.clone())); + let mut intern = |s: &str| -> u16 { + match strings.iter().position(|x| x == s) { + Some(i) => i as u16, + None => { + strings.push(s.to_string()); + (strings.len() - 1) as u16 + } + } + }; + let base_cap = intern(&base.capability); + let floor_cap = intern(&floor_rung.capability); + let mut objects: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); + let mut entries: Vec = Vec::new(); + let mut piece_recs: Vec = Vec::new(); + let mut layer_bytes: Vec<(Vec, Option)> = Vec::new(); + let (fw, fh) = (fl.seq.max_frame_width, fl.seq.max_frame_height); + let mut add = |env: Vec, entries: &mut Vec, e: EntryRec| { + let mut e = e; + if entries.iter().any(|x| x.content_hash == e.content_hash) { + e.flags |= wsix::ENTRY_DUP; + } + objects.insert(e.content_hash, env); + entries.push(e); + }; + for (p, piece) in pieces.iter().enumerate() { + let kind: PieceKind = classify::classify(&piece.stats, &policy.classify); + let pp = plan_piece(&policy, tb, piece.start, piece.len, kind); + let src_frames: Vec> = frames[piece.start as usize..(piece.start + piece.len) as usize].iter().map(|f| f.rgb.clone()).collect(); + let q = choose_q(&src_frames, h, w, &mp).map_err(|e| PackError::new("pack-encode", e.to_string()))?; + let n = src_frames.len() as u64; + let base_env = objects::base_envelope("video", &base.capability, q.encoded.wiv1, n, h, w); + let base_hash = objects::object_hash(&base_env); + let first = entries.len() as u32; + let tier = wsix::capability_prefix_tier(&base.capability); + let digest = if tier == wsix::TIER_DECODE { clip_digest(&q.encoded.recon) } else { [0u8; 32] }; + let be = entry(&base_env, p as u64, base_cap, Role::Base, tier, digest, q.encoded.sse, 0)?; + let base_len = be.byte_len; + add(base_env, &mut entries, be); + let df = objects::supplied_derived_from(&base_hash, &base.capability, &listing); + let payload = floor::av1_payload(&fl.pieces[p].units, fh, fw, tb.num, tb.den); + let floor_env = objects::floor_envelope("video", &floor_rung.capability, payload, n, fh, fw, &df); + let fe = entry(&floor_env, p as u64, floor_cap, Role::Floor, wsix::TIER_NONE, [0u8; 32], wsix::SSE_UNKNOWN, wsix::ENTRY_DERIVED_FROM)?; + add(floor_env, &mut entries, fe); + piece_recs.push(PieceRec { + start: piece.start, + duration: u32::try_from(piece.len).map_err(|_| PackError::new("pack-piece-length", "a piece past 2^32 - 1 units"))?, + first_entry: first, + n_entries: (entries.len() as u32 - first) as u16, + kind: kind as u8, + flags: piece.flags, + rules: pp.rules, + moments: pp.moments, + }); + layer_bytes.push((vec![base_len], pp.max_layer)); + } + let plan: Vec> = policy + .classes + .iter() + .map(|c| piece_recs.iter().zip(&layer_bytes).map(|(pr, (lb, rm))| class_plan(c, lb, *rm, pr.duration as u64, tb)).collect()) + .collect(); + let classes: Vec = policy + .classes + .iter() + .enumerate() + .map(|(i, c)| ClassRec { + name: (1 + i) as u16, + max_layer: c.max_layer.min(255) as u8, + flags: if c.floor_only { wsix::CLASS_FLOOR_ONLY } else { 0 }, + max_kbps: c.max_kbps.map_or(0, |k| k.min(u32::MAX as u64) as u32), + }) + .collect(); + let policy_digest = policy.digest(); + let index = StageIndex { + header: Header { + media_class: MediaClass::Video, + tb_num: tb.num, + tb_den: tb.den, + policy_digest, + prev_index_digest: [0u8; 32], + first_piece: 0, + first_entry: 0, + plan_digest: wsix::plan_digest(&starts), + component: 0, + }, + pieces: piece_recs, + entries, + classes, + plan, + strings, + }; + index.check().map_err(|r| PackError::new(r.code, format!("the packer wrote an index that does not check: {}", r.detail)))?; + let (init_path, init, frags) = cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num).map_err(|e| PackError::new("floor-av1-malformed", e))?; + let view = CompView { + index: &index, + tb, + w, + h, + objects: &objects, + floor: Some(FloorView { codec: fl.seq.codec_string(), w: fw, h: fh, init: init_path.clone(), fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect() }), + }; + let catalogs = catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &policy_digest).map_err(|e| PackError::new("pack-catalog", e.0))?; + let index_bytes = index.to_bytes(); + // A meter that was asked for and gives no figure (a span too short, a product past + // its range) refuses the pack: it never turns into an unmetered package. + let energy = match opts.meter.as_mut() { + None => wai::energy_meter::EnergyReading::unmetered(), + Some(m) => m.stop().ok_or_else(|| PackError::new("pack-meter", "the meter gave no figure for the pack"))?.as_seal_args(), + }; + let receipt = PackageReceiptV2::seal( + &opts.signer, + &opts.signer_id, + &policy.asset_id, + policy_digest, + &[(comp.id.clone(), index_bytes.clone())], + &packager_id(), + energy, + opts.grant.clone(), + Parent::None, + ) + .map_err(|r| PackError::new(r.code, r.detail))?; + let mut files: BTreeMap> = BTreeMap::new(); + files.insert("policy.toml".into(), policy_bytes.to_vec()); + files.insert("policy.json".into(), policy.canonical_json().into_bytes()); + files.insert(format!("plan-{}.txt", comp.id), plan_file(&starts).into_bytes()); + files.insert(wai::package::index_file(&comp.id), index_bytes.clone()); + for (h, env) in objects { + files.insert(objects::object_path(&h), env); + } + files.insert(init_path, init); + for (p, b) in frags { + files.insert(p, b); + } + files.extend(catalogs); + files.insert("package.receipt.json".into(), receipt.to_json().into_bytes()); + if receipt.energy_provenance.is_some() { + let claim = measurement_claim(&receipt, &index_bytes, opts).map_err(|e| PackError::new("pack-measurement-claim", e))?; + files.insert(claim_path(&receipt), claim.into_bytes()); + } + let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let listing = wai::pin::listing_from(&view).map_err(|e| PackError::new("pack-listing", e.to_string()))?; + files.insert("LISTING".into(), listing); + Ok(PackOutput { files, receipt }) +} + +/// Where a package's measurement claim is: `claims/.measurement.json`. +pub fn claim_path(r: &PackageReceiptV2) -> String { + format!("claims/{}.measurement.json", crate::hex(&r.receipt_hash())) +} + +/// The `measurement` claim about a metered receipt's figure (jwp-receipts §2.4.8), signed +/// with the package key and checked against the receipt before it is returned. +fn measurement_claim(r: &PackageReceiptV2, index: &[u8], opts: &PackOpts) -> Result { + use wai::energy_figure::{meter_report, EnergyFigures}; + let indexes: Vec<&[u8]> = vec![index]; + let view = (r, indexes.as_slice()).figure_view().ok_or("the receipt does not verify")?; + let claim = meter_report::emit(&view, None, None, wai::energy_report::BuildRecord::of_this_build(), &opts.power_reader, &opts.signer, &opts.signer_id, None, &opts.issued_at)?; + Ok(claim.to_json()) +} diff --git a/wai/wai-pack/src/plan.rs b/wai/wai-pack/src/plan.rs new file mode 100644 index 000000000..65418177a --- /dev/null +++ b/wai/wai-pack/src/plan.rs @@ -0,0 +1,128 @@ +//! What a piece is packed as, and what each class lists of it ([packaging] §8.1). +//! +//! - **Moments.** A piece of `[start, start + duration)` timebase units spans +//! `[s_ms, e_ms)` with `s_ms = floor(start · den · 1000 / num)` and `e_ms` the same of +//! its end; it lies in moment `i` when `s_ms < to_ms` and `e_ms > from_ms`. +//! - **Rules.** Rule `i` matches a piece when each condition it states holds: its kind is +//! in `when.piece_kind`, and a moment the piece lies in has a tag in `when.moment_tag`. +//! Matching rules fold in document order, a later `then` member overriding an earlier +//! one (§3.4). +//! - **Class plan.** For class `c` and piece `p`: a floor-only class lists the floor +//! (`0xFF`). Otherwise `listed = min(c.max_layer, the rules' max_layer, the piece's +//! highest layer)`; under `max_kbps`, while the listed prefix needs more than it, +//! `ceil(8 · bytes · num / (1000 · duration · den))` kilobits per second, the top +//! layer is dropped; a base that alone needs more makes the piece floor-only for the +//! class. +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::classify::PieceKind; +use crate::policy::{Class, Policy, Timebase}; +use wai::stage_index::FLOOR_ONLY; + +/// A span in timebase units as milliseconds, rounded down. +pub fn units_to_ms(units: u64, tb: Timebase) -> u64 { + u64::try_from(units as u128 * tb.den as u128 * 1000 / tb.num as u128).unwrap_or(u64::MAX) +} + +/// What the rules decide for one piece. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PiecePlan { + /// Bit `i`: rule `i` matched. + pub rules: u32, + /// Bit `i`: the piece lies in moment `i`. + pub moments: u32, + pub max_layer: Option, + pub deadline_ms: u64, + pub ladder: Option>, +} + +/// The plan of a piece of `[start, start + duration)` units of kind `kind`. +pub fn plan_piece(p: &Policy, tb: Timebase, start: u64, duration: u64, kind: PieceKind) -> PiecePlan { + let (s_ms, e_ms) = (units_to_ms(start, tb), units_to_ms(start.saturating_add(duration), tb)); + let mut out = PiecePlan { deadline_ms: p.deadline_ms, ..PiecePlan::default() }; + let mut tags: Vec<&str> = Vec::new(); + for (i, m) in p.moments.iter().enumerate() { + if s_ms < m.to_ms && e_ms > m.from_ms { + out.moments |= 1 << i; + tags.extend(m.tags.iter().map(String::as_str)); + } + } + for (i, r) in p.rules.iter().enumerate() { + let kind_ok = r.when.piece_kind.as_ref().is_none_or(|ks| ks.iter().any(|k| k == kind.label())); + let tag_ok = r.when.moment_tag.as_ref().is_none_or(|ts| ts.iter().any(|t| tags.contains(&t.as_str()))); + if kind_ok && tag_ok { + out.rules |= 1 << i; + if let Some(m) = r.then.max_layer { + out.max_layer = Some(m); + } + if let Some(d) = r.then.deadline_ms { + out.deadline_ms = d; + } + if let Some(l) = &r.then.ladder { + out.ladder = Some(l.clone()); + } + } + } + out +} + +/// The layer class `c` lists for a piece: `layer_bytes[k]` is the bytes of the piece's +/// layer-`k` object(s), `rule_max` the rules' `max_layer`, and the piece lasts `duration` +/// units of `tb`. +pub fn class_plan(c: &Class, layer_bytes: &[u64], rule_max: Option, duration: u64, tb: Timebase) -> u8 { + if c.floor_only || layer_bytes.is_empty() { + return FLOOR_ONLY; + } + let available = (layer_bytes.len() - 1) as u64; + let mut listed = c.max_layer.min(available).min(rule_max.unwrap_or(u64::MAX)); + if let Some(max) = c.max_kbps { + loop { + let bytes: u128 = layer_bytes[..=listed as usize].iter().map(|&b| b as u128).sum(); + let need = (8 * bytes * tb.num as u128).div_ceil(1000 * duration as u128 * tb.den as u128); + if need <= max as u128 { + break; + } + if listed == 0 { + return FLOOR_ONLY; + } + listed -= 1; + } + } + listed as u8 +} + +#[cfg(test)] +mod tests { + use super::*; + + fn class(max_layer: u64, max_kbps: Option, floor_only: bool) -> Class { + Class { id: "c".into(), max_layer, max_kbps, floor_only } + } + + const TB: Timebase = Timebase { num: 25, den: 1 }; + + #[test] + fn the_class_plan_bounds_layers_by_class_rules_and_rate() { + // A 2 s piece (50 frames): 1000 bytes = 4 kbps. + assert_eq!(class_plan(&class(2, None, false), &[1000, 2000, 3000], None, 50, TB), 2); + assert_eq!(class_plan(&class(1, None, false), &[1000, 2000, 3000], None, 50, TB), 1); + assert_eq!(class_plan(&class(2, None, false), &[1000, 2000, 3000], Some(0), 50, TB), 0); + assert_eq!(class_plan(&class(5, None, false), &[1000], None, 50, TB), 0); + assert_eq!(class_plan(&class(2, None, true), &[1000, 2000], None, 50, TB), FLOOR_ONLY); + // 1000 + 2000 bytes over 2 s = 12 kbps; the base alone 4 kbps. + assert_eq!(class_plan(&class(2, Some(12), false), &[1000, 2000, 3000], None, 50, TB), 1); + assert_eq!(class_plan(&class(2, Some(11), false), &[1000, 2000, 3000], None, 50, TB), 0); + assert_eq!(class_plan(&class(2, Some(4), false), &[1000, 2000], None, 50, TB), 0); + assert_eq!(class_plan(&class(2, Some(3), false), &[1000, 2000], None, 50, TB), FLOOR_ONLY); + // Rounded up: 1001 bytes over 2 s is 4.004 kbps. + assert_eq!(class_plan(&class(0, Some(4), false), &[1001], None, 50, TB), FLOOR_ONLY); + } + + #[test] + fn units_convert_to_milliseconds_rounding_down() { + assert_eq!(units_to_ms(50, TB), 2000); + assert_eq!(units_to_ms(1, Timebase { num: 30000, den: 1001 }), 33); + assert_eq!(units_to_ms(48000, Timebase { num: 48000, den: 1 }), 1000); + } +} diff --git a/wai/wai-pack/src/policy.rs b/wai/wai-pack/src/policy.rs index b652a2a15..f12d52c10 100644 --- a/wai/wai-pack/src/policy.rs +++ b/wai/wai-pack/src/policy.rs @@ -382,10 +382,12 @@ pub struct ClassifyParams { pub motion_high_milli: u64, } -/// Values the command line may override before the digest is taken. +/// Values the command line may override before the digest is taken (§3.2). #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct Overrides { - pub royalty_free_only: Option, + /// Force `royalty_free_only` on. An override only ever forces it on: it never turns + /// off what the policy asks for. + pub royalty_free_only: bool, } /// A policy, every default written in. @@ -862,9 +864,7 @@ impl Policy { /// The effective policy: this one with `o` applied. pub fn effective(&self, o: &Overrides) -> Policy { let mut p = self.clone(); - if let Some(b) = o.royalty_free_only { - p.royalty_free_only = b; - } + p.royalty_free_only |= o.royalty_free_only; p } @@ -1166,6 +1166,15 @@ impl Policy { *h.finalize().as_bytes() } + /// The policy digest of a `policy.json` as written: BLAKE3 over the domain and its + /// bytes (§3.2). + pub fn digest_of_json(json: &[u8]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_POLICY); + h.update(json); + *h.finalize().as_bytes() + } + /// The component `id`. pub fn component(&self, id: &str) -> Option<&Component> { self.components.iter().find(|c| c.id == id) @@ -1205,9 +1214,14 @@ role = "base" assert_eq!(p.ladder[0].param_u64("block"), Some(16)); assert_eq!(p.ladder[0].param_u64_list("q_candidates"), Some(vec![8])); // The digest covers the effective policy: an override changes it. - let o = Overrides { royalty_free_only: Some(true) }; + let o = Overrides { royalty_free_only: true }; assert_ne!(p.digest(), p.effective(&o).digest()); assert_eq!(p.digest(), p.effective(&Overrides::default()).digest()); + // An override forces royalty_free_only on and never turns it off. + let on = p.effective(&o); + assert!(on.royalty_free_only); + assert_eq!(on.effective(&Overrides::default()), on); + assert_eq!(Policy::digest_of_json(p.canonical_json().as_bytes()), p.digest()); } /// Each value kind canonical JSON cannot carry is refused wherever it sits, even diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs new file mode 100644 index 000000000..037d71dd5 --- /dev/null +++ b/wai/wai-pack/src/verify.rs @@ -0,0 +1,506 @@ +//! A package's verifier ([packaging] §9, §11.3): everything a package states that its own +//! files decide, recomputed from those files and bound to the policy its receipt signs. It +//! reads no source: a statement only a source decides (an entry's `sse`) is the +//! producer's. +//! +//! In §11.3's order, each failure a reason of §12: +//! +//! 1. Every file below the package's directory, read strictly (a name beginning with `.` +//! and a `*.sha256` file included): each is a regular file whose path a listing can +//! carry, and every directory below it holds something and can be read +//! (`unexpected-file`). +//! 2. `LISTING` lists every other file with its SHA-256 (`listing-missing`, +//! `listing-mismatch`). +//! 3. The receipt reads as version 2 (`receipt-json`, `receipt-version`), and its file is +//! its canonical form (`receipt-json`). +//! 4. The policy: `policy.json` hashes to the receipt's policy digest +//! (`policy-digest-mismatch`); `policy.toml` is a policy (its own reason) whose +//! effective canonical form, `royalty_free_only` forced on where `policy.json` says so +//! (an override never turns it off, §3.2), is `policy.json` (`policy-json-mismatch`); +//! its ids can name files (`pack-id`); under that effective policy it asks for nothing +//! a packager may not emit (§3.6's reason). +//! 5. The receipt's components are the policy's, by id, in order, each of its media +//! (`policy-component-mismatch`); each index is present (`index-missing`); the receipt +//! verifies against them (§9.1's reasons, the index rules among them). +//! 6. Per component: the index against the policy (`index-policy-mismatch`); the plan +//! file (`plan-mismatch`); every object (`object-missing`, the object rules, +//! `object-lengths`); every decode-equivalent prefix (`prefix-digest`). Then, only when +//! all of that holds, the floor (`floor-object-mismatch`, `floor-motif-mismatch`, +//! `floor-av1-malformed`, `floor-keyframe-mismatch`, `floor-cmaf-mismatch`) and the +//! catalogs (`catalog-mismatch`). +//! 7. A metered receipt's measurement claim (`measurement-claim-missing`, +//! `measurement-claim-invalid`); then object files no entry names (`object-unlisted`), +//! catalogs past the ones given (`catalog-unexpected`), and any other file +//! (`unexpected-file`). +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::catalog::{self, CompView, FloorView}; +use crate::classify::PieceKind; +use crate::plan::{class_plan, plan_piece}; +use crate::policy::{Component, Overrides, Policy, Role as RungRole, Timebase}; +use std::collections::BTreeMap; +use wai::package::{AnyPackageReceipt, PackageReceiptV2}; +use wai::stage_index::{self as wsix, Role, StageIndex}; + +/// A refusal: its reason and what failed. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Reason { + pub code: String, + pub detail: String, +} + +impl std::fmt::Display for Reason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.detail) + } +} + +fn reason(code: impl Into, detail: impl Into) -> Reason { + Reason { code: code.into(), detail: detail.into() } +} + +/// What a verified package states. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct VerifyReport { + pub receipt_hash: [u8; 32], + /// The SHA-256 of `LISTING`. + pub pack_digest: String, + pub total_entries: u64, + pub unique_objects: u64, + pub total_bytes: u64, + pub stored_bytes: u64, + pub metered: bool, +} + +/// Every regular file below `root`, by its path relative to `root` with `/` between +/// names, every name included (one beginning with `.`, a `*.sha256` file). A symbolic link, +/// an entry that is neither a file nor a directory, a name that is not UTF-8, a directory +/// that cannot be read, and a directory below `root` that holds nothing (§11.1: no other +/// file or directory) are refused. +pub fn read_strict(root: &std::path::Path) -> Result>, String> { + fn walk(root: &std::path::Path, rel: &str, out: &mut BTreeMap>) -> Result<(), String> { + let dir = if rel.is_empty() { root.to_path_buf() } else { root.join(rel) }; + let mut names = Vec::new(); + for entry in std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().into_string().map_err(|n| format!("{n:?}: a name that is not UTF-8"))?; + names.push((name, entry.file_type().map_err(|e| e.to_string())?)); + } + if names.is_empty() && !rel.is_empty() { + return Err(format!("{rel}: a directory that holds nothing")); + } + names.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes())); + for (name, ty) in names { + let path = if rel.is_empty() { name } else { format!("{rel}/{name}") }; + if ty.is_symlink() { + return Err(format!("{path}: a symbolic link")); + } else if ty.is_dir() { + walk(root, &path, out)?; + } else if ty.is_file() { + let b = std::fs::read(root.join(&path)).map_err(|e| format!("{path}: {e}"))?; + out.insert(path, b); + } else { + return Err(format!("{path}: neither a file nor a directory")); + } + } + Ok(()) + } + let mut out = BTreeMap::new(); + walk(root, "", &mut out)?; + Ok(out) +} + +/// Verify the package in `dir`, read strictly ([`read_strict`]). +pub fn verify_dir(dir: &std::path::Path) -> Result> { + let files = read_strict(dir).map_err(|e| vec![reason("unexpected-file", e)])?; + verify_files(&files) +} + +/// The policy's own refusals a package is held to (§11.1, §3.6): its ids, then what it +/// asks a packager to emit. +pub fn policy_refusal(policy: &Policy) -> Option { + let classes_ok = policy.classes.iter().all(|c| crate::pack::id_is_path_segment(&c.id) && !c.id.eq_ignore_ascii_case("classic")); + let folded: std::collections::BTreeSet = policy.classes.iter().map(|c| c.id.to_ascii_lowercase()).collect(); + if !policy.components.iter().all(|c| crate::pack::id_is_path_segment(&c.id)) || !classes_ok || folded.len() != policy.classes.len() { + return Some(reason("pack-id", "a component or class id that cannot name a file, two class ids equal in any case, or a class named classic")); + } + if let Err(v) = crate::capcheck::check(policy) { + return Some(reason(v[0].code.clone(), format!("ladder.{}", v[0].rung))); + } + None +} + +/// Verify a package given as its files, by path. +pub fn verify_files(files: &BTreeMap>) -> Result> { + let one = |r: Reason| vec![r]; + // 1. Every path one a listing can carry. + let bad: Vec<&String> = files.keys().filter(|p| !wai::pin::is_listing_path(p)).collect(); + if !bad.is_empty() { + return Err(bad.into_iter().map(|p| reason("unexpected-file", format!("{p}: not a path a listing carries"))).collect()); + } + // 2. LISTING. + let listing = files.get("LISTING").ok_or_else(|| one(reason("listing-missing", "no LISTING")))?; + let rest: BTreeMap = files.iter().filter(|(k, _)| *k != "LISTING").map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let want = wai::pin::listing_from(&rest).map_err(|e| one(reason("listing-mismatch", e.to_string())))?; + if &want != listing { + return Err(one(reason("listing-mismatch", "LISTING is not the listing of the package's other files"))); + } + // 3. The receipt. + let raw = files.get("package.receipt.json").ok_or_else(|| one(reason("receipt-json", "no package.receipt.json")))?; + let text = std::str::from_utf8(raw).map_err(|_| one(reason("receipt-json", "not UTF-8")))?; + let r: PackageReceiptV2 = match AnyPackageReceipt::from_json(text).map_err(|e| one(reason(e.code, e.detail)))? { + AnyPackageReceipt::V2(r) => r, + AnyPackageReceipt::V1(_) => return Err(one(reason("receipt-version", "a package directory carries a version-2 receipt"))), + }; + if r.to_json().as_bytes() != raw.as_slice() { + return Err(one(reason("receipt-json", "package.receipt.json is not its receipt's canonical form"))); + } + // 4. The policy. + let pj = files.get("policy.json").ok_or_else(|| one(reason("policy-json-mismatch", "no policy.json")))?; + let mut h = blake3::Hasher::new(); + h.update(b"wai:pack-policy\x01"); + h.update(pj); + if *h.finalize().as_bytes() != r.policy_digest { + return Err(one(reason("policy-digest-mismatch", "policy.json does not hash to the receipt's policy digest"))); + } + let toml = files.get("policy.toml").ok_or_else(|| one(reason("policy-json-mismatch", "no policy.toml")))?; + let parsed = Policy::parse_bytes(toml).map_err(|e| one(reason(e.code(), e.to_string())))?; + // policy.json's royalty_free_only is the packer's override, which only ever forces it + // on (§3.2): a policy.json that turns off what policy.toml asks for is not its + // effective form. + let rfo = serde_json::from_slice::(pj).ok().and_then(|v| v.get("royalty_free_only").and_then(|b| b.as_bool())) == Some(true); + let policy = parsed.effective(&Overrides { royalty_free_only: rfo }); + if policy.canonical_json().as_bytes() != pj.as_slice() { + return Err(one(reason("policy-json-mismatch", "policy.json is not policy.toml's effective canonical form"))); + } + if let Some(x) = policy_refusal(&policy) { + return Err(one(x)); + } + // 5. The receipt's components are the policy's; the receipt against its indexes. + let same = r.components.len() == policy.components.len() + && r.components.iter().zip(&policy.components).all(|(c, p)| c.id == p.id && c.media_class.label() == p.media.label()); + if !same { + return Err(one(reason("policy-component-mismatch", "the receipt's components are not the policy's, by id, order and media"))); + } + let mut index_bytes: Vec<&[u8]> = Vec::new(); + for c in &r.components { + index_bytes.push(files.get(&c.index_file()).ok_or_else(|| one(reason("index-missing", c.index_file())))?); + } + r.verify_detailed(&index_bytes).map_err(|e| one(reason(e.code, e.detail)))?; + // 6. Per component. + let mut expected: std::collections::BTreeSet = + ["LISTING", "package.receipt.json", "policy.json", "policy.toml"].iter().map(|s| s.to_string()).collect(); + let mut listed_objects = std::collections::BTreeSet::new(); + let mut catalog_files: BTreeMap> = BTreeMap::new(); + for ((c, bytes), comp) in r.components.iter().zip(&index_bytes).zip(&policy.components) { + expected.insert(c.index_file()); + let ix = StageIndex::parse(bytes).map_err(|e| one(reason(e.code, e.detail)))?; + let produced = verify_component(files, &policy, comp, &ix, &r.policy_digest, &mut expected, &mut listed_objects)?; + catalog_files.extend(produced); + } + let mut errs: Vec = Vec::new(); + for (p, b) in &catalog_files { + expected.insert(p.clone()); + if files.get(p) != Some(b) { + errs.push(reason("catalog-mismatch", p.clone())); + } + } + // 7. The measurement claim, then files no rule names. + let claim_path = crate::pack::claim_path(&r); + if r.energy_provenance.is_some() { + expected.insert(claim_path.clone()); + match files.get(&claim_path) { + None => errs.push(reason("measurement-claim-missing", claim_path.clone())), + Some(b) => { + if let Err(e) = check_claim(&r, &index_bytes, b) { + errs.push(reason("measurement-claim-invalid", e)); + } + } + } + } + for p in files.keys() { + if p.starts_with("objects/") { + if !listed_objects.contains(p) { + errs.push(reason("object-unlisted", p.clone())); + } + } else if !expected.contains(p) { + let code = if ["hls/", "dash/", "moq/", "stages/"].iter().any(|d| p.starts_with(d)) { "catalog-unexpected" } else { "unexpected-file" }; + errs.push(reason(code, p.clone())); + } + } + if !errs.is_empty() { + return Err(errs); + } + Ok(VerifyReport { + receipt_hash: r.receipt_hash(), + pack_digest: crate::sha256_hex(listing), + total_entries: r.total_entries, + unique_objects: r.unique_objects, + total_bytes: r.total_bytes, + stored_bytes: r.stored_bytes, + metered: r.energy_provenance.is_some(), + }) +} + +/// The index against its component's policy (§11.3 step 6): `None` when it holds. +pub fn index_against_policy(policy: &Policy, comp: &Component, ix: &StageIndex) -> Option { + let bad = |why: String| Some(reason("index-policy-mismatch", format!("component {}: {why}", comp.id))); + let tb = comp.timebase; + if (ix.header.tb_num, ix.header.tb_den) != (tb.num, tb.den) || ix.header.media_class.label() != comp.media.label() { + return bad("the index's timebase or media class is not the component's".into()); + } + // The string table: the component's id, the class ids, each capability at first use. + let mut strings: Vec = vec![comp.id.clone()]; + strings.extend(policy.classes.iter().map(|c| c.id.clone())); + for e in &ix.entries { + let cap = ix.capability(e); + if !strings[1 + policy.classes.len()..].iter().any(|s| s == cap) { + strings.push(cap.to_string()); + } + } + if ix.strings != strings || ix.header.component != 0 { + return bad("the string table is not the component's id, the class ids and each capability at its first use".into()); + } + // The class records: the policy's classes, in its order. + let classes_ok = ix.classes.len() == policy.classes.len() + && ix.classes.iter().zip(&policy.classes).enumerate().all(|(i, (r, c))| { + r.name as usize == 1 + i + && r.max_layer as u64 == c.max_layer.min(255) + && (r.flags & wsix::CLASS_FLOOR_ONLY != 0) == c.floor_only + && r.max_kbps as u64 == c.max_kbps.map_or(0, |k| k.min(u32::MAX as u64)) + }); + if !classes_ok { + return bad("the class records are not the policy's classes".into()); + } + // Each entry's role and capability are a rung's of its component. + let layers = policy.layers(); + for (k, e) in ix.entries.iter().enumerate() { + let want = match e.role { + Role::Base => Some(RungRole::Base), + Role::Refinement => Some(RungRole::Refinement), + Role::Floor => Some(RungRole::Floor), + _ => None, + }; + let cap = ix.capability(e); + let found = want.is_some_and(|w| { + policy.ladder.iter().any(|r| r.component == comp.id && r.role == w && r.capability == cap && layers.get(r.id.as_str()).copied() == Some(e.layer as u64)) + }); + if !found { + return bad(format!("entry {k}: {cap} as {:?} at layer {} is no rung of the component", e.role, e.layer)); + } + } + // Each piece's rules, moments and class plan (§8.1). + for (p, piece) in ix.pieces.iter().enumerate() { + let Some(kind) = PieceKind::from_code(piece.kind) else { return bad(format!("piece {p}: a kind a video piece does not have")) }; + let pp = plan_piece(policy, tb, piece.start, piece.duration as u64, kind); + if (pp.rules, pp.moments) != (piece.rules, piece.moments) { + return bad(format!("piece {p}: its rules and moments are not §8.1's")); + } + let mut layer_bytes: Vec = Vec::new(); + for e in ix.piece_entries(p).iter().filter(|e| matches!(e.role, Role::Base | Role::Refinement)) { + if layer_bytes.len() <= e.layer as usize { + layer_bytes.resize(e.layer as usize + 1, 0); + } + layer_bytes[e.layer as usize] = layer_bytes[e.layer as usize].saturating_add(e.byte_len); + } + for (ci, c) in policy.classes.iter().enumerate() { + if ix.plan[ci][p] != class_plan(c, &layer_bytes, pp.max_layer, piece.duration as u64, tb) { + return bad(format!("piece {p}: class {}'s plan is not §8.1's", c.id)); + } + } + } + None +} + +/// One component's checks of §11.3 step 6; the catalogs it gives, which the caller compares. +fn verify_component( + files: &BTreeMap>, + policy: &Policy, + comp: &Component, + ix: &StageIndex, + policy_digest: &[u8; 32], + expected: &mut std::collections::BTreeSet, + listed_objects: &mut std::collections::BTreeSet, +) -> Result>, Vec> { + let mut errs: Vec = Vec::new(); + if let Some(x) = index_against_policy(policy, comp, ix) { + return Err(vec![x]); + } + let tb = comp.timebase; + let plan_path = format!("plan-{}.txt", comp.id); + expected.insert(plan_path.clone()); + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + match files.get(&plan_path) { + Some(b) if *b == crate::pack::plan_file(&starts).into_bytes() && wsix::plan_digest(&starts) == ix.header.plan_digest => {} + _ => errs.push(reason("plan-mismatch", format!("{plan_path} is not the index's plan"))), + } + let mut objects: BTreeMap<[u8; 32], Vec> = BTreeMap::new(); + let mut dims: Option<(u32, u32)> = None; + for (k, e) in ix.entries.iter().enumerate() { + let path = crate::objects::object_path(&e.content_hash); + listed_objects.insert(path.clone()); + let Some(b) = files.get(&path) else { + errs.push(reason("object-missing", format!("entry {k}: {path}"))); + continue; + }; + if let Err(x) = wsix::check_object(ix, k, b) { + errs.push(reason(x.code, format!("entry {k}: {}", x.detail))); + continue; + } + // Where WAI checks the payload's format, the lengths are its counted bytes + // (staged-measurement §3), and the payload parses as that format. + if crate::pack::object_lengths(b).ok() != Some((e.manifest_len, e.payload_len)) { + errs.push(reason("object-lengths", format!("entry {k}: not its counted bytes"))); + continue; + } + objects.insert(e.content_hash, b.clone()); + if e.prefix_tier == wsix::TIER_DECODE && e.role == Role::Base { + match decode_frames(ix.capability(e), b) { + Ok((frames, w, hgt)) => { + dims.get_or_insert((w, hgt)); + if crate::pack::clip_digest(&frames) != e.prefix_digest { + errs.push(reason("prefix-digest", format!("entry {k}: the decode is not its prefix digest"))); + } + } + Err(why) => errs.push(reason("prefix-digest", format!("entry {k}: {why}"))), + } + } else if e.prefix_tier == wsix::TIER_DECODE { + errs.push(reason("prefix-digest", format!("entry {k}: a refinement's prefix is checked by a later revision"))); + } + } + // The floor and the catalogs are read only from objects that hold. + if !errs.is_empty() { + return Err(errs); + } + let (floor, produced) = floor_view(policy, comp, ix, &objects).map_err(|e| vec![e])?; + for (p, b) in &produced { + expected.insert(p.clone()); + if files.get(p) != Some(b) { + errs.push(reason("floor-cmaf-mismatch", format!("{p} is not the file the floor's temporal units give"))); + } + } + if !errs.is_empty() { + return Err(errs); + } + let (w, hgt) = dims.unwrap_or((0, 0)); + let view = CompView { index: ix, tb: Timebase { num: tb.num, den: tb.den }, w, h: hgt, objects: &objects, floor }; + catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, policy_digest).map_err(|e| vec![reason("catalog-mismatch", format!("no catalog can be written: {}", e.0))]) +} + +/// A base object decoded to its frames and dimensions. +fn decode_frames(cap: &str, env: &[u8]) -> Result<(Vec>, u32, u32), String> { + let w = wai::container::Wai::from_bytes(env).map_err(|e| e.to_string())?; + match wai::int_payload::decode_int(cap, &w.payload, &wai::int_payload::IntParams::none()).map_err(|e| e.to_string())? { + wai::int_payload::IntDecoded::Video { frames, width, height } => Ok((frames, width, height)), + _ => Err(format!("{cap} does not decode to frames here")), + } +} + +type FloorFiles = (Option, Vec<(String, Vec)>); + +/// The floor of a component as its objects give it (§7.2, §7.3, §7.6), and the CMAF files +/// its temporal units give. Per piece: one floor object of the floor rung's capability, +/// presenting the piece's base (`floor-motif-mismatch`), whose renderer, settings, target +/// and framing header are §7.2's (`floor-object-mismatch`), one temporal unit per frame; +/// the first unit of the first piece carries a sequence header, every sequence header is +/// the first's bytes, and a shown key frame carries one (`floor-av1-malformed`); each +/// piece's first unit is a shown key frame, and no other is unless the policy allows extra +/// key frames (`floor-keyframe-mismatch`). +fn floor_view(policy: &Policy, comp: &Component, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>) -> Result { + let tb = comp.timebase; + let rung = policy.ladder.iter().find(|r| r.component == comp.id && r.role == RungRole::Floor); + let Some(rung) = rung else { return Ok((None, Vec::new())) }; + let object = |why: String| reason("floor-object-mismatch", why); + let listing = policy.floor.supplied_listing.clone().ok_or_else(|| object("a supplied floor with no listing in the policy".into()))?; + let df_want = crate::objects::supplied_derived_from(&[0u8; 32], "", &listing); + let mut pieces = Vec::new(); + let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; + // The objects first: their members, framing and units. + for p in 0..ix.pieces.len() { + let es = ix.piece_entries(p); + let floors: Vec<&wsix::EntryRec> = es.iter().filter(|e| e.role == Role::Floor).collect(); + let [fe] = floors.as_slice() else { return Err(object(format!("piece {p}: not one floor object"))) }; + if ix.capability(fe) != rung.capability { + return Err(object(format!("piece {p}: the floor is not the floor rung's capability"))); + } + let base = es.iter().find(|e| e.role == Role::Base).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p} has no base")))?; + let bytes = objects.get(&fe.content_hash).ok_or_else(|| object(format!("piece {p}: the floor object is not held")))?; + let w = wai::container::Wai::from_bytes(bytes).map_err(|e| object(e.to_string()))?; + let df = w.manifest.derived_from.as_ref().and_then(|v| wai::container::DerivedFrom::from_value(v).ok()).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p}")))?; + if df.motif != crate::hex(&base.content_hash) || df.motif_capability != ix.capability(base) { + return Err(reason("floor-motif-mismatch", format!("piece {p}: the floor does not present its piece's base"))); + } + if df.renderer != df_want.renderer || df.settings != df_want.settings { + return Err(object(format!("piece {p}: a supplied floor's renderer and settings are §7.2's"))); + } + let (units, hdr) = crate::floor::av1_units(&w.payload).ok_or_else(|| object(format!("piece {p}: the floor's payload is not §7.2's framing")))?; + if units.len() as u64 != ix.pieces[p].duration as u64 { + return Err(object(format!("piece {p}: {} units for {} frames", units.len(), ix.pieces[p].duration))); + } + let mut tus = Vec::with_capacity(units.len()); + for (i, u) in units.iter().enumerate() { + let t = crate::av1::temporal_units(u).map_err(|e| reason("floor-av1-malformed", format!("piece {p} unit {i}: {}", e.0)))?; + if t.len() != 1 { + return Err(reason("floor-av1-malformed", format!("piece {p} unit {i} is not one temporal unit"))); + } + tus.push(t); + } + if seq.is_none() { + let s = tus[0][0].sequence_header().ok_or_else(|| reason("floor-av1-malformed", "the floor's first unit has no sequence header"))?; + seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| reason("floor-av1-malformed", e.0))?, s.bytes.to_vec())); + } + let (s, obu) = seq.as_ref().expect("set"); + if hdr != (s.max_frame_height, s.max_frame_width, tb.num, tb.den) { + return Err(object(format!("piece {p}: the framing header is not the piece's height, width and timebase"))); + } + let target = serde_json::json!({"frames": ix.pieces[p].duration, "h": s.max_frame_height, "w": s.max_frame_width}); + if w.manifest.target != target { + return Err(object(format!("piece {p}: the floor's target is not its piece's frames and size"))); + } + let mut key = Vec::with_capacity(units.len()); + for (i, t) in tus.iter().enumerate() { + let tu = &t[0]; + if let Some(h) = tu.sequence_header() + && h.bytes != obu.as_slice() + { + return Err(reason("floor-av1-malformed", format!("piece {p} unit {i}: a sequence header other than the first"))); + } + let k = crate::av1::starts_with_shown_key_frame(tu, s).map_err(|e| reason("floor-av1-malformed", e.0))?; + if k && tu.sequence_header().is_none() { + return Err(reason("floor-av1-malformed", format!("piece {p} unit {i}: a shown key frame without a sequence header"))); + } + if (i == 0 && !k) || (i > 0 && k && !policy.floor.extra_keyframes) { + return Err(reason("floor-keyframe-mismatch", format!("piece {p} unit {i}: {}", if i == 0 { "a piece that does not open on a shown key frame" } else { "a key frame inside a piece" }))); + } + key.push(k); + } + pieces.push(crate::floor::FloorPiece { units, key }); + } + let Some((s, obu)) = seq else { return Ok((None, Vec::new())) }; + let fl = crate::floor::Floor { seq: s, seq_obu: obu, pieces }; + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + let (init_path, init, frags) = crate::pack::cmaf_files(&comp.id, &fl, &starts, tb.den, tb.num).map_err(|e| reason("floor-cmaf-mismatch", e))?; + let view = FloorView { + codec: fl.seq.codec_string(), + w: fl.seq.max_frame_width, + h: fl.seq.max_frame_height, + init: init_path.clone(), + fragments: frags.iter().map(|(p, b)| (p.clone(), b.len() as u64)).collect(), + }; + let mut produced = vec![(init_path, init)]; + produced.extend(frags); + Ok((Some(view), produced)) +} + +fn check_claim(r: &PackageReceiptV2, indexes: &[&[u8]], b: &[u8]) -> Result<(), String> { + use wai::energy_figure::EnergyFigures; + let text = std::str::from_utf8(b).map_err(|_| "not UTF-8".to_string())?; + let claim = wai::claim_receipt::verify_claim_json(text).map_err(|e| format!("{e:?}"))?; + let view = (r, indexes).figure_view().ok_or("the receipt does not verify")?; + claim.check_figure_against(Some(&view)).map_err(|e| format!("{e:?}"))?; + if claim.subject != r.receipt_hash() { + return Err("the claim's subject is not the receipt".into()); + } + Ok(()) +} diff --git a/wai/wai-pack/tests/packs.rs b/wai/wai-pack/tests/packs.rs new file mode 100644 index 000000000..2f665dc5a --- /dev/null +++ b/wai/wai-pack/tests/packs.rs @@ -0,0 +1,300 @@ +//! The packer, end to end, over the golden `video-base-01` of `wai/pack-conformance`: +//! byte-identical packages (test 5), every mutation's reason (test 6), deduplication +//! (test 7), what a packager may emit (test 8), the floor's CMAF files (test 14), the +//! measurement claim (test 17) and the receipts' JSON (test 18). + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::{Role, StageIndex}; +use wai_pack::golden; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn held(p: &Path) -> BTreeMap> { + wai::pin::read_dir_set(p).unwrap() +} + +fn golden_package() -> BTreeMap> { + held(&corpus().join("packs/video-base-01/package")) +} + +/// Test 5: packing the golden's inputs gives the committed package byte for byte, its +/// LISTING included, and packing again gives the same bytes. +#[test] +fn the_golden_pack_is_byte_identical() { + let a = golden::pack_golden(&corpus(), "video-base-01").unwrap(); + let want = golden_package(); + assert_eq!(a.files.keys().collect::>(), want.keys().collect::>()); + for (p, b) in &want { + assert!(a.files[p] == *b, "{p} differs from the committed golden"); + } + assert_eq!(a.files["LISTING"], want["LISTING"]); + let b = golden::pack_golden(&corpus(), "video-base-01").unwrap(); + assert_eq!(a.files, b.files, "a second pack writes the same bytes"); + wai_pack::verify::verify_files(&want).unwrap(); +} + +/// Test 6: every index, object and receipt case of `mutations/` gets its named reason. +#[test] +fn every_mutation_names_its_reason() { + let mut files = held(&corpus().join("mutations")); + let expected: serde_json::Value = serde_json::from_slice(&files.remove("expected.json").unwrap()).unwrap(); + let cases = expected["cases"].as_array().unwrap(); + for want in [ + "refines-mismatch", "layer-gap", "tier-entropy-passthrough", "tier-weakest", "digest-without-tier", "dup-flag", "non-envelope-entry", + "refines-outside-window", "index-truncated", "receipt-counts", "receipt-parent-kind", + ] { + assert!(cases.iter().any(|c| c["reason"] == want), "the corpus has a case for {want}"); + } + for c in cases { + assert_eq!(golden::case_verdict(c, &files).unwrap(), c["reason"].as_str().unwrap(), "{}", c["name"]); + } +} + +/// A fresh pack of the golden's inputs. +fn fresh_package() -> BTreeMap> { + golden::pack_golden(&corpus(), "video-base-01").unwrap().files +} + +/// Test 7: the bumper played twice is one object, so the package stores fewer bytes +/// than it references, and every count is what the index gives. +#[test] +fn a_repeated_piece_is_one_object() { + let pkg = fresh_package(); + let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); + let base = |p: usize| ix.piece_entries(p).iter().find(|e| e.role == Role::Base).unwrap().content_hash; + let floor = |p: usize| ix.piece_entries(p).iter().find(|e| e.role == Role::Floor).unwrap().content_hash; + assert_eq!(base(1), base(3), "the bumper's base is one object"); + assert_eq!(floor(1), floor(3), "and so is its floor"); + assert_ne!(base(0), base(1)); + let r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&pkg["package.receipt.json"]).unwrap()).unwrap(); + assert!(r.unique_objects < r.total_entries, "{} of {}", r.unique_objects, r.total_entries); + let mut first: BTreeMap<[u8; 32], u64> = BTreeMap::new(); + for e in &ix.entries { + first.entry(e.content_hash).or_insert(e.byte_len); + } + assert_eq!(r.unique_objects, first.len() as u64); + assert_eq!(r.stored_bytes, first.values().sum::()); + assert_eq!(r.total_bytes, ix.entries.iter().map(|e| e.byte_len).sum::()); + assert_eq!(pkg.keys().filter(|k| k.starts_with("objects/")).count() as u64, r.unique_objects); +} + +/// Pack `policy`, with `royalty_free_only` forced on when `force_royalty_free_only`. +fn pack_with(policy: &[u8], force_royalty_free_only: bool) -> Result { + let d = corpus().join("packs/video-base-01"); + let floor = held(&d.join("floor")); + let key = ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]); + let mut o = wai_pack::pack::PackOpts::new(key, "did:key:test", floor); + o.overrides.royalty_free_only = force_royalty_free_only; + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::fixture::generate("video-base-01").unwrap()); + wai_pack::pack::pack(policy, &src, &mut o) +} + +fn golden_policy() -> String { + String::from_utf8(std::fs::read(corpus().join("packs/video-base-01/policy.toml")).unwrap()).unwrap() +} + +fn with_rung(cap: &str, role: &str) -> Vec { + format!("{}\n[[ladder]]\nid = \"x\"\ncomponent = \"v\"\ncapability = \"{cap}\"\nrole = \"{role}\"\n", golden_policy()).into_bytes() +} + +/// A policy with its `royalty_free_only = true` line taken out. +fn not_royalty_free_only(p: &[u8]) -> Vec { + let t = String::from_utf8(p.to_vec()).unwrap(); + assert!(t.contains("royalty_free_only = true\n")); + t.replacen("royalty_free_only = true\n", "", 1).into_bytes() +} + +/// Test 8: a packager refuses, before it writes anything, a capability the registry +/// does not classify as royalty-free when the policy asks for royalty-free only, and a +/// record or a candidate whatever the policy asks. +#[test] +fn royalty_free_only_and_candidates_are_refused() { + let code = |r: Result| r.map(|_| "ok".to_string()).unwrap_or_else(|e| e.code); + let avc = with_rung("wai.video.avc", "floor"); + assert_eq!(code(pack_with(&avc, false)), "license-refused:wai.video.avc:patent-pool"); + // Without royalty_free_only it is not the licence that stops it; forced on by the + // command line (§3.2), it is. + let off = not_royalty_free_only(&avc); + assert_ne!(code(pack_with(&off, false)), "license-refused:wai.video.avc:patent-pool"); + assert_eq!(code(pack_with(&off, true)), "license-refused:wai.video.avc:patent-pool"); + assert_eq!(code(pack_with(¬_royalty_free_only(&with_rung("wai.video.package", "floor")), false)), "capability-not-payload:wai.video.package"); + assert_eq!(code(pack_with(¬_royalty_free_only(&with_rung("wai.splat.sog", "base")), false)), "capability-candidate:wai.splat.sog"); + assert_eq!(code(pack_with(¬_royalty_free_only(&with_rung("wai.video.nope", "base")), false)), "capability-unknown:wai.video.nope"); + // The command line writes nothing on a refusal. + let dir = std::env::temp_dir().join(format!("wai-pack-refusal-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + std::fs::write(dir.join("policy.toml"), &avc).unwrap(); + std::fs::write(dir.join("key.hex"), "5a".repeat(32)).unwrap(); + std::fs::write(dir.join("v.y4m"), wai_pack::fixture::generate("video-base-01").unwrap()).unwrap(); + let out = std::process::Command::new(env!("CARGO_BIN_EXE_wai-pack")) + .args(["pack", "--policy", dir.join("policy.toml").to_str().unwrap(), "--out", dir.join("out").to_str().unwrap(), "--key", dir.join("key.hex").to_str().unwrap()]) + .arg("--source") + .arg(format!("v={}", dir.join("v.y4m").display())) + .arg("--floor") + .arg(corpus().join("packs/video-base-01/floor")) + .output() + .unwrap(); + assert!(!out.status.success()); + assert!(String::from_utf8_lossy(&out.stderr).contains("license-refused:wai.video.avc:patent-pool"), "{}", String::from_utf8_lossy(&out.stderr)); + assert!(!dir.join("out").exists(), "nothing is written"); + let _ = std::fs::remove_dir_all(&dir); +} + +fn boxes(b: &[u8]) -> Vec<([u8; 4], &[u8])> { + wai_pack::cmaf::boxes(b).expect("boxes tile the file") +} + +fn child<'a>(b: &'a [u8], path: &[&[u8; 4]]) -> &'a [u8] { + let mut cur = b; + for k in path { + cur = boxes(cur).into_iter().find(|(t, _)| t == *k).unwrap_or_else(|| panic!("no {}", String::from_utf8_lossy(*k))).1; + } + cur +} + +/// Test 14 (the structure; CI's media job reads the same files with a media prober): +/// the initialization segment carries the floor's sequence header in its `av1C`, and +/// each fragment holds its piece's frames at its start, a sync sample first and none +/// after (the supplied floor has no extra key frame). +#[test] +fn the_floor_s_cmaf_files_hold_every_frame() { + let pkg = fresh_package(); + let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); + let init = &pkg["floor/v/init.mp4"]; + let top = boxes(init); + assert_eq!(&top[0].0, b"ftyp"); + assert_eq!(&top[0].1[..4], b"cmfc"); + let stsd = child(init, &[b"moov", b"trak", b"mdia", b"minf", b"stbl", b"stsd"]); + let av01 = &stsd[8..]; + assert_eq!(&av01[4..8], b"av01"); + let av1c = &av01[8 + 78..]; + assert_eq!(&av1c[4..8], b"av1C"); + let floor = std::fs::read(corpus().join("packs/video-base-01/floor/v.obu")).unwrap(); + let tus = wai_pack::av1::temporal_units(&floor).unwrap(); + assert_eq!(&av1c[12..], tus[0].sequence_header().unwrap().bytes); + let playlist = String::from_utf8(pkg["hls/classic/floor-v.m3u8"].clone()).unwrap(); + let frags: Vec<&str> = playlist.lines().filter(|l| !l.starts_with('#')).map(|l| l.trim_start_matches("../../")).collect(); + assert_eq!(frags.len(), ix.pieces.len()); + let mut total = 0; + for (p, f) in frags.iter().enumerate() { + let b = &pkg[*f]; + assert_eq!(wai_pack::sha256_hex(b), f.trim_start_matches("floor/v/").trim_end_matches(".m4s"), "named by its SHA-256"); + let tfdt = child(b, &[b"moof", b"traf", b"tfdt"]); + assert_eq!(u64::from_be_bytes(tfdt[4..12].try_into().unwrap()), ix.pieces[p].start); + let trun = child(b, &[b"moof", b"traf", b"trun"]); + let n = u32::from_be_bytes(trun[4..8].try_into().unwrap()) as usize; + assert_eq!(n as u32, ix.pieces[p].duration); + let flags: Vec = (0..n).map(|i| u32::from_be_bytes(trun[12 + 12 * i + 8..12 + 12 * i + 12].try_into().unwrap())).collect(); + assert_eq!(flags[0], wai_pack::cmaf::SYNC); + assert!(flags[1..].iter().all(|&f| f == wai_pack::cmaf::NON_SYNC)); + total += n; + } + assert_eq!(total, 200); +} + +/// A meter for tests: a clock that advances one second between its reads, at 500 mW. +fn model_meter() -> Box { + let mut t = 0u64; + Box::new(wai::energy_meter::ModelMeter::new( + move || { + let v = t; + t += 1_000_000; + v + }, + 500, + )) +} + +fn relist(files: &mut BTreeMap>) { + files.remove("LISTING"); + let view: BTreeMap = files.iter().map(|(k, v)| (k.clone(), v.as_slice())).collect(); + let l = wai::pin::listing_from(&view).unwrap(); + files.insert("LISTING".into(), l); +} + +/// Test 17: a metered pack writes its measurement claim and verifies; without the claim +/// it is refused (`measurement-claim-missing`), with another key's claim too +/// (`measurement-claim-invalid`); an unmetered pack needs none. +#[test] +fn a_metered_pack_carries_its_measurement_claim() { + let d = corpus().join("packs/video-base-01"); + let policy = std::fs::read(d.join("policy.toml")).unwrap(); + let key = ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]); + let id = wai::signer_id::did_key(&key.verifying_key().to_bytes()); + let mut o = wai_pack::pack::PackOpts::new(key, id, held(&d.join("floor"))); + o.meter = Some(model_meter()); + o.power_reader = "model:500mW".into(); + o.issued_at = "2026-10-06T00:00:00Z".into(); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::fixture::generate("video-base-01").unwrap()); + let out = wai_pack::pack::pack(&policy, &src, &mut o).unwrap(); + assert_eq!(out.receipt.joules_micro, 500_000); + let claim = wai_pack::pack::claim_path(&out.receipt); + assert!(out.files.contains_key(&claim), "{claim}"); + let rep = wai_pack::verify::verify_files(&out.files).unwrap(); + assert!(rep.metered); + let codes = |f: &BTreeMap>| wai_pack::verify::verify_files(f).err().unwrap_or_default().into_iter().map(|r| r.code).collect::>(); + let mut missing = out.files.clone(); + missing.remove(&claim); + relist(&mut missing); + assert_eq!(codes(&missing), vec!["measurement-claim-missing"]); + // A claim about the figure signed by another key. + let other = ed25519_dalek::SigningKey::from_bytes(&[0x77; 32]); + let ix: Vec<&[u8]> = vec![&out.files["index-v.wsix"]]; + use wai::energy_figure::EnergyFigures; + let view = (&out.receipt, ix.as_slice()).figure_view().unwrap(); + let forged = wai::claim_receipt::ClaimReceipt::seal_unchecked(&other, "x", view_subject_type(), view.identity, 0, "2026-10-06T00:00:00Z", forged_body(&view), None).unwrap(); + let mut bad = out.files.clone(); + bad.insert(claim.clone(), forged.to_json().into_bytes()); + relist(&mut bad); + assert_eq!(codes(&bad), vec!["measurement-claim-invalid"]); + // The golden is unmetered and carries none. + let g = golden_package(); + assert!(!g.keys().any(|k| k.starts_with("claims/"))); + assert!(!wai_pack::verify::verify_files(&g).unwrap().metered); +} + +fn view_subject_type() -> wai::claim_receipt::SubjectType { + wai::claim_receipt::SubjectType::Receipt +} + +fn forged_body(view: &wai::energy_figure::FigureView) -> wai::claim_receipt::ClaimBody { + let f = view.figure(None).unwrap(); + wai::claim_receipt::ClaimBody::Measurement(wai::claim_receipt::MeasurementClaim { + figure: wai::claim_receipt::FigureHeader::new(&view.profile, None, None, f.joules_micro, f.class.as_ref().unwrap()), + bracket: None, + build: wai::energy_report::BuildRecord::of_this_build(), + power_reader: Some("model".into()), + }) +} + +/// Test 18: the golden's receipt JSON is RFC 8785 canonical and reads back to itself; the +/// version-1 goldens read back to receipts that verify with their pinned hashes. +#[test] +fn the_receipts_json_round_trips() { + let pkg = golden_package(); + let text = std::str::from_utf8(&pkg["package.receipt.json"]).unwrap(); + let r = wai::package::PackageReceiptV2::from_json(text).unwrap(); + assert_eq!(r.to_json(), text); + let v: serde_json::Value = serde_json::from_str(text).unwrap(); + assert_eq!(wai::jcs::jcs(&v).unwrap(), text); + let keys: Vec<&String> = v.as_object().unwrap().keys().collect(); + let mut sorted = keys.clone(); + sorted.sort(); + assert_eq!(keys, sorted, "members in code-unit order"); + let mut files = held(&corpus().join("receipts")); + let expected: serde_json::Value = serde_json::from_slice(&files.remove("expected.json").unwrap()).unwrap(); + for c in expected["cases"].as_array().unwrap() { + let t = std::str::from_utf8(&files[c["file"].as_str().unwrap()]).unwrap(); + let r1 = wai::wai_video_package::PackageReceipt::from_json(t).unwrap(); + assert!(r1.verify()); + assert_eq!(wai_pack::hex(&r1.receipt_hash()), c["receipt_hash"].as_str().unwrap()); + assert_eq!(r1.to_json().unwrap(), t); + } + assert!(expected["cases"].as_array().unwrap().iter().any(|c| c["receipt_hash"] == "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b"), "the legacy pin is unchanged"); +} diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs new file mode 100644 index 000000000..f7c68978b --- /dev/null +++ b/wai/wai-pack/tests/refusals.rs @@ -0,0 +1,226 @@ +//! Every refusal of the packer this revision names (packaging §7.3, §11.4, §12), each from +//! the golden's inputs with one thing changed: the supplied floor's reasons, what this +//! revision does not pack yet, a meter that gives no figure, the encoder's caps, and an +//! output directory that is not empty. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn golden_policy() -> String { + String::from_utf8(std::fs::read(corpus().join("packs/video-base-01/policy.toml")).unwrap()).unwrap() +} + +fn golden_floor() -> Vec { + std::fs::read(corpus().join("packs/video-base-01/floor/v.obu")).unwrap() +} + +/// The golden policy with its floor pinned to the listing of `floor`, and `edit` applied. +fn policy_for(floor: &Files, edit: &dyn Fn(String) -> String) -> Vec { + let listing = wai_pack::floor::listing_sha256(floor).unwrap(); + let p = golden_policy(); + let at = p.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + let p = format!("{}{listing}{}", &p[..at], &p[at + 64..]); + edit(p).into_bytes() +} + +fn pack(policy: &[u8], floor: Files, meter: Option>) -> String { + let mut o = wai_pack::pack::PackOpts::new(ed25519_dalek::SigningKey::from_bytes(&[0x5a; 32]), "did:key:test", floor); + o.meter = meter; + o.power_reader = "model".into(); + o.issued_at = "2026-10-06T00:00:00Z".into(); + let mut src = BTreeMap::new(); + src.insert("v".to_string(), wai_pack::fixture::generate("video-base-01").unwrap()); + match wai_pack::pack::pack(policy, &src, &mut o) { + Ok(_) => "ok".into(), + Err(e) => e.code, + } +} + +fn units() -> Vec> { + wai_pack::av1::temporal_units(&golden_floor()).unwrap().iter().map(|t| t.bytes()).collect() +} + +fn floor_of(stream: Vec) -> Files { + let mut f = BTreeMap::new(); + f.insert("v.obu".to_string(), stream); + f +} + +fn case(stream: Vec, edit: &dyn Fn(String) -> String) -> String { + let fl = floor_of(stream); + pack(&policy_for(&fl, edit), fl, None) +} + +/// §7.3: each reason a supplied floor is refused with, before anything is written. +#[test] +fn every_floor_reason() { + let u = units(); + let id = |p: String| p; + assert_eq!(case(golden_floor(), &id), "ok"); + // No floor rung. + assert_eq!( + case(golden_floor(), &|p: String| p.replace("[[ladder]]\nid = \"f0\"\ncomponent = \"v\"\ncapability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }\n", "")), + "floor-missing" + ); + // A listing the policy does not pin. + let fl = floor_of(golden_floor()); + let mut pol = String::from_utf8(policy_for(&fl, &id)).unwrap(); + let at = pol.find("supplied_listing = \"").unwrap() + "supplied_listing = \"".len(); + pol.replace_range(at..at + 64, &"0".repeat(64)); + assert_eq!(pack(pol.as_bytes(), fl, None), "floor-listing-mismatch"); + // No stream for the component. + let mut other = BTreeMap::new(); + other.insert("w.obu".to_string(), golden_floor()); + assert_eq!(pack(&policy_for(&other, &id), other, None), "floor-stream-missing"); + // A stream cut inside its last unit; an empty stream. + let g = golden_floor(); + assert_eq!(case(g[..g.len() - 3].to_vec(), &id), "floor-av1-malformed"); + assert_eq!(case(Vec::new(), &id), "floor-av1-malformed"); + // One unit short. + assert_eq!(case(u[..u.len() - 1].concat(), &id), "floor-frame-count"); + // The piece of frames 85 to 134 opening on an inter frame, with no key frame + // anywhere in it. + let mut inter = u.clone(); + inter[85] = u[86].clone(); + assert_eq!(case(inter.concat(), &id), "floor-keyframe-mismatch"); + // A key frame inside piece 2: refused, unless extra key frames are allowed. + let mut extra = u.clone(); + extra[86] = u[85].clone(); + assert_eq!(case(extra.concat(), &id), "floor-keyframe-mismatch"); + assert_eq!(case(extra.concat(), &|p: String| p.replace("[floor]\n", "[floor]\nextra_keyframes = true\n")), "ok"); +} + +/// §11.4: what this revision does not pack yet is refused by name, every +/// `pack-capability-unsupported` naming its capability. +#[test] +fn what_this_revision_does_not_pack() { + let fl = floor_of(golden_floor()); + let run = |edit: &dyn Fn(String) -> String| pack(&policy_for(&fl, edit), fl.clone(), None); + // Another medium, and two components. + assert_eq!(run(&|p: String| p.replace("media = \"video\"", "media = \"image\"")), "pack-media-unsupported"); + assert_eq!( + run(&|p: String| p.replace("[segmentation]", "[[component]]\nid = \"a\"\nmedia = \"audio\"\n\n[component.timebase]\nnum = 48000\nden = 1\n\n[[ladder]]\nid = \"ab\"\ncomponent = \"a\"\ncapability = \"wai.audio.flac\"\nrole = \"base\"\n\n[segmentation]")), + "pack-media-unsupported" + ); + // A refinement rung. + assert_eq!( + run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"r1\"\ncomponent = \"v\"\ncapability = \"wai.video.int_refine\"\nrole = \"refinement\"\nrefines = \"b0\"\nparams = { q = 6 }\n\n[live]\nlayer_lag_ms = [0, 200]\n\n[floor]")), + "pack-refinement-unsupported" + ); + // A rule's ladder subset. + assert_eq!( + run(&|p: String| format!("{p}\n[[rule]]\nid = \"s\"\nwhen = {{ piece_kind = [\"static\"] }}\nthen = {{ ladder = [\"b0\", \"f0\"] }}\n")), + "pack-rule-ladder-unsupported" + ); + // A floor to encode. + assert_eq!(run(&|p: String| p.replace("params = { source = \"supplied\" }", "params = { source = \"encode\" }")), "pack-floor-encode-unsupported"); + // A base of another capability; two bases; a floor of another capability. + assert_eq!( + run(&|p: String| p.replace("capability = \"wai.video.int_motion\"\nrole = \"base\"\nparams = { block = 16, search = 4, mv_precision = 1, q_candidates = [16, 12, 8], max_sse_px_milli = 30000 }", "capability = \"wai.image.png\"\nrole = \"base\"")), + "pack-capability-unsupported:wai.image.png" + ); + assert_eq!( + run(&|p: String| p.replace("[floor]", "[[ladder]]\nid = \"b1\"\ncomponent = \"v\"\ncapability = \"wai.image.png\"\nrole = \"base\"\n\n[floor]")), + "pack-capability-unsupported:wai.image.png" + ); + assert_eq!( + run(&|p: String| p.replace("capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.image.png\"\nrole = \"floor\"")), + "pack-capability-unsupported:wai.image.png" + ); +} + +/// A meter that was asked for and gives no figure refuses the pack: a model past its +/// range, and one whose figure rounds to nothing. +#[test] +fn a_meter_with_no_figure_refuses_the_pack() { + let fl = floor_of(golden_floor()); + let pol = policy_for(&fl, &|p: String| p); + let clock = |step: u64| { + let mut t = 0u64; + move || { + let v = t; + t += step; + v + } + }; + let huge = wai::energy_meter::ModelMeter::new(clock(1_000_000), u64::MAX / 2); + assert_eq!(pack(&pol, fl.clone(), Some(Box::new(huge))), "pack-meter"); + let tiny = wai::energy_meter::ModelMeter::new(clock(1), 1); + assert_eq!(pack(&pol, fl.clone(), Some(Box::new(tiny))), "pack-meter"); + let fine = wai::energy_meter::ModelMeter::new(clock(1_000_000), 500); + assert_eq!(pack(&pol, fl, Some(Box::new(fine))), "ok"); +} + +/// The encoder's caps (the review's clipcap): a 4K piece of 50 frames is past the clip +/// cap (more than 2^30 samples), refused for that before anything is allocated or any +/// frame is read; the packer reports it `pack-encode`. +#[test] +fn the_encoder_refuses_a_piece_past_its_caps() { + let (h, w) = (2160u32, 3840u32); + let frames: Vec> = (0..50).map(|_| Vec::new()).collect(); + let p = wai_pack::encode::MotionParams { block: 16, search: 7, mv_precision: 2, q_candidates: vec![8], max_sse_px_milli: 0 }; + let e = wai_pack::encode::choose_q(&frames, h, w, &p).map(|q| q.q).unwrap_err(); + assert_eq!((e.code(), e.detail()), ("too_large", "the clip exceeds the clip cap")); + // One frame fewer than the cap allows passes the caps (and is then refused for its + // frames, which hold nothing). + let n = ((1u64 << 30) / (3 * h as u64 * w as u64)) as usize; + let e = wai_pack::encode::choose_q(&frames[..n], h, w, &p).map(|q| q.q).unwrap_err(); + assert_ne!(e.code(), "too_large", "{e}"); +} + +/// §11.4: `wai-pack pack` writes a package only into a new or empty directory. Into one +/// that holds a file (a measurement claim of an earlier pack, here) it refuses with +/// `pack-out-not-empty` before any work, and leaves the directory as it was; into an +/// empty one it packs a package that verifies. +#[test] +fn the_packer_refuses_an_out_directory_that_is_not_empty() { + let bin = env!("CARGO_BIN_EXE_wai-pack"); + let dir = std::env::temp_dir().join(format!("wai-pack-out-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let src = dir.join("v.y4m"); + std::fs::write(&src, wai_pack::fixture::generate("video-base-01").unwrap()).unwrap(); + let golden = corpus().join("packs/video-base-01"); + let run = |out: &Path| { + std::process::Command::new(bin) + .arg("pack") + .arg("--policy") + .arg(golden.join("policy.toml")) + .arg("--out") + .arg(out) + .arg("--key") + .arg(golden.join("key.hex")) + .arg("--source") + .arg(format!("v={}", src.display())) + .output() + .unwrap() + }; + // A directory holding an earlier pack's measurement claim. + let stale = dir.join("stale"); + std::fs::create_dir_all(stale.join("claims")).unwrap(); + std::fs::write(stale.join("claims/old.measurement.json"), b"{}").unwrap(); + let o = run(&stale); + assert!(!o.status.success()); + let err = String::from_utf8_lossy(&o.stderr); + assert!(err.contains("pack-out-not-empty"), "{err}"); + let left = wai_pack::verify::read_strict(&stale).unwrap(); + assert_eq!(left.keys().collect::>(), ["claims/old.measurement.json"], "nothing written"); + // A file where the directory would be. + let file = dir.join("a-file"); + std::fs::write(&file, b"x").unwrap(); + assert!(String::from_utf8_lossy(&run(&file).stderr).contains("pack-out-not-empty")); + // An empty directory, and one that does not exist yet. + std::fs::create_dir_all(dir.join("empty")).unwrap(); + for out in [dir.join("empty"), dir.join("new")] { + let o = run(&out); + assert!(o.status.success(), "{}", String::from_utf8_lossy(&o.stderr)); + assert!(wai_pack::verify::verify_dir(&out).is_ok(), "the package verifies"); + } + let _ = std::fs::remove_dir_all(&dir); +} diff --git a/wai/wai-pack/tests/review.rs b/wai/wai-pack/tests/review.rs new file mode 100644 index 000000000..6654d0a28 --- /dev/null +++ b/wai/wai-pack/tests/review.rs @@ -0,0 +1,408 @@ +//! What a package's verifier must refuse, each case a package altered and, where the +//! alteration would otherwise be caught by the receipt or `LISTING` alone, re-sealed with +//! the package key (what a dishonest packager holding the key could publish): the +//! package bound to its policy, malformed packages refused and never panicked on, the +//! floor's key frames and members, hidden files, the receipt's canonical bytes, and a +//! seeded run of random alterations that must never panic. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use wai::stage_index::StageIndex; +use wai_pack::golden; + +type Files = BTreeMap>; + +fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../pack-conformance") +} + +fn golden_files() -> Files { + wai::pin::read_dir_set(&corpus().join("packs/video-base-01/package")).unwrap() +} + +fn key() -> ed25519_dalek::SigningKey { + let t = std::fs::read_to_string(corpus().join("packs/video-base-01/key.hex")).unwrap(); + let t = t.trim(); + let mut seed = [0u8; 32]; + for (i, s) in seed.iter_mut().enumerate() { + *s = u8::from_str_radix(&t[2 * i..2 * i + 2], 16).unwrap(); + } + ed25519_dalek::SigningKey::from_bytes(&seed) +} + +fn codes(f: &Files) -> Vec { + match wai_pack::verify::verify_files(f) { + Ok(_) => vec!["ok".into()], + Err(v) => v.into_iter().map(|r| r.code).collect(), + } +} + +fn first(f: &Files) -> String { + codes(f).into_iter().next().unwrap() +} + +fn index(f: &Files) -> StageIndex { + StageIndex::parse(&f["index-v.wsix"]).unwrap() +} + +fn set_index(f: &mut Files, ix: &StageIndex) { + f.insert("index-v.wsix".into(), ix.to_bytes()); +} + +fn resealed(mut f: Files) -> Files { + golden::reseal(&mut f, &key()).unwrap(); + f +} + +/// The package mutations of the corpus (golden.rs), by name. +fn mutations() -> BTreeMap<&'static str, golden::PackageMutation> { + golden::package_mutations(&golden_files(), &key()).into_iter().map(|m| (m.name, m)).collect() +} + +/// Each named case, written out as a directory and read by the verifier's own walk, is +/// refused with the reason the corpus gives it, first. +fn holds(cases: &[(&str, &str)]) { + let m = mutations(); + for (name, reason) in cases { + let c = m.get(name).unwrap_or_else(|| panic!("no case {name}")); + assert_eq!(c.reason, *reason, "{name}: the corpus's reason"); + assert_eq!(golden::package_case_verdict(&c.files, &c.empty_dirs, &c.unreadable_dirs).unwrap(), *reason, "{name}"); + } +} + +#[test] +fn the_golden_verifies_and_reseals_to_itself() { + let g = golden_files(); + assert_eq!(first(&g), "ok"); + assert_eq!(resealed(g.clone()), g, "the re-sealer reproduces the golden byte for byte"); +} + +/// Item 1 of the review: a package is bound to the policy its receipt signs. +#[test] +fn a_package_is_bound_to_its_policy() { + holds(&[ + // A version-2 receipt over no component at all, signed with the package key. + ("no-component", "policy-component-mismatch"), + ("no-component-live", "policy-component-mismatch"), + // The index's class records (with its class plan, and a rate alone), an entry's + // capability, the string table's order and a class plan, each not the policy's. + ("index-class-records", "index-policy-mismatch"), + ("index-class-rate", "index-policy-mismatch"), + ("index-capability", "index-policy-mismatch"), + ("index-string-order", "index-policy-mismatch"), + ("index-class-plan", "index-policy-mismatch"), + // A class named classic, in any case; under royalty_free_only (the policy's, and + // the packer's override as policy.json states it), a patent-pool floor, and a + // policy.json that turns it off; the policy's component of another medium. + ("class-named-classic", "pack-id"), + ("class-named-classic-in-capitals", "pack-id"), + ("policy-json-relaxes-royalty-free", "policy-json-mismatch"), + ("patent-pool-floor", "license-refused:wai.video.avc:patent-pool"), + ("patent-pool-floor-by-override", "license-refused:wai.video.avc:patent-pool"), + ("component-medium", "policy-component-mismatch"), + ]); +} + +/// Item 2: a malformed package is refused, never panicked on. +#[test] +fn malformed_packages_are_refused() { + holds(&[("floor-object-missing", "object-missing"), ("live-window-past-2-53", "catalog-mismatch")]); + // A corrupted floor object is refused for its hash, and for nothing it would have led + // to. + let m = mutations(); + let c = codes(&m["floor-object-corrupt"].files); + assert!(c.iter().all(|c| c == "object-hash"), "{c:?}"); + // An on-demand index whose first piece does not start at 0: a packager cannot seal it. + let g = golden_files(); + let mut f = g.clone(); + let mut ix = index(&f); + for p in ix.pieces.iter_mut() { + p.start += 10; + } + set_index(&mut f, &ix); + golden::relist(&mut f); + assert!(golden::reseal(&mut f, &key()).is_err(), "a packager cannot seal it either"); + // A fragment's decode time past 2^64 − 1. + let (_, b) = golden::floor_entry(&g, 0); + let (_, _, units) = golden::floor_parts(&b); + let tus: Vec = units.iter().map(|u| wai_pack::av1::temporal_units(u).unwrap().remove(0)).collect(); + let seq = wai_pack::av1::SequenceHeader::parse(tus[0].sequence_header().unwrap().payload).unwrap(); + let fl = wai_pack::floor::Floor { seq, seq_obu: tus[0].sequence_header().unwrap().bytes.to_vec(), pieces: vec![wai_pack::floor::FloorPiece { units: units.clone(), key: vec![true; units.len()] }] }; + assert!(wai_pack::pack::cmaf_files("v", &fl, &[u64::MAX / 2], 3, 25).is_err()); +} + +/// Item 3: a floor fragment opens on a shown key frame, and holds no other unless the +/// policy allows; every sequence header is the first's. +#[test] +fn a_floor_piece_opens_on_a_key_frame() { + holds(&[ + ("floor-opens-on-inter", "floor-keyframe-mismatch"), + ("floor-extra-key-frame", "floor-keyframe-mismatch"), + ("floor-extra-key-frame-allowed", "ok"), + ("floor-sequence-header", "floor-av1-malformed"), + ]); +} + +/// Item 6: a supplied floor's members are §7.2's; it presents its piece's base, and its +/// CMAF files are the ones its units give. +#[test] +fn a_floor_object_is_bound_to_its_profile() { + holds(&[ + ("floor-framing-header", "floor-object-mismatch"), + ("floor-renderer-settings", "floor-object-mismatch"), + ("floor-target", "floor-object-mismatch"), + ("floor-motif", "floor-motif-mismatch"), + ("floor-cmaf", "floor-cmaf-mismatch"), + ]); +} + +/// Item 7: a directory read strictly, dot-names and `*.sha256` files included; and a +/// directory that holds nothing or cannot be read. +#[test] +fn hidden_files_are_refused() { + holds(&[ + ("dot-file", "unexpected-file"), + ("dot-directory", "unexpected-file"), + ("dot-object", "unexpected-file"), + ("sha256-file", "unexpected-file"), + ("empty-directory", "unexpected-file"), + ("empty-dot-directory", "unexpected-file"), + ("unreadable-directory", "unexpected-file"), + ]); + // The strict walk reads hidden files rather than skipping them, and refuses a + // directory that holds nothing. + let dir = std::env::temp_dir().join(format!("wai-pack-hidden-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + let m = mutations(); + for (name, extra) in [("dot-file", ".hidden"), ("dot-directory", ".cache/blob"), ("dot-object", "objects/.x.wai"), ("sha256-file", "payload.sha256")] { + let d = dir.join(name); + for (p, b) in &m[name].files { + let path = d.join(p); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, b).unwrap(); + } + let files = wai_pack::verify::read_strict(&d).unwrap(); + assert!(files.contains_key(extra), "{extra} is read"); + } + std::fs::create_dir_all(dir.join("dot-file/hls/empty")).unwrap(); + assert!(wai_pack::verify::read_strict(&dir.join("dot-file")).is_err(), "a directory that holds nothing"); + let _ = std::fs::remove_dir_all(&dir); +} + +/// Item 4: the receipt file is its canonical form; a member named twice is refused. +#[test] +fn the_receipt_file_is_canonical() { + holds(&[("receipt-pretty", "receipt-json"), ("receipt-newline", "receipt-json"), ("receipt-duplicate-member", "receipt-json")]); +} + +/// Item 2: random alterations of the golden, some re-sealed, never make the verifier +/// panic. Seeded, so a failure reproduces. +#[test] +fn random_alterations_never_panic() { + let g = golden_files(); + let paths: Vec = g.keys().cloned().collect(); + let mut s = 0x9e37_79b9_7f4a_7c15u64; + let mut next = || { + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + s + }; + let mut refused = 0; + for round in 0..240 { + let mut f = g.clone(); + for _ in 0..1 + next() % 3 { + let p = &paths[(next() % paths.len() as u64) as usize]; + let Some(b) = f.get_mut(p) else { continue }; + match next() % 6 { + 0 if !b.is_empty() => { + let i = (next() % b.len() as u64) as usize; + b[i] ^= 1 << (next() % 8); + } + 1 => { + let n = (next() % (b.len() as u64 + 1)) as usize; + b.truncate(n); + } + 2 => { + b.extend_from_slice(&[0u8; 9]); + } + 3 if p.ends_with(".wsix") && b.len() > 200 => { + let i = 160 + (next() % (b.len() as u64 - 160)) as usize; + b[i] = (next() & 0xff) as u8; + } + 4 => { + f.remove(p); + } + _ => { + let i = (next() % b.len().max(1) as u64) as usize; + if i < b.len() { + b[i] = b'"'; + } + } + } + } + if round % 2 == 0 { + golden::relist(&mut f); + } + if round % 3 == 0 { + let _ = golden::reseal(&mut f, &key()); + } + let r = std::panic::catch_unwind(|| wai_pack::verify::verify_files(&f)); + assert!(r.is_ok(), "round {round} panicked"); + if r.unwrap().is_err() { + refused += 1; + } + } + // A re-sealed alteration of a derived file can be the golden again; most are refused. + assert!(refused > 120, "{refused} of 240 refused"); +} + +/// Round 3, item F: alterations of the floor, a base and the index that keep the package +/// consistent (the index following the new bytes through `golden::replace_object`, the +/// package re-sealed with its key) reach the verifier's floor, prefix and catalog checks, +/// and never make it panic. Seeded and bounded, so a failure reproduces. +/// +/// What a verifier without the source accepts, by design; an altered package it accepts +/// must be in one of these classes (or be the golden again): +/// - the AV1 frame data a floor's units carry: the verifier checks each unit's framing, +/// the sequence headers and where the key frames are, not the pictures (it decodes no +/// AV1), so other bytes inside a unit, and inter units moved or repeated within a +/// piece, are accepted; +/// - producer-statement fields only the source decides: a piece's kind and flags (§4.3, +/// §4.6) and an entry's `sse` (§6.6), where the rules, moments and class plan they lead +/// to are unchanged; +/// - equivalent WIV1 encodings: a base payload whose bytes differ but which decodes to +/// the same frames, so its prefix digest holds (§6.5). +/// +/// A floor object's framing header, a unit cut short and a unit with a second temporal +/// unit appended are never accepted. +#[test] +fn deep_alterations_never_panic() { + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum Alteration { + UnitBytes, + UnitOrder, + Framing, + UnitCut, + UnitExtended, + BasePayload, + IndexField, + } + use Alteration::*; + let g = golden_files(); + let key = key(); + let mut s = 0x2545_f491_4f6c_dd1du64; + let mut next = move || { + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + s + }; + let (mut accepted, mut unsealed) = (0u32, 0u32); + let mut refused: BTreeMap = BTreeMap::new(); + for round in 0..300 { + let mut f = g.clone(); + let ix = index(&f); + let p = (next() % ix.pieces.len() as u64) as usize; + let alteration = match next() % 5 { + kind @ 0..=2 => { + let (k, b) = golden::floor_entry(&f, p); + let (man, mut hdr, mut units) = golden::floor_parts(&b); + let u = (next() % units.len() as u64) as usize; + let a = match (kind, next() % 6) { + (0, _) => { + let n = units[u].len(); + let i = (next() % n as u64) as usize; + units[u][i] ^= 1 << (next() % 8); + UnitBytes + } + (1, 0) => { + let n = units[u].len() as u64; + units[u].truncate((next() % (n + 1)) as usize); + UnitCut + } + (1, 1) => { + let x = units.remove(u); + let j = (next() % (units.len() as u64 + 1)) as usize; + units.insert(j, x); + UnitOrder + } + (1, 2) => { + let x = units[u].clone(); + units.insert(u, x); + units.pop(); + UnitOrder + } + (1, 3) => { + // A temporal delimiter OBU: a second temporal unit in one unit. + units[u].extend_from_slice(&[0x12, 0x00]); + UnitExtended + } + (1, 4) => { + if units[u].len() > 2 { + units[u][1] = (next() & 0xff) as u8; + } + UnitBytes + } + (1, _) => { + units.swap(0, u); + UnitOrder + } + (_, w) => { + match w % 4 { + 0 => hdr.0 = (next() % 70_000) as u32, + 1 => hdr.1 = 0, + 2 => hdr.2 = u32::MAX, + _ => hdr.3 = 0, + } + Framing + } + }; + golden::replace_object(&mut f, k, golden::floor_envelope(&man, hdr, &units)); + a + } + 3 => { + let k = ix.pieces[p].first_entry as usize; + let mut b = f[&wai_pack::objects::object_path(&ix.entries[k].content_hash)].clone(); + let ml = u32::from_le_bytes(b[4..8].try_into().unwrap()) as usize; + let at = 12 + ml + (next() % (b.len() - 12 - ml) as u64) as usize; + b[at] = (next() & 0xff) as u8; + golden::replace_object(&mut f, k, b); + BasePayload + } + _ => { + let mut ix = ix.clone(); + let k = (next() % ix.entries.len() as u64) as usize; + match next() % 5 { + 0 => ix.pieces[p].kind = (next() % 4) as u8, + 1 => ix.pieces[p].flags = (next() % 16) as u8, + 2 => ix.entries[k].sse = next(), + 3 => ix.pieces[p].moments = (next() % 4) as u32, + _ => ix.pieces[p].rules = (next() % 4) as u32, + } + set_index(&mut f, &ix); + IndexField + } + }; + // What a packager cannot seal (an index its own rules refuse) is not a package. + if golden::reseal(&mut f, &key).is_err() { + unsealed += 1; + continue; + } + let verdict = std::panic::catch_unwind(|| wai_pack::verify::verify_files(&f)).unwrap_or_else(|_| panic!("round {round} ({alteration:?}): the verifier panicked")); + if let Err(v) = verdict { + *refused.entry(v[0].code.clone()).or_default() += 1; + } else { + accepted += 1; + assert!( + f == g || matches!(alteration, UnitBytes | UnitOrder | BasePayload | IndexField), + "round {round}: a {alteration:?} alteration accepted, outside the classes a verifier without the source accepts" + ); + } + } + // The alterations reach the floor and prefix checks, past the receipt and the index. + for reached in ["floor-object-mismatch", "floor-keyframe-mismatch", "prefix-digest", "index-policy-mismatch"] { + assert!(refused.contains_key(reached), "no round reached {reached}: {refused:?}"); + } + assert!(accepted > 0 && unsealed < 100, "{refused:?} refused, {accepted} accepted, {unsealed} not sealable"); +} diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 3411f5624..22b875dbe 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -268,8 +268,10 @@ video_ssai = ["video_manifest", "video_policy"] # Ed25519). video_keys = ["video_manifest"] # wai.video.package — deterministic segmentation + attested, deduplicating -# SegmentMap. Reuses the manifest floor (EnergyProvenance, GrantRef, Merkle). -video_package = ["video_manifest"] +# SegmentMap. Reuses the manifest floor (EnergyProvenance, GrantRef, Merkle). Also the +# packaging extension's stage index (WSIX) and version-2 package receipt, which name +# each object by jwp-receipts §2.1's content hash (jwp_hash). +video_package = ["video_manifest", "jwp_hash"] # wai.video.channel — virtual linear / FAST assembly; the program-as-run hash # chain. Reuses the manifest floor (EnergyProvenance, GrantRef, Merkle). video_channel = ["video_manifest"] diff --git a/wai/wai-rs/include/wai.h b/wai/wai-rs/include/wai.h index 988c60e4e..024e56bbc 100644 --- a/wai/wai-rs/include/wai.h +++ b/wai/wai-rs/include/wai.h @@ -333,13 +333,6 @@ */ #define CHAIN_HELD_LINKS 65536 -/** - * How many parents one chain keeps as requested (§3 step 3, §6.1): the parents named by - * groups it refused `parent_not_held`. A group whose link hash is one of them fills a - * gap, and is exempt from the freshness bound. Past it, the oldest request is dropped. - */ -#define CHAIN_REQUESTED_LINKS 64 - /** * `CausalOp::mutation_kind` for an action input (move/look/buttons + free payload). */ diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index c33a3a130..e43e293a3 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -461,6 +461,7 @@ pub struct WireIdentifier { const HTTP_DELIVERY: &str = "extensions/http-delivery.md"; const MOQ_FORMAT: &str = "extensions/moq-streaming-format.md"; const JWP_RECEIPTS: &str = "extensions/jwp-receipts.md"; +const PACKAGING: &str = "extensions/packaging.md"; const fn wire(name: &'static str, kind: WireKind, document: &'static str, section: &'static str, status: WireStatus) -> WireIdentifier { @@ -468,9 +469,9 @@ const fn wire(name: &'static str, kind: WireKind, document: &'static str, sectio } /// The wire identifiers of the container, of its two delivery bindings -/// (http-delivery, moq-streaming-format) and of the key discovery those bindings -/// serve (jwp-receipts §7), in document order. The export lists them sorted by name -/// and kind. +/// (http-delivery, moq-streaming-format), of the key discovery those bindings serve +/// (jwp-receipts §7) and of packaging, in document order. The export lists them sorted +/// by name and kind. pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("WAI1", WireKind::Magic, "SPEC.md", "§2", WireStatus::Defined), wire("WAI2", WireKind::Magic, "SPEC.md", "§7.1", WireStatus::Defined), @@ -503,6 +504,13 @@ pub const WIRE_IDENTIFIERS: &[WireIdentifier] = &[ wire("/.well-known/wai-keys", WireKind::WellKnown, JWP_RECEIPTS, "§7.1", WireStatus::Defined), wire("wai-key-status", WireKind::DocumentKind, JWP_RECEIPTS, "§7.9", WireStatus::Defined), wire("wai-module-grant", WireKind::DocumentKind, JWP_RECEIPTS, "§7.9", WireStatus::Defined), + wire("WSIX", WireKind::Magic, PACKAGING, "§6", WireStatus::Defined), + wire("science.transaction.wai.class", WireKind::DataId, PACKAGING, "§8", WireStatus::Defined), + wire("science.transaction.wai.stages", WireKind::DataId, PACKAGING, "§8", WireStatus::Defined), + wire("tag:transaction.science,2026:wai/class", WireKind::Scheme, PACKAGING, "§8", WireStatus::Defined), + wire("tag:transaction.science,2026:wai/stages", WireKind::Scheme, PACKAGING, "§8", WireStatus::Defined), + wire("wai_layer", WireKind::Catalog, PACKAGING, "§8", WireStatus::Defined), + wire("wai_delivery_timeout_ms", WireKind::Catalog, PACKAGING, "§8", WireStatus::Defined), ]; /// A string with the shape of a capability that the reference implementation uses diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index aeb5d2352..eb3181326 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -759,6 +759,12 @@ pub struct Manifest { /// `refines-malformed`). #[serde(default, skip_serializing_if = "is_none")] pub refines: Option, + /// `derived_from` (SPEC §7.1, *Derived renditions*): the payload presents another + /// object, the motif, and was made from it by a named renderer or encoder. Kept as + /// the JSON it is, so a malformed value still parses ([`DerivedFrom::from_value`] + /// reports it); a reader of an earlier revision ignores it (§8). + #[serde(default, skip_serializing_if = "is_none")] + pub derived_from: Option, #[serde(default)] pub target: Value, /// Members this revision does not define, kept so they survive a round trip. @@ -782,6 +788,7 @@ impl Manifest { conditioning, security: None, refines: None, + derived_from: None, target: Value::Null, extra: Map::new(), } @@ -796,6 +803,11 @@ impl Manifest { self.refines = Some(refines.to_value()); self } + /// Set `derived_from` (SPEC §7.1, *Derived renditions*). + pub fn with_derived_from(mut self, d: &DerivedFrom) -> Self { + self.derived_from = Some(d.to_value()); + self + } /// Set the `wai` version string. pub fn with_wai(mut self, wai: impl Into) -> Self { self.wai = wai.into(); @@ -1033,6 +1045,74 @@ impl Refines { } } +/// A well-formed `derived_from` member (SPEC §7.1, *Derived renditions*): the payload +/// presents another object, the **motif**, and was made from it by a named renderer or +/// encoder. +/// +/// - `motif`: the motif's content hash (jwp-receipts §2.1), 64 lowercase hexadecimal +/// digits; +/// - `motif_capability`: the motif's capability; +/// - `renderer`: the renderer or encoder that made the payload, `@`; +/// - `settings`: `BLAKE3("wai:render-settings\x01" ‖ JCS(settings))`, 64 lowercase +/// hexadecimal digits. +/// +/// A sink never presents such a payload's output as meeting the motif's equivalence +/// criterion, its tier or a `replicate` intent. A verifier checks `motif` against a +/// motif it holds; every other member is the producer's statement. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DerivedFrom { + pub motif: String, + pub motif_capability: String, + pub renderer: String, + pub settings: String, +} + +/// Why a `derived_from` member is malformed: what failed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DerivedFromMalformed(pub &'static str); + +impl std::fmt::Display for DerivedFromMalformed { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "derived-from-malformed: {}", self.0) + } +} + +impl std::error::Error for DerivedFromMalformed {} + +impl DerivedFrom { + /// The domain of `settings`. + pub const SETTINGS_DOMAIN: &'static [u8] = b"wai:render-settings\x01"; + + /// Read `derived_from`. Members it does not define are ignored (SPEC §8). + /// Malformed: a value that is not an object; `motif` or `settings` not 64 lowercase + /// hexadecimal digits; `motif_capability` or `renderer` not a non-empty string. + pub fn from_value(v: &Value) -> Result { + let bad = DerivedFromMalformed; + let o = v.as_object().ok_or(bad("derived_from is not an object"))?; + let hex64 = |k: &str, why: &'static str| { + o.get(k).and_then(Value::as_str).filter(|s| RefForm::ContentHash.accepts(s)).map(str::to_string).ok_or(bad(why)) + }; + let text = |k: &str, why: &'static str| o.get(k).and_then(Value::as_str).filter(|s| !s.is_empty()).map(str::to_string).ok_or(bad(why)); + Ok(DerivedFrom { + motif: hex64("motif", "motif is not 64 lowercase hexadecimal digits")?, + motif_capability: text("motif_capability", "motif_capability is not a non-empty string")?, + renderer: text("renderer", "renderer is not a non-empty string")?, + settings: hex64("settings", "settings is not 64 lowercase hexadecimal digits")?, + }) + } + + /// The JSON this member is written as: `motif`, `motif_capability`, `renderer`, + /// `settings`. + pub fn to_value(&self) -> Value { + let mut o = Map::new(); + o.insert("motif".into(), Value::from(self.motif.clone())); + o.insert("motif_capability".into(), Value::from(self.motif_capability.clone())); + o.insert("renderer".into(), Value::from(self.renderer.clone())); + o.insert("settings".into(), Value::from(self.settings.clone())); + Value::Object(o) + } +} + pub struct Wai { pub manifest: Manifest, pub payload: Vec, @@ -1502,6 +1582,11 @@ pub struct RenditionMeta { /// manifest, verbatim from the envelope bytes. #[serde(default, skip_serializing_if = "is_none")] pub envelope_manifest: Option, + /// `derived_from` (SPEC §7.1, *Derived renditions*): this entry's payload presents + /// another object, made from it by a named renderer or encoder. Kept as the JSON it + /// is ([`DerivedFrom::from_value`]). + #[serde(default, skip_serializing_if = "is_none")] + pub derived_from: Option, /// Members this revision does not define, kept so they survive a round trip. #[serde(flatten)] pub extra: Map, @@ -1522,6 +1607,7 @@ impl RenditionMeta { companion_of_id: None, refines: None, envelope_manifest: None, + derived_from: None, extra: Map::new(), } } @@ -1541,6 +1627,11 @@ impl RenditionMeta { self.companion_of = Some(base.into()); self } + /// Set `derived_from` (SPEC §7.1, *Derived renditions*). + pub fn with_derived_from(mut self, d: &DerivedFrom) -> Self { + self.derived_from = Some(d.to_value()); + self + } /// Give the entry an id. pub fn with_id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); @@ -4137,4 +4228,40 @@ mod tests { assert_eq!(v2.matches(&format!("\nwai:{term} a ")).count(), 1, "v2.ttl defines wai:{term} once"); } } + + /// `derived_from` (SPEC §7.1, *Derived renditions*): an OPTIONAL member under the + /// MINOR rule (§8). Written after `refines`, read back to the value it was, absent + /// from the bytes when unset, and a malformed value still parses (its typed reading + /// reports it). + #[test] + fn derived_from_is_an_optional_member_that_never_breaks_a_parse() { + let d = DerivedFrom { motif: "ab".repeat(32), motif_capability: "wai.video.int_motion".into(), renderer: "enc@1.0".into(), settings: "cd".repeat(32) }; + let m = Manifest::new("video", "replicate", ModelRequirement::new("wai.video.av1"), Conditioning::new("av1")); + let plain = Wai::new(m.clone(), b"p".to_vec()).to_bytes().unwrap(); + assert!(!String::from_utf8_lossy(&plain).contains("derived_from")); + let with = Wai::new(m.with_derived_from(&d), b"p".to_vec()).to_bytes().unwrap(); + let text = String::from_utf8_lossy(&with).to_string(); + assert!(text.contains(&format!("\"derived_from\":{{\"motif\":\"{}\",\"motif_capability\":\"wai.video.int_motion\",\"renderer\":\"enc@1.0\",\"settings\":\"{}\"}},\"target\"", "ab".repeat(32), "cd".repeat(32))), "{text}"); + let back = Wai::from_bytes(&with).unwrap(); + assert_eq!(DerivedFrom::from_value(back.manifest.derived_from.as_ref().unwrap()).unwrap(), d); + assert_eq!(back.to_bytes().unwrap(), with, "the member round-trips byte for byte"); + // A malformed member parses; the typed reading names what is wrong. + let bad = String::from_utf8(with[8..8 + u32::from_le_bytes(with[4..8].try_into().unwrap()) as usize].to_vec()).unwrap().replace(&"ab".repeat(32), &"AB".repeat(32)); + let m2: Manifest = serde_json::from_str(&bad).unwrap(); + assert_eq!(DerivedFrom::from_value(m2.derived_from.as_ref().unwrap()).unwrap_err().0, "motif is not 64 lowercase hexadecimal digits"); + for (v, why) in [ + (serde_json::json!(1), "derived_from is not an object"), + (serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "", "renderer": "r", "settings": "cd".repeat(32)}), "motif_capability is not a non-empty string"), + (serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "c", "settings": "cd".repeat(32)}), "renderer is not a non-empty string"), + (serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "c", "renderer": "r", "settings": "x"}), "settings is not 64 lowercase hexadecimal digits"), + ] { + assert_eq!(DerivedFrom::from_value(&v).unwrap_err().0, why); + } + // An entry of a WAI2 carries it the same way. + let e = RenditionMeta::new("wai.video.av1", "av1").with_derived_from(&d); + let j = serde_json::to_string(&e).unwrap(); + assert!(j.ends_with(&format!("\"settings\":\"{}\"}}}}", "cd".repeat(32))), "{j}"); + let back: RenditionMeta = serde_json::from_str(&j).unwrap(); + assert_eq!(DerivedFrom::from_value(back.derived_from.as_ref().unwrap()).unwrap(), d); + } } diff --git a/wai/wai-rs/src/energy_figure.rs b/wai/wai-rs/src/energy_figure.rs index 439edb6a0..94974ca63 100644 --- a/wai/wai-rs/src/energy_figure.rs +++ b/wai/wai-rs/src/energy_figure.rs @@ -241,6 +241,19 @@ single_figure!("splat4d", crate::splat4d::Splat4dReceipt, "wai:splat4d-receipt-i single_figure!("audio_scene", crate::audio_scene::SceneReceipt, "wai:audio-receipt-id"); single_figure!("ternary_receipt", crate::ternary_receipt::TernaryReceipt, "wai:ternary-receipt-id"); +/// A version-2 package receipt (packaging §9), viewed with its components' index files +/// in its order: it verifies only against them, so the view holds both. Its profile is +/// version 1's, `wai:video-package-receipt-id`: the two versions share the identity +/// domain. +#[cfg(feature = "video_package")] +impl EnergyFigures for (&crate::package::PackageReceiptV2, &[&[u8]]) { + fn figure_view(&self) -> Option { + let (r, indexes) = *self; + r.verify_detailed(indexes).ok()?; + Some(FigureView::single("wai:video-package-receipt-id", r.receipt_hash(), r.joules_micro, r.energy_provenance.clone(), r.signer_pubkey)) + } +} + /// A provenance receipt (`wai:asset-receipt-id`): one figure per step, indexed from 0. impl EnergyFigures for crate::provenance::AssetReceipt { fn figure_view(&self) -> Option { diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 2fae3496b..5c075baf4 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -511,6 +511,16 @@ pub mod wai_video_keys; #[cfg(feature = "video_package")] pub mod wai_video_package; +// The packaging extension's stage index (extensions/packaging.md §6, `WSIX`) and the +// version-2 package receipt over it (§9): per component, every WAI object of a package +// by content hash, its role, layer, prefix tier and digest, and the class plan; one +// signed receipt binding the policy digest, each index by digest and stage root, and +// counts every verifier recomputes. +#[cfg(feature = "video_package")] +pub mod stage_index; +#[cfg(feature = "video_package")] +pub mod package; + // Virtual linear / FAST channel assembly (wai.video.channel): the capstone. A // continuous schedule (gapless, non-overlapping — verify() rejects dead air) // whose program-as-run per window is recomputed, not logged. Windows chain via diff --git a/wai/wai-rs/src/package.rs b/wai/wai-rs/src/package.rs new file mode 100644 index 000000000..d897f082e --- /dev/null +++ b/wai/wai-rs/src/package.rs @@ -0,0 +1,732 @@ +//! The version-2 package receipt (`extensions/packaging.md` §9): one signed statement +//! over a whole package, binding its policy digest and, per component, the stage index +//! (`WSIX`, [`crate::stage_index`]) by its digest and its stage root. The counts it signs +//! (entries, distinct objects, bytes referenced and bytes stored) are recomputed from the +//! indexes by every verifier, so a package cannot state a saving its indexes do not +//! hold. +//! +//! The version-1 receipt (`wai_video_package::PackageReceipt`) keeps its bytes and still +//! verifies. Both share the identity domain `wai:video-package-receipt-id\x01`, so the +//! figure profile `wai:video-package-receipt-id` (energy-measurement §6.1) covers both; +//! their signing domains differ, so no version-2 payload can equal a version-1 payload. +//! [`AnyPackageReceipt`] reads either from its JSON. +//! +//! ```text +//! labelled_domain("wai:pack-receipt\x01", label) +//! u32 len asset_id ‖ asset_id ‖ policy_digest[32] +//! u8 parent_kind (0 none, 1 live-window, 2 re-pack) ‖ [parent_receipt_hash[32] if kind ≠ 0] +//! u32 n_components ‖ n × ( u32 len id ‖ id ‖ u8 media_class ‖ index_digest[32] ‖ stage_root[32] ) +//! u64 total_entries ‖ u64 unique_objects ‖ u64 total_bytes ‖ u64 stored_bytes ‖ u64 work_units +//! u32 len packager ‖ packager +//! u64 joules_micro ‖ write_label(label) +//! grant_hash[32] ‖ u32 len capability ‖ capability ‖ u64 joule_ceiling_micro ‖ u8 + [u64] funds_ceiling +//! signer_pubkey[32] ‖ u32 len signer_id ‖ signer_id +//! receipt_hash = BLAKE3("wai:video-package-receipt-id\x01" ‖ payload ‖ sig) +//! ``` +//! +//! Integers are big-endian, as in every receipt of the family. + +use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; +use serde_json::{json, Map, Value}; + +use crate::energy_class::{ + check_label, figure_and_class_cohere, json_fields, label_from_json, labelled_domain, write_label, JSON_CALIBRATION_REF, JSON_CLASS, JSON_FILTERING, JSON_UNCERTAINTY, +}; +use crate::jcs::{jcs, MAX_INTEGER}; +use crate::quantum_ops::GrantRef; +use crate::stage_index::{IndexReason, MediaClass, StageIndex}; +use crate::wai_video_manifest::EnergyProvenance; +use crate::wai_video_package::PackageReceipt; + +/// The version-2 signing domain. +/// +/// cbindgen:ignore +pub const DOMAIN_RECEIPT_V2: &[u8] = b"wai:pack-receipt\x01"; +/// The identity domain both versions share. +/// +/// cbindgen:ignore +pub const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-package-receipt-id\x01"; +/// The `kind` member of a package receipt's JSON. +/// +/// cbindgen:ignore +pub const KIND: &str = "package"; + +/// Why a package receipt, or a package against it, is refused. The code is a reason of +/// packaging §12: an index's own (`dup-flag`, `tier-weakest`, …) or the receipt's +/// (`receipt-*`). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackageReason { + pub code: &'static str, + pub detail: String, +} + +impl PackageReason { + fn new(code: &'static str, detail: impl Into) -> PackageReason { + PackageReason { code, detail: detail.into() } + } + + fn index(component: &str, r: IndexReason) -> PackageReason { + PackageReason { code: r.code, detail: format!("component {component}: {}", r.detail) } + } +} + +impl std::fmt::Display for PackageReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.detail) + } +} + +impl std::error::Error for PackageReason {} + +fn err(code: &'static str, detail: impl Into) -> Result { + Err(PackageReason::new(code, detail)) +} + +/// What a package was made after (§9.2). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Parent { + /// A package with no parent. + None, + /// A live window: the receipt of the window before it. + LiveWindow([u8; 32]), + /// A re-pack: the receipt of the package it re-packs. + RePack([u8; 32]), +} + +impl Parent { + /// 0, 1 or 2. + pub fn code(self) -> u8 { + match self { + Parent::None => 0, + Parent::LiveWindow(_) => 1, + Parent::RePack(_) => 2, + } + } + + /// `none`, `live-window` or `re-pack`. + pub fn label(self) -> &'static str { + match self { + Parent::None => "none", + Parent::LiveWindow(_) => "live-window", + Parent::RePack(_) => "re-pack", + } + } + + /// The parent's receipt hash. + pub fn hash(self) -> Option<[u8; 32]> { + match self { + Parent::None => None, + Parent::LiveWindow(h) | Parent::RePack(h) => Some(h), + } + } +} + +/// One component's binding: its id, media class, and its stage index's digest and +/// stage root. Its index is the file `index-.wsix`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ComponentBinding { + pub id: String, + pub media_class: MediaClass, + pub index_digest: [u8; 32], + pub stage_root: [u8; 32], +} + +impl ComponentBinding { + /// The index's file name, `index-.wsix`. + pub fn index_file(&self) -> String { + index_file(&self.id) + } +} + +/// A component's index file name, `index-.wsix`. +pub fn index_file(id: &str) -> String { + format!("index-{id}.wsix") +} + +/// A signed version-2 package receipt. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PackageReceiptV2 { + pub asset_id: String, + pub policy_digest: [u8; 32], + pub parent: Parent, + pub components: Vec, + /// Entries over every component's index. + pub total_entries: u64, + /// Distinct content hashes over every component's index. + pub unique_objects: u64, + /// `Σ byte_len` over every entry. + pub total_bytes: u64, + /// `Σ byte_len` over each content hash's first entry. + pub stored_bytes: u64, + /// Portable work: the entries packed (= `total_entries`). + pub work_units: u64, + /// The packager, `builtin:@`. + pub packager: String, + /// The energy figure, `0` with no class when unmetered. + pub joules_micro: u64, + pub energy_provenance: Option, + pub grant: GrantRef, + pub signer_pubkey: [u8; 32], + pub signer_id: String, + pub sig: [u8; 64], +} + +/// The counts a package's indexes give: entries, distinct objects, bytes referenced and +/// bytes stored (one copy per content hash). A content hash at two lengths has no count: +/// `receipt-counts`. +pub fn counts(indexes: &[StageIndex]) -> Result<(u64, u64, u64, u64), PackageReason> { + let items = indexes.iter().flat_map(|ix| ix.entries.iter().map(|e| (e.content_hash, e.byte_len))); + let t = crate::content_dedup::totals_checked(items) + .map_err(|(h, a, b)| PackageReason::new("receipt-counts", format!("{} at {a} and {b} bytes", hex(&h))))?; + Ok((t.objects, t.unique, t.referenced_bytes, t.stored_bytes)) +} + +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn unhex(s: &str) -> Option<[u8; N]> { + if s.len() != 2 * N || !s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) { + return None; + } + let mut out = [0u8; N]; + for (i, o) in out.iter_mut().enumerate() { + *o = u8::from_str_radix(&s[2 * i..2 * i + 2], 16).ok()?; + } + Some(out) +} + +fn write_grant(g: &GrantRef, o: &mut Vec) { + o.extend_from_slice(&g.grant_hash); + o.extend_from_slice(&(g.capability.len() as u32).to_be_bytes()); + o.extend_from_slice(g.capability.as_bytes()); + o.extend_from_slice(&g.joule_ceiling_micro.to_be_bytes()); + match g.funds_ceiling { + Some(f) => { + o.push(1); + o.extend_from_slice(&f.to_be_bytes()); + } + None => o.push(0), + } +} + +/// A grant as JSON: `joule_ceiling_micro` `null` for no ceiling (`u64::MAX`), and +/// `funds_ceiling` `null` for none. +fn grant_json(g: &GrantRef) -> Value { + json!({ + "capability": g.capability, + "funds_ceiling": g.funds_ceiling, + "grant_hash": hex(&g.grant_hash), + "joule_ceiling_micro": if g.joule_ceiling_micro == u64::MAX { Value::Null } else { Value::from(g.joule_ceiling_micro) }, + }) +} + +fn grant_from_json(v: &Value) -> Result { + let bad = |w: &str| PackageReason::new("receipt-json", format!("grant: {w}")); + let o = v.as_object().ok_or_else(|| bad("not an object"))?; + exact_members(o, &["capability", "funds_ceiling", "grant_hash", "joule_ceiling_micro"]).map_err(|m| bad(&m))?; + let int = |k: &str| match o.get(k) { + Some(Value::Null) => Ok(None), + Some(x) => x.as_u64().filter(|&n| n <= MAX_INTEGER).map(Some).ok_or_else(|| bad(k)), + None => Err(bad(k)), + }; + Ok(GrantRef { + grant_hash: o.get("grant_hash").and_then(Value::as_str).and_then(unhex::<32>).ok_or_else(|| bad("grant_hash"))?, + capability: o.get("capability").and_then(Value::as_str).ok_or_else(|| bad("capability"))?.to_string(), + joule_ceiling_micro: int("joule_ceiling_micro")?.unwrap_or(u64::MAX), + funds_ceiling: int("funds_ceiling")?, + }) +} + +/// Whether a grant's integers have a JSON form: every one at most `2^53 − 1`, except a +/// joule ceiling of `u64::MAX` (none), written `null`. +fn grant_fits_json(g: &GrantRef) -> bool { + (g.joule_ceiling_micro == u64::MAX || g.joule_ceiling_micro <= MAX_INTEGER) && g.funds_ceiling.is_none_or(|f| f <= MAX_INTEGER) +} + +/// The object's members are exactly `names` plus, where `label` is true, the energy +/// label's fields. +fn exact_members(o: &Map, names: &[&str]) -> Result<(), String> { + let label = [JSON_CLASS, JSON_UNCERTAINTY, JSON_FILTERING, JSON_CALIBRATION_REF]; + for k in o.keys() { + if !names.contains(&k.as_str()) && !label.contains(&k.as_str()) { + return Err(format!("a member {k:?} this version does not define")); + } + } + for n in names { + if !o.contains_key(*n) { + return Err(format!("no member {n:?}")); + } + } + Ok(()) +} + +/// A receipt's JSON text read as a value: RFC 8259, with no object naming a member twice +/// (`receipt-json` otherwise). +fn read_json(text: &str) -> Result { + let bad = |e: serde_json::Error| PackageReason::new("receipt-json", e.to_string()); + crate::container::check_unique_members(text.as_bytes()).map_err(bad)?; + serde_json::from_str(text).map_err(bad) +} + +fn str_of<'a>(o: &'a Map, k: &str) -> Result<&'a str, PackageReason> { + o.get(k).and_then(Value::as_str).ok_or_else(|| PackageReason::new("receipt-json", format!("{k} is not a string"))) +} + +fn u64_of(o: &Map, k: &str) -> Result { + o.get(k).and_then(Value::as_u64).filter(|&n| n <= MAX_INTEGER).ok_or_else(|| PackageReason::new("receipt-json", format!("{k} is not an integer 0..=2^53-1"))) +} + +fn hex_of(o: &Map, k: &str) -> Result<[u8; N], PackageReason> { + o.get(k).and_then(Value::as_str).and_then(unhex::).ok_or_else(|| PackageReason::new("receipt-json", format!("{k} is not {} lowercase hexadecimal digits", 2 * N))) +} + +impl PackageReceiptV2 { + /// The signing payload. + pub fn signing_payload(&self) -> Vec { + let label = self.energy_provenance.as_ref(); + let mut o = labelled_domain(DOMAIN_RECEIPT_V2, label); + o.extend_from_slice(&(self.asset_id.len() as u32).to_be_bytes()); + o.extend_from_slice(self.asset_id.as_bytes()); + o.extend_from_slice(&self.policy_digest); + o.push(self.parent.code()); + if let Some(h) = self.parent.hash() { + o.extend_from_slice(&h); + } + o.extend_from_slice(&(self.components.len() as u32).to_be_bytes()); + for c in &self.components { + o.extend_from_slice(&(c.id.len() as u32).to_be_bytes()); + o.extend_from_slice(c.id.as_bytes()); + o.push(c.media_class.code() as u8); + o.extend_from_slice(&c.index_digest); + o.extend_from_slice(&c.stage_root); + } + for n in [self.total_entries, self.unique_objects, self.total_bytes, self.stored_bytes, self.work_units] { + o.extend_from_slice(&n.to_be_bytes()); + } + o.extend_from_slice(&(self.packager.len() as u32).to_be_bytes()); + o.extend_from_slice(self.packager.as_bytes()); + o.extend_from_slice(&self.joules_micro.to_be_bytes()); + write_label(label, &mut o); + write_grant(&self.grant, &mut o); + o.extend_from_slice(&self.signer_pubkey); + o.extend_from_slice(&(self.signer_id.len() as u32).to_be_bytes()); + o.extend_from_slice(self.signer_id.as_bytes()); + o + } + + /// `BLAKE3("wai:video-package-receipt-id\x01" ‖ payload ‖ sig)`. + pub fn receipt_hash(&self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_RECEIPT_ID); + h.update(&self.signing_payload()); + h.update(&self.sig); + *h.finalize().as_bytes() + } + + /// Seal a package: each component's id and index file, in the order the receipt + /// lists them. Every index is parsed and checked (§6), its media class read from its + /// header and its component id compared with the one given, and the counts computed + /// from the indexes. Refused: an index that does not parse or check; a component + /// whose index names another id or another policy digest (`receipt-component`, + /// `receipt-policy-digest`); two components of one id (`receipt-component`); a parent + /// that does not match the indexes' `prev_index_digest` (`receipt-parent-kind`); an + /// energy figure and class that do not cohere (`receipt-energy`); and a value with no + /// JSON form, an integer past `2^53 − 1` (`receipt-json`). + #[allow(clippy::too_many_arguments)] + pub fn seal( + signer: &SigningKey, + signer_id: &str, + asset_id: &str, + policy_digest: [u8; 32], + indexes: &[(String, Vec)], + packager: &str, + energy: (u64, Option), + grant: GrantRef, + parent: Parent, + ) -> Result { + if indexes.is_empty() { + return err("receipt-index-count", "a package has at least one component"); + } + let mut parsed = Vec::with_capacity(indexes.len()); + let mut components = Vec::with_capacity(indexes.len()); + for (id, bytes) in indexes { + let ix = StageIndex::parse(bytes).map_err(|r| PackageReason::index(id, r))?; + ix.check().map_err(|r| PackageReason::index(id, r))?; + if ix.component() != id || components.iter().any(|c: &ComponentBinding| &c.id == id) { + return err("receipt-component", format!("component {id}: its index names {}", ix.component())); + } + if ix.header.policy_digest != policy_digest { + return err("receipt-policy-digest", format!("component {id}: its index states another policy digest")); + } + components.push(ComponentBinding { id: id.clone(), media_class: ix.header.media_class, index_digest: crate::stage_index::digest_of(bytes), stage_root: ix.stage_root() }); + parsed.push(ix); + } + check_parent(parent, &parsed)?; + let (total_entries, unique_objects, total_bytes, stored_bytes) = counts(&parsed)?; + let (joules_micro, energy_provenance) = energy; + let r = PackageReceiptV2 { + asset_id: asset_id.to_string(), + policy_digest, + parent, + components, + total_entries, + unique_objects, + total_bytes, + stored_bytes, + work_units: total_entries, + packager: packager.to_string(), + joules_micro, + energy_provenance, + grant, + signer_pubkey: signer.verifying_key().to_bytes(), + signer_id: signer_id.to_string(), + sig: [0u8; 64], + }; + r.check_energy()?; + let mut r = r; + r.sig = signer.sign(&r.signing_payload()).to_bytes(); + r.json_value().map_err(|why| PackageReason::new("receipt-json", why))?; + Ok(r) + } + + fn check_energy(&self) -> Result<(), PackageReason> { + let label = self.energy_provenance.as_ref(); + if !figure_and_class_cohere(self.joules_micro, label) || check_label(self.joules_micro, label).is_err() { + return err("receipt-energy", "the energy figure and its class do not cohere"); + } + Ok(()) + } + + /// [`Self::verify_detailed`] as a predicate. + pub fn verify(&self, indexes: &[&[u8]]) -> bool { + self.verify_detailed(indexes).is_ok() + } + + /// Verify the receipt against its components' index files, in its order: + /// + /// 1. at least one component, and one index per component (`receipt-index-count`), a + /// live window's included; each parses and checks (its + /// own reason), hashes to the component's `index_digest` (`receipt-index-digest`), + /// has its `stage_root` (`receipt-stage-root`), names its id and media class + /// (`receipt-component`) and the receipt's policy digest (`receipt-policy-digest`); + /// 2. the counts, recomputed (`receipt-counts`); + /// 3. the parent kind against the indexes' `prev_index_digest`: zero for none and + /// a re-pack, non-zero for a live window (`receipt-parent-kind`); + /// 4. the energy figure and its class (`receipt-energy`); + /// 5. the signature (`receipt-signature`). + pub fn verify_detailed(&self, indexes: &[&[u8]]) -> Result<(), PackageReason> { + if self.components.is_empty() || indexes.len() != self.components.len() { + return err("receipt-index-count", format!("{} indexes for {} components; a package has at least one", indexes.len(), self.components.len())); + } + let mut parsed = Vec::with_capacity(indexes.len()); + for (c, bytes) in self.components.iter().zip(indexes) { + let ix = StageIndex::parse(bytes).map_err(|r| PackageReason::index(&c.id, r))?; + ix.check().map_err(|r| PackageReason::index(&c.id, r))?; + if crate::stage_index::digest_of(bytes) != c.index_digest { + return err("receipt-index-digest", format!("component {}: the index does not hash to index_digest", c.id)); + } + if ix.stage_root() != c.stage_root { + return err("receipt-stage-root", format!("component {}: the index's stage root differs", c.id)); + } + if ix.component() != c.id || ix.header.media_class != c.media_class { + return err("receipt-component", format!("component {}: the index names {} ({})", c.id, ix.component(), ix.header.media_class.label())); + } + if ix.header.policy_digest != self.policy_digest { + return err("receipt-policy-digest", format!("component {}: its index states another policy digest", c.id)); + } + parsed.push(ix); + } + let ids: std::collections::BTreeSet<&str> = self.components.iter().map(|c| c.id.as_str()).collect(); + if ids.len() != self.components.len() { + return err("receipt-component", "two components of one id"); + } + let got = counts(&parsed)?; + if got != (self.total_entries, self.unique_objects, self.total_bytes, self.stored_bytes) || self.work_units != self.total_entries { + return err("receipt-counts", format!("the indexes give {got:?}")); + } + check_parent(self.parent, &parsed)?; + self.check_energy()?; + let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { + return err("receipt-signature", "signer_pubkey is not an Ed25519 key"); + }; + k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).map_err(|_| PackageReason::new("receipt-signature", "the signature does not verify")) + } + + fn json_value(&self) -> Result { + if !grant_fits_json(&self.grant) { + return Err("a grant ceiling past 2^53 - 1".into()); + } + let comps: Vec = self + .components + .iter() + .map(|c| { + json!({ + "id": c.id, + "index": c.index_file(), + "index_digest": hex(&c.index_digest), + "media_class": c.media_class.label(), + "stage_root": hex(&c.stage_root), + }) + }) + .collect(); + let mut o = json!({ + "asset_id": self.asset_id, + "components": comps, + "energy_provenance": Value::Null, + "grant": grant_json(&self.grant), + "joules_micro": self.joules_micro, + "kind": KIND, + "packager": self.packager, + "parent_kind": self.parent.label(), + "parent_receipt_hash": self.parent.hash().map(|h| hex(&h)), + "policy_digest": hex(&self.policy_digest), + "receipt_hash": hex(&self.receipt_hash()), + "sig": hex(&self.sig), + "signer_id": self.signer_id, + "signer_pubkey": hex(&self.signer_pubkey), + "stored_bytes": self.stored_bytes, + "total_bytes": self.total_bytes, + "total_entries": self.total_entries, + "unique_objects": self.unique_objects, + "version": 2, + "work_units": self.work_units, + }); + if let (Some(p), Value::Object(m)) = (&self.energy_provenance, &mut o) { + for (k, v) in json_fields(p) { + m.insert(k.to_string(), v); + } + } + jcs(&o).map_err(|e| e.to_string())?; + Ok(o) + } + + /// The receipt as JSON: RFC 8785 canonical (members in code-unit order, no + /// insignificant whitespace), hexadecimal lowercase, integers as numbers. An energy + /// label writes its class as `energy_provenance` and its evidence beside it; an + /// unmetered receipt writes `energy_provenance` as `null` and no evidence. + pub fn to_json(&self) -> String { + let v = self.json_value().expect("a sealed receipt has a JSON form"); + jcs(&v).expect("checked at seal") + } + + /// Read a receipt from its JSON (§9.1). Refused (`receipt-json`): JSON that does not + /// parse or names a member twice in one object, a member this version does not define + /// or one it requires missing (`energy_provenance` among them, `null` when unmetered), + /// a value of the wrong form (a negative number, `-0` included), a class's evidence + /// other than exactly what the class carries or `null` in its place, a parent kind and + /// hash that disagree, an `index` that is not `index-.wsix`, and a `receipt_hash` + /// that is not the receipt's. The signature is not checked here: + /// [`Self::verify_detailed`] does. A reader does not require the canonical form; a + /// package's file must be it (§11.3). + pub fn from_json(text: &str) -> Result { + let bad = |w: String| PackageReason::new("receipt-json", w); + let v = read_json(text)?; + let o = v.as_object().ok_or_else(|| bad("not an object".into()))?; + exact_members( + o, + &[ + "asset_id", "components", "energy_provenance", "grant", "joules_micro", "kind", "packager", "parent_kind", "parent_receipt_hash", "policy_digest", + "receipt_hash", "sig", "signer_id", "signer_pubkey", "stored_bytes", "total_bytes", "total_entries", "unique_objects", "version", "work_units", + ], + ) + .map_err(bad)?; + if str_of(o, "kind")? != KIND || u64_of(o, "version")? != 2 { + return Err(bad("not a version-2 package receipt".into())); + } + let parent_hash = match o.get("parent_receipt_hash") { + Some(Value::Null) => None, + _ => Some(hex_of::<32>(o, "parent_receipt_hash")?), + }; + let parent = match (str_of(o, "parent_kind")?, parent_hash) { + ("none", None) => Parent::None, + ("live-window", Some(h)) => Parent::LiveWindow(h), + ("re-pack", Some(h)) => Parent::RePack(h), + _ => return Err(bad("parent_kind and parent_receipt_hash disagree".into())), + }; + let mut components = Vec::new(); + for c in o.get("components").and_then(Value::as_array).ok_or_else(|| bad("components is not an array".into()))? { + let c = c.as_object().ok_or_else(|| bad("a component is not an object".into()))?; + exact_members(c, &["id", "index", "index_digest", "media_class", "stage_root"]).map_err(bad)?; + let id = str_of(c, "id")?.to_string(); + if str_of(c, "index")? != index_file(&id) { + return Err(bad(format!("component {id}: index is not {}", index_file(&id)))); + } + let media_class = MediaClass::from_label(str_of(c, "media_class")?).ok_or_else(|| bad("media_class".into()))?; + components.push(ComponentBinding { id, media_class, index_digest: hex_of(c, "index_digest")?, stage_root: hex_of(c, "stage_root")? }); + } + let energy_provenance = label_from_json(o).map_err(|e| bad(format!("energy label: {e:?}")))?; + // Version 2 writes its class's evidence exactly as the class carries it, never a + // null in its place. + let want: Vec<&str> = energy_provenance.as_ref().map(|p| json_fields(p).into_iter().map(|(k, _)| k).filter(|k| *k != JSON_CLASS).collect()).unwrap_or_default(); + for k in [JSON_UNCERTAINTY, JSON_FILTERING, JSON_CALIBRATION_REF] { + if o.contains_key(k) != want.contains(&k) || o.get(k).is_some_and(Value::is_null) { + return Err(bad(format!("{k} is not its class's evidence"))); + } + } + let r = PackageReceiptV2 { + asset_id: str_of(o, "asset_id")?.to_string(), + policy_digest: hex_of(o, "policy_digest")?, + parent, + components, + total_entries: u64_of(o, "total_entries")?, + unique_objects: u64_of(o, "unique_objects")?, + total_bytes: u64_of(o, "total_bytes")?, + stored_bytes: u64_of(o, "stored_bytes")?, + work_units: u64_of(o, "work_units")?, + packager: str_of(o, "packager")?.to_string(), + joules_micro: u64_of(o, "joules_micro")?, + energy_provenance, + grant: grant_from_json(o.get("grant").unwrap_or(&Value::Null))?, + signer_pubkey: hex_of(o, "signer_pubkey")?, + signer_id: str_of(o, "signer_id")?.to_string(), + sig: hex_of(o, "sig")?, + }; + if hex_of::<32>(o, "receipt_hash")? != r.receipt_hash() { + return Err(bad("receipt_hash is not the receipt's".into())); + } + Ok(r) + } +} + +/// §9.2 and verification step 3: a package with no parent, or a re-pack, has +/// `prev_index_digest` zero in every index; a live window has it non-zero in every one. +fn check_parent(parent: Parent, indexes: &[StageIndex]) -> Result<(), PackageReason> { + let live = matches!(parent, Parent::LiveWindow(_)); + for ix in indexes { + if (ix.header.prev_index_digest != [0u8; 32]) != live { + return err("receipt-parent-kind", format!("parent kind {} with component {}'s prev_index_digest", parent.label(), ix.component())); + } + } + Ok(()) +} + +/// The version-1 receipt as JSON (§9.3), and back. v1's class is a member of the receipt +/// from the start: `energy_provenance` is `null` when unmetered, and otherwise the label +/// with its evidence. +impl PackageReceipt { + fn json_value(&self) -> Result { + if !grant_fits_json(&self.grant) { + return Err("a grant ceiling past 2^53 - 1".into()); + } + let segs: Vec = self + .segments + .iter() + .map(|s| json!({"byte_len": s.byte_len, "content_hash": hex(&s.content_hash), "duration_ms": s.duration_ms, "index": s.index})) + .collect(); + let mut o = json!({ + "asset_id": self.asset_id, + "energy_provenance": Value::Null, + "grant": grant_json(&self.grant), + "joules_micro": self.joules_micro, + "kind": KIND, + "parent_receipt_hash": self.parent_receipt_hash.map(|h| hex(&h)), + "receipt_hash": hex(&self.receipt_hash()), + "segment_map_root": hex(&self.segment_map_root), + "segments": segs, + "sig": hex(&self.sig), + "signer_id": self.signer_id, + "signer_pubkey": hex(&self.signer_pubkey), + "stored_bytes": self.stored_bytes, + "total_bytes": self.total_bytes, + "total_segments": self.total_segments, + "unique_segments": self.unique_segments, + "version": 1, + "work_segment_ops": self.work_segment_ops, + }); + if let (Some(p), Value::Object(m)) = (&self.energy_provenance, &mut o) { + for (k, v) in json_fields(p) { + m.insert(k.to_string(), v); + } + } + jcs(&o).map_err(|e| e.to_string())?; + Ok(o) + } + + /// The receipt as JSON: RFC 8785 canonical, `"version":1`. `None` for a receipt with + /// an integer past `2^53 − 1`, which has no JSON form. + pub fn to_json(&self) -> Option { + jcs(&self.json_value().ok()?).ok() + } + + /// Read a version-1 receipt from its JSON (`"version"` 1, or absent). Refused as + /// [`PackageReceiptV2::from_json`] refuses, except that `energy_provenance` may be + /// absent (read as unmetered), a `null` evidence member is absent, and an on-chip + /// counter's filtering, absent, reads as unknown: version-1 receipts were issued before + /// this form, and they keep verifying. The signature is checked by `verify`. + pub fn from_json(text: &str) -> Result { + use crate::wai_video_package::SegmentEntry; + let bad = |w: String| PackageReason::new("receipt-json", w); + let v = read_json(text)?; + let o = v.as_object().ok_or_else(|| bad("not an object".into()))?; + let mut names = vec![ + "asset_id", "grant", "joules_micro", "kind", "parent_receipt_hash", "receipt_hash", "segment_map_root", "segments", "sig", "signer_id", "signer_pubkey", + "stored_bytes", "total_bytes", "total_segments", "unique_segments", "work_segment_ops", + ]; + if o.contains_key("version") { + names.push("version"); + if u64_of(o, "version")? != 1 { + return Err(bad("not a version-1 package receipt".into())); + } + } + exact_members(o, &names).map_err(bad)?; + if str_of(o, "kind")? != KIND { + return Err(bad("not a package receipt".into())); + } + let mut segments = Vec::new(); + for s in o.get("segments").and_then(Value::as_array).ok_or_else(|| bad("segments is not an array".into()))? { + let s = s.as_object().ok_or_else(|| bad("a segment is not an object".into()))?; + exact_members(s, &["byte_len", "content_hash", "duration_ms", "index"]).map_err(bad)?; + segments.push(SegmentEntry { index: u64_of(s, "index")?, content_hash: hex_of(s, "content_hash")?, duration_ms: u64_of(s, "duration_ms")?, byte_len: u64_of(s, "byte_len")? }); + } + let parent_receipt_hash = match o.get("parent_receipt_hash") { + Some(Value::Null) => None, + _ => Some(hex_of::<32>(o, "parent_receipt_hash")?), + }; + let r = PackageReceipt { + asset_id: str_of(o, "asset_id")?.to_string(), + segment_map_root: hex_of(o, "segment_map_root")?, + segments, + total_segments: u64_of(o, "total_segments")?, + unique_segments: u64_of(o, "unique_segments")?, + total_bytes: u64_of(o, "total_bytes")?, + stored_bytes: u64_of(o, "stored_bytes")?, + work_segment_ops: u64_of(o, "work_segment_ops")?, + joules_micro: u64_of(o, "joules_micro")?, + energy_provenance: label_from_json(o).map_err(|e| bad(format!("energy label: {e:?}")))?, + grant: grant_from_json(o.get("grant").unwrap_or(&Value::Null))?, + parent_receipt_hash, + signer_pubkey: hex_of(o, "signer_pubkey")?, + signer_id: str_of(o, "signer_id")?.to_string(), + sig: hex_of(o, "sig")?, + }; + if hex_of::<32>(o, "receipt_hash")? != r.receipt_hash() { + return Err(bad("receipt_hash is not the receipt's".into())); + } + Ok(r) + } +} + +/// A package receipt of either version. +#[derive(Clone, Debug, PartialEq)] +pub enum AnyPackageReceipt { + V1(PackageReceipt), + V2(PackageReceiptV2), +} + +impl AnyPackageReceipt { + /// Read either version from its JSON, by its `"version"` member: absent or 1 is + /// version 1, 2 is version 2, any other refused (`receipt-version`). + pub fn from_json(text: &str) -> Result { + let v = read_json(text)?; + match v.get("version") { + None => PackageReceipt::from_json(text).map(AnyPackageReceipt::V1), + Some(n) if n.as_u64() == Some(1) => PackageReceipt::from_json(text).map(AnyPackageReceipt::V1), + Some(n) if n.as_u64() == Some(2) => PackageReceiptV2::from_json(text).map(AnyPackageReceipt::V2), + Some(n) => err("receipt-version", format!("version {n}")), + } + } +} + +#[cfg(test)] +mod tests; diff --git a/wai/wai-rs/src/package/tests.rs b/wai/wai-rs/src/package/tests.rs new file mode 100644 index 000000000..7d6e8a166 --- /dev/null +++ b/wai/wai-rs/src/package/tests.rs @@ -0,0 +1,315 @@ +use super::*; +use crate::stage_index::{ClassRec, EntryRec, Header, PieceRec, Role, StageIndex, NO_REFINES, SSE_UNKNOWN, TIER_DECODE}; + +fn key(s: u8) -> SigningKey { + SigningKey::from_bytes(&[s; 32]) +} + +/// A one-piece index of component `id`, its base object named by `hash` at `len` bytes; +/// `prev` its previous window's digest. +fn index(id: &str, media: MediaClass, objects: &[([u8; 32], u64)], prev: [u8; 32]) -> Vec { + let mut seen = Vec::new(); + let entries: Vec = objects + .iter() + .enumerate() + .map(|(i, (h, len))| { + let dup = seen.contains(h); + seen.push(*h); + EntryRec { + content_hash: *h, + byte_len: *len, + piece: i as u64, + refines: NO_REFINES, + capability: 1, + role: Role::Base, + layer: 0, + prefix_tier: TIER_DECODE, + flags: if dup { crate::stage_index::ENTRY_DUP } else { 0 }, + deadline_ms: 0, + prefix_digest: [3; 32], + sse: SSE_UNKNOWN, + manifest_len: 10, + payload_len: len - 22, + } + }) + .collect(); + StageIndex { + header: Header { + media_class: media, + tb_num: 25, + tb_den: 1, + policy_digest: [5; 32], + prev_index_digest: prev, + first_piece: 0, + first_entry: 0, + plan_digest: [0; 32], + component: 0, + }, + pieces: (0..objects.len() as u64) + .map(|i| PieceRec { start: 25 * i, duration: 25, first_entry: i as u32, n_entries: 1, kind: 0, flags: 0, rules: 0, moments: 0 }) + .collect(), + entries, + classes: vec![ClassRec { name: 2, max_layer: 0, flags: 0, max_kbps: 0 }], + plan: vec![vec![0; objects.len()]], + strings: vec![id.into(), "wai.video.int_motion".into(), "tv".into()], + } + .to_bytes() +} + +/// Two components: `v` with a bumper twice (one stored copy), `a` repeating one of `v`'s +/// objects (a content hash shared across components is one stored copy too). +fn indexes(prev: [u8; 32]) -> Vec<(String, Vec)> { + vec![ + ("v".into(), index("v", MediaClass::Video, &[([1; 32], 100), ([2; 32], 40), ([1; 32], 100)], prev)), + ("a".into(), index("a", MediaClass::Audio, &[([2; 32], 40), ([3; 32], 30)], prev)), + ] +} + +fn seal(energy: (u64, Option), parent: Parent) -> (PackageReceiptV2, Vec<(String, Vec)>) { + let prev = if matches!(parent, Parent::LiveWindow(_)) { [9; 32] } else { [0; 32] }; + let ix = indexes(prev); + let r = PackageReceiptV2::seal(&key(1), "did:key:pkg", "asset-01", [5; 32], &ix, "builtin:wai-pack@0.1.0", energy, GrantRef::unbounded("pack"), parent).unwrap(); + (r, ix) +} + +fn refs(ix: &[(String, Vec)]) -> Vec<&[u8]> { + ix.iter().map(|(_, b)| b.as_slice()).collect() +} + +#[test] +fn a_sealed_receipt_verifies_and_recomputes_its_counts() { + let (r, ix) = seal((0, None), Parent::None); + assert_eq!((r.total_entries, r.unique_objects, r.total_bytes, r.stored_bytes, r.work_units), (5, 3, 310, 170, 5)); + assert_eq!(r.components[0].media_class, MediaClass::Video); + assert_eq!(r.components[1].index_file(), "index-a.wsix"); + r.verify_detailed(&refs(&ix)).unwrap(); + // A receipt is not a v1 payload: the domains differ from their first bytes on. + assert!(r.signing_payload().starts_with(b"wai:pack-receipt\x01")); +} + +/// Test 6 (the receipt rules) and the rest of verification, each with its reason. +#[test] +fn every_receipt_mutation_names_its_reason() { + let (r, ix) = seal((0, None), Parent::None); + let resign = |mut r: PackageReceiptV2| { + r.sig = key(1).sign(&r.signing_payload()).to_bytes(); + r + }; + let code = |r: &PackageReceiptV2, ix: &[&[u8]]| r.verify_detailed(ix).map(|_| "ok").unwrap_or_else(|e| e.code); + let good = refs(&ix); + // The counts lie, signed: the indexes give others. + let mut lie = r.clone(); + lie.unique_objects = 2; + lie.stored_bytes = 100; + assert_eq!(code(&resign(lie), &good), "receipt-counts"); + let mut lie = r.clone(); + lie.work_units = 4; + assert_eq!(code(&resign(lie), &good), "receipt-counts"); + // A live window's parent kind over indexes with no previous window. + let mut wrong = r.clone(); + wrong.parent = Parent::LiveWindow([4; 32]); + assert_eq!(code(&resign(wrong), &good), "receipt-parent-kind"); + // A re-pack is fine with no previous window. + let mut repack = r.clone(); + repack.parent = Parent::RePack([4; 32]); + assert_eq!(code(&resign(repack), &good), "ok"); + // And a package with no parent over a live window's indexes. + let live = indexes([9; 32]); + let mut over_live = r.clone(); + for (c, (_, b)) in over_live.components.iter_mut().zip(&live) { + c.index_digest = crate::stage_index::digest_of(b); + c.stage_root = StageIndex::parse(b).unwrap().stage_root(); + } + assert_eq!(code(&resign(over_live), &refs(&live)), "receipt-parent-kind"); + assert_eq!(code(&r, &good[..1]), "receipt-index-count"); + // A receipt over no component at all, signed: refused, whatever its parent. + for parent in [Parent::None, Parent::LiveWindow([4; 32])] { + let mut none = r.clone(); + none.components.clear(); + none.parent = parent; + none.total_entries = 0; + none.unique_objects = 0; + none.total_bytes = 0; + none.stored_bytes = 0; + none.work_units = 0; + assert_eq!(code(&resign(none), &[]), "receipt-index-count"); + } + assert_eq!(code(&r, &[good[1], good[0]]), "receipt-index-digest"); + let mut root = r.clone(); + root.components[0].stage_root = [0; 32]; + assert_eq!(code(&resign(root), &good), "receipt-stage-root"); + let mut media = r.clone(); + media.components[1].media_class = MediaClass::Video; + assert_eq!(code(&resign(media), &good), "receipt-component"); + let mut policy = r.clone(); + policy.policy_digest = [6; 32]; + assert_eq!(code(&resign(policy), &good), "receipt-policy-digest"); + let mut energy = r.clone(); + energy.joules_micro = 12; + assert_eq!(code(&resign(energy), &good), "receipt-energy"); + let mut sig = r.clone(); + sig.sig[0] ^= 1; + assert_eq!(code(&sig, &good), "receipt-signature"); + let mut field = r.clone(); + field.asset_id.push('x'); + assert_eq!(code(&field, &good), "receipt-signature"); + // An index that breaks its own rules names the index's reason. + let mut bad = StageIndex::parse(&ix[0].1).unwrap(); + bad.entries[2].flags = 0; + let bad = bad.to_bytes(); + assert_eq!(code(&r, &[&bad, good[1]]), "dup-flag"); +} + +#[test] +fn seal_refuses_what_it_cannot_sign() { + let ix = indexes([0; 32]); + let s = |ix: &[(String, Vec)], energy, grant, parent| { + PackageReceiptV2::seal(&key(1), "o", "a", [5; 32], ix, "p", energy, grant, parent).map(|_| "ok").unwrap_or_else(|e| e.code) + }; + let g = || GrantRef::unbounded("pack"); + assert_eq!(s(&ix, (0, None), g(), Parent::None), "ok"); + assert_eq!(s(&[], (0, None), g(), Parent::LiveWindow([1; 32])), "receipt-index-count"); + assert_eq!(s(&[ix[0].clone(), ("v".into(), ix[0].1.clone())], (0, None), g(), Parent::None), "receipt-component"); + assert_eq!(s(&[("w".into(), ix[0].1.clone())], (0, None), g(), Parent::None), "receipt-component"); + assert_eq!(s(&ix, (0, None), g(), Parent::LiveWindow([1; 32])), "receipt-parent-kind"); + assert_eq!(s(&ix, (5, None), g(), Parent::None), "receipt-energy"); + #[allow(deprecated)] + let legacy = s(&ix, (5, Some(EnergyProvenance::HwShunt)), g(), Parent::None); + assert_eq!(legacy, "receipt-energy", "a version-2 receipt takes no legacy label"); + let mut big = g(); + big.joule_ceiling_micro = 1 << 60; + assert_eq!(s(&ix, (0, None), big, Parent::None), "receipt-json"); + let mut wrong_policy = StageIndex::parse(&ix[0].1).unwrap(); + wrong_policy.header.policy_digest = [8; 32]; + assert_eq!(s(&[("v".into(), wrong_policy.to_bytes())], (0, None), g(), Parent::None), "receipt-policy-digest"); +} + +/// Test 18: the JSON is RFC 8785 canonical, reads back to the receipt, and a labelled +/// receipt carries its class and evidence. +#[test] +fn the_v2_json_is_canonical_and_round_trips() { + for (energy, parent) in [ + ((0, None), Parent::None), + ((25_000, Some(EnergyProvenance::ModelBased)), Parent::RePack([7; 32])), + ((25_000, Some(crate::energy_class::test_on_chip())), Parent::LiveWindow([7; 32])), + ] { + let (r, ix) = seal(energy, parent); + let j = r.to_json(); + let v: Value = serde_json::from_str(&j).unwrap(); + assert_eq!(jcs(&v).unwrap(), j, "the JSON is its own canonical form"); + assert_eq!(v["version"], 2); + assert_eq!(v["kind"], "package"); + assert_eq!(v["receipt_hash"], hex(&r.receipt_hash())); + assert_eq!(v["components"][0]["index"], "index-v.wsix"); + assert_eq!(v["grant"]["joule_ceiling_micro"], Value::Null); + assert_eq!(v["energy_provenance"].is_null(), r.energy_provenance.is_none(), "always present, null when unmetered"); + let back = PackageReceiptV2::from_json(&j).unwrap(); + assert_eq!(back, r); + back.verify_detailed(&refs(&ix)).unwrap(); + assert_eq!(AnyPackageReceipt::from_json(&j).unwrap(), AnyPackageReceipt::V2(r)); + } + // Its key order is code-unit order. + let (r, _) = seal((0, None), Parent::None); + let j = r.to_json(); + assert!(j.starts_with("{\"asset_id\":\"asset-01\",\"components\":[{\"id\":\"v\",\"index\":\"index-v.wsix\""), "{j}"); +} + +#[test] +fn the_v2_json_reader_refuses_what_it_does_not_define() { + let (r, _) = seal((0, None), Parent::None); + let j = r.to_json(); + let code = |t: &str| PackageReceiptV2::from_json(t).map(|_| "ok").unwrap_or_else(|e| e.code); + assert_eq!(code(&j), "ok"); + assert_eq!(code(&j.replacen("{", "{\"extra\":1,", 1)), "receipt-json"); + assert_eq!(code(&j.replace("\"index\":\"index-v.wsix\"", "\"index\":\"other.wsix\"")), "receipt-json"); + assert_eq!(code(&j.replace("\"parent_kind\":\"none\"", "\"parent_kind\":\"re-pack\"")), "receipt-json"); + assert_eq!(code(&j.replace("\"total_bytes\":310", "\"total_bytes\":311")), "receipt-json", "receipt_hash no longer matches"); + assert_eq!(code(&j.replace("\"version\":2", "\"version\":3")), "receipt-json"); + assert_eq!(AnyPackageReceipt::from_json(&j.replace("\"version\":2", "\"version\":3")).unwrap_err().code, "receipt-version"); + // §9.1: a member named twice, at any depth, whichever value comes first. + assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"asset-01\",\"asset_id\"", 1)), "receipt-json"); + assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"forged\",\"asset_id\"", 1)), "receipt-json"); + assert_eq!(code(&j.replacen("\"grant\":{\"capability\"", "\"grant\":{\"capability\":\"x\",\"capability\"", 1)), "receipt-json"); + assert_eq!(AnyPackageReceipt::from_json(&j.replacen("\"version\":2", "\"version\":3,\"version\":2", 1)).unwrap_err().code, "receipt-json", "before the version is read"); + // -0 is not an integer of the form. + assert_eq!(code(&j.replace("\"joules_micro\":0", "\"joules_micro\":-0")), "receipt-json"); + // energy_provenance is always present: null when unmetered, never left out. + assert_eq!(code(&j.replace("\"energy_provenance\":null,", "")), "receipt-json"); + // Evidence only as the class carries it, never null in its place. + assert_eq!(code(&j.replace("\"energy_provenance\":null,", "\"energy_provenance\":null,\"energy_uncertainty\":null,")), "receipt-json"); + let (lab, _) = seal((25_000, Some(crate::energy_class::test_on_chip())), Parent::None); + let lj = lab.to_json(); + assert_eq!(code(&lj), "ok"); + assert_eq!(code(&lj.replace(",\"energy_counter_filtering\":\"Unknown\"", "")), "receipt-json", "an on-chip counter's filtering is part of its evidence"); +} + +/// §9.3: a version-1 receipt keeps verifying when its JSON leaves the class out (it was +/// issued unmetered) or writes evidence as null, and names no member twice. +#[test] +fn the_v1_reader_reads_issued_receipts() { + use crate::wai_video_package::{segment, PackageReceipt}; + let src: Vec<(&[u8], u64)> = vec![(b"a", 2000), (b"b", 2000)]; + let r = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 0, None, GrantRef::unbounded("video.package"), None); + let j = r.to_json().unwrap(); + let code = |t: &str| PackageReceipt::from_json(t).map(|_| "ok").unwrap_or_else(|e| e.code); + assert_eq!(code(&j), "ok"); + assert_eq!(code(&j.replace("\"energy_provenance\":null,", "")), "ok"); + assert_eq!(code(&j.replace("\"energy_provenance\":null,", "\"energy_provenance\":null,\"energy_uncertainty\":null,")), "ok"); + assert_eq!(code(&j.replacen("{\"asset_id\"", "{\"asset_id\":\"x\",\"asset_id\"", 1)), "receipt-json"); +} + +/// Test 18: version 1 gets a JSON form; it keeps its signed bytes (the legacy pin) and +/// reads back to itself, labelled or not, with or without a `"version"` member. +#[test] +#[allow(deprecated)] // the legacy HwShunt receipt is the point +fn the_v1_json_round_trips_and_keeps_the_legacy_pin() { + use crate::wai_video_package::{segment, PackageReceipt}; + let src: Vec<(&[u8], u64)> = vec![(b"content-gop-0000", 2000), (b"AD-creative-0001", 2000), (b"content-gop-0002", 2000), (b"AD-creative-0001", 2000)]; + let g = GrantRef::unbounded("video.package"); + let legacy = PackageReceipt::seal(&key(1), "did:key:pkg", "asset-abc", segment(&src, 2000), 12_000, Some(EnergyProvenance::HwShunt), g.clone(), None); + assert_eq!(hex(&legacy.receipt_hash()), "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b"); + let j = legacy.to_json().unwrap(); + assert_eq!(jcs(&serde_json::from_str(&j).unwrap()).unwrap(), j); + assert!(j.contains("\"energy_provenance\":\"HwShunt\"") && j.contains("\"version\":1"), "{j}"); + let back = PackageReceipt::from_json(&j).unwrap(); + assert_eq!(back, legacy); + assert!(back.verify()); + assert_eq!(AnyPackageReceipt::from_json(&j).unwrap(), AnyPackageReceipt::V1(legacy.clone())); + // Absent "version" reads as version 1. + let unversioned = j.replace(",\"version\":1", ""); + assert_eq!(PackageReceipt::from_json(&unversioned).unwrap(), legacy); + assert!(matches!(AnyPackageReceipt::from_json(&unversioned).unwrap(), AnyPackageReceipt::V1(_))); + let unmetered = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 0, None, g.clone(), Some([3; 32])); + let j = unmetered.to_json().unwrap(); + assert!(j.contains("\"energy_provenance\":null") && j.contains("\"parent_receipt_hash\":\"0303"), "{j}"); + assert_eq!(PackageReceipt::from_json(&j).unwrap(), unmetered); + let refined = PackageReceipt::seal(&key(2), "o", "asset", segment(&src, 2000), 9_000, Some(crate::energy_class::test_on_chip()), g, None); + assert_eq!(PackageReceipt::from_json(&refined.to_json().unwrap()).unwrap(), refined); +} + +/// Test 17's library half: the version-2 receipt shows its figure under the version-1 +/// profile, only with the indexes it verifies against, and a reference meter's +/// measurement claim about it checks against that view. +#[cfg(feature = "receipt_claims")] +#[test] +fn the_v2_figure_view_and_its_measurement_claim() { + use crate::energy_figure::{meter_report, EnergyFigures}; + let (r, ix) = seal((25_000, Some(EnergyProvenance::ModelBased)), Parent::None); + let good = refs(&ix); + let view = (&r, good.as_slice()).figure_view().unwrap(); + assert_eq!(view.profile, "wai:video-package-receipt-id"); + assert_eq!(view.identity, r.receipt_hash()); + assert_eq!(view.figure(None).unwrap().joules_micro, 25_000); + assert!(crate::energy_figure::profile_name_of_domain(DOMAIN_RECEIPT_ID) == Some("wai:video-package-receipt-id")); + let none: Vec<&[u8]> = Vec::new(); + assert!((&r, none.as_slice()).figure_view().is_none(), "no view without the indexes it verifies against"); + let claim = meter_report::emit(&view, None, None, crate::energy_report::BuildRecord::of_this_build(), "model:500mW", &key(1), "did:key:pkg", None, "2026-10-06T00:00:00Z").unwrap(); + claim.check_figure_against(Some(&view)).unwrap(); + // Another key may not report the figure. + assert!(meter_report::emit(&view, None, None, crate::energy_report::BuildRecord::of_this_build(), "model", &key(2), "x", None, "2026-10-06T00:00:00Z").is_err()); + // An unmetered receipt gets no report. + let (u, uix) = seal((0, None), Parent::None); + let ug = refs(&uix); + let uview = (&u, ug.as_slice()).figure_view().unwrap(); + assert!(meter_report::emit(&uview, None, None, crate::energy_report::BuildRecord::of_this_build(), "model", &key(1), "x", None, "2026-10-06T00:00:00Z").is_err()); +} diff --git a/wai/wai-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs new file mode 100644 index 000000000..11cacae57 --- /dev/null +++ b/wai/wai-rs/src/stage_index.rs @@ -0,0 +1,871 @@ +//! The stage index, `WSIX` (`extensions/packaging.md` §6): one per component and +//! window of a package, listing every WAI object of the component in piece order with +//! its content hash, its lengths, its role and layer, the tier and decoded-output digest +//! of the prefix it completes, and the class plan. +//! +//! All integers are little-endian. The file is a 160-byte header, `n_pieces` piece +//! records of 32 bytes, `n_entries` entry records of 128 bytes, `n_classes` class records +//! of 16 bytes, the class plan (`n_classes × n_pieces` bytes, zero-padded to 8), and the +//! string table (`n_strings × (u16 len ‖ UTF-8)`, zero-padded to 8). Nothing follows it. +//! +//! ```text +//! index_digest = BLAKE3("wai:pack-stage-index\x01" ‖ file) +//! leaf_i = BLAKE3("wai:pack-stage\x01" ‖ entry_i) (its 128 bytes) +//! stage_root = world::receipt::merkle_root(leaves) (jwp-receipts §2.2's shape) +//! plan_digest = BLAKE3("wai:pack-plan\x01" ‖ u64 n ‖ n × u64 start) +//! ``` +//! +//! [`StageIndex::parse`] refuses a file that does not have this layout; +//! [`StageIndex::check`] applies every rule of §6 that the index alone decides; and +//! [`check_object`] the rules that need the object an entry names. Decoding a prefix to +//! check its digest is the packager's verifier's, which holds the decoders. + +use crate::codecs::{capability_replicate_class, ReplicateClass}; +use crate::container::{DerivedFrom, RefForm, Refines, Wai, WaiMulti, MAGIC, MAGIC_V2}; + +/// `"WSIX"`. +/// +/// cbindgen:ignore +pub const MAGIC_WSIX: &[u8; 4] = b"WSIX"; +/// The version this module reads and writes. +/// +/// cbindgen:ignore +pub const VERSION: u16 = 1; +/// The header's length. +/// +/// cbindgen:ignore +pub const HEADER_LEN: usize = 160; +/// A piece record's length. +/// +/// cbindgen:ignore +pub const PIECE_LEN: usize = 32; +/// An entry record's length. +/// +/// cbindgen:ignore +pub const ENTRY_LEN: usize = 128; +/// A class record's length. +/// +/// cbindgen:ignore +pub const CLASS_LEN: usize = 16; +/// The index digest's domain. +/// +/// cbindgen:ignore +pub const DOMAIN_INDEX: &[u8] = b"wai:pack-stage-index\x01"; +/// A stage leaf's domain. +/// +/// cbindgen:ignore +pub const DOMAIN_LEAF: &[u8] = b"wai:pack-stage\x01"; +/// The plan digest's domain (§4.2). +/// +/// cbindgen:ignore +pub const DOMAIN_PLAN: &[u8] = b"wai:pack-plan\x01"; +/// `refines` of an entry that refines nothing. +/// +/// cbindgen:ignore +pub const NO_REFINES: u32 = u32::MAX; +/// A class plan value: the piece is floor only for the class. +/// +/// cbindgen:ignore +pub const FLOOR_ONLY: u8 = 0xFF; +/// `sse` not computed. +/// +/// cbindgen:ignore +pub const SSE_UNKNOWN: u64 = u64::MAX; + +/// Piece flag: the piece's first frame follows a scene cut. +/// +/// cbindgen:ignore +pub const PIECE_SCENE_CUT: u8 = 1; +/// Piece flag: the piece was started at `max`. +/// +/// cbindgen:ignore +pub const PIECE_FORCED: u8 = 2; +/// Piece flag: the piece was started by a cue. +/// +/// cbindgen:ignore +pub const PIECE_CUE: u8 = 4; +/// Piece flag: the piece starts at a lead component's boundary (§4.5). +/// +/// cbindgen:ignore +pub const PIECE_LEAD_ALIGNED: u8 = 8; + +/// Entry flag: the object may be delivered after its piece's presentation instant. +/// +/// cbindgen:ignore +pub const ENTRY_DEFERRABLE: u8 = 1; +/// Entry flag: an earlier entry of this index has the same content hash. +/// +/// cbindgen:ignore +pub const ENTRY_DUP: u8 = 2; +/// Entry flag: the object's manifest carries `derived_from`. +/// +/// cbindgen:ignore +pub const ENTRY_DERIVED_FROM: u8 = 4; + +/// Class flag: the class receives the floor only. +/// +/// cbindgen:ignore +pub const CLASS_FLOOR_ONLY: u8 = 1; + +/// A component's media class (§6, the header's `media_class`). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MediaClass { + Video = 0, + Image = 1, + Audio = 2, + World = 3, + Feed = 4, + Avatar = 5, + Film = 6, +} + +impl MediaClass { + /// Every class, by its code. + pub fn all() -> [MediaClass; 7] { + [MediaClass::Video, MediaClass::Image, MediaClass::Audio, MediaClass::World, MediaClass::Feed, MediaClass::Avatar, MediaClass::Film] + } + + /// The class a code names. + pub fn from_code(c: u16) -> Option { + Self::all().get(c as usize).copied() + } + + /// The class's code. + pub fn code(self) -> u16 { + self as u16 + } + + /// The policy's `media` value for the class: `video`, `image`, `audio`, `world`, + /// `feed`, `avatar` or `film`. + pub fn label(self) -> &'static str { + match self { + MediaClass::Video => "video", + MediaClass::Image => "image", + MediaClass::Audio => "audio", + MediaClass::World => "world", + MediaClass::Feed => "feed", + MediaClass::Avatar => "avatar", + MediaClass::Film => "film", + } + } + + /// The class a label names. + pub fn from_label(s: &str) -> Option { + Self::all().into_iter().find(|m| m.label() == s) + } +} + +/// An entry's role. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Role { + Base = 0, + Refinement = 1, + Floor = 2, + Snapshot = 3, + Ops = 4, + Receipt = 5, + Asset = 6, +} + +impl Role { + /// The role a code names. + pub fn from_code(c: u8) -> Option { + [Role::Base, Role::Refinement, Role::Floor, Role::Snapshot, Role::Ops, Role::Receipt, Role::Asset].get(c as usize).copied() + } + + /// Its order within a piece (§6.2): base, refinements, floor, then other roles. + fn rank(self) -> u8 { + match self { + Role::Base => 0, + Role::Refinement => 1, + Role::Floor => 2, + _ => 3, + } + } +} + +/// A prefix's determinism tier as an entry states it (§6.3). +/// +/// cbindgen:ignore +pub const TIER_NONE: u8 = 0; +/// `entropy-consistency`, admitted on layer 0 only. +/// +/// cbindgen:ignore +pub const TIER_ENTROPY: u8 = 1; +/// `decode-equivalence`. +/// +/// cbindgen:ignore +pub const TIER_DECODE: u8 = 2; + +/// The tier a capability's own decode holds (§6.3): `decode-equivalence` for a +/// capability whose registry class reconstructs exactly (`StandardDefined`, and +/// `NeuralIntegerExact`, whose pin a sink verifies before decoding), `entropy-consistency` +/// for `NeuralEntropyExact`, and none otherwise. +pub fn capability_prefix_tier(cap: &str) -> u8 { + match capability_replicate_class(cap) { + Some(ReplicateClass::StandardDefined | ReplicateClass::NeuralIntegerExact) => TIER_DECODE, + Some(ReplicateClass::NeuralEntropyExact) => TIER_ENTROPY, + _ => TIER_NONE, + } +} + +/// The 160-byte header, without the counts (which are the tables' lengths). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Header { + pub media_class: MediaClass, + pub tb_num: u32, + pub tb_den: u32, + pub policy_digest: [u8; 32], + /// The previous window's index digest; zero for on-demand content and the first + /// window. + pub prev_index_digest: [u8; 32], + /// The absolute number of the window's first piece. + pub first_piece: u64, + /// The absolute ordinal of the window's first entry across the chain. + pub first_entry: u64, + pub plan_digest: [u8; 32], + /// The component's id, as a string index. + pub component: u16, +} + +/// A piece record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PieceRec { + /// Its start, in timebase units. + pub start: u64, + /// Its duration, in timebase units. + pub duration: u32, + /// Its first entry, window-relative. + pub first_entry: u32, + pub n_entries: u16, + /// 0 natural, 1 static, 2 synthetic, 3 motion_high, 4 world segment, 5 audio. + pub kind: u8, + pub flags: u8, + /// Bit `i`: rule `i` of the policy matched the piece. + pub rules: u32, + /// Bit `i`: the piece lies in moment `i` of the policy. + pub moments: u32, +} + +/// An entry record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EntryRec { + pub content_hash: [u8; 32], + /// The whole envelope's length. + pub byte_len: u64, + /// The absolute piece number. + pub piece: u64, + /// The window-relative index of the entry this one refines, or [`NO_REFINES`]. + pub refines: u32, + /// The capability, as a string index. + pub capability: u16, + pub role: Role, + pub layer: u8, + pub prefix_tier: u8, + pub flags: u8, + /// The longest a refinement may arrive after its piece's base was published and + /// still be presented (§8.6); 0 for none. + pub deadline_ms: u32, + /// The SHA-256 decoded-output digest of the prefix this entry completes (§6.4), all + /// zero unless `prefix_tier` is decode-equivalence. + pub prefix_digest: [u8; 32], + /// `Σ (source − output)²` over the prefix (§6.5); [`SSE_UNKNOWN`] when not computed. + pub sse: u64, + pub manifest_len: u32, + pub payload_len: u64, +} + +/// A class record. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ClassRec { + /// The class's id, as a string index. + pub name: u16, + pub max_layer: u8, + pub flags: u8, + /// 0 for none. + pub max_kbps: u32, +} + +/// A stage index. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct StageIndex { + pub header: Header, + pub pieces: Vec, + pub entries: Vec, + pub classes: Vec, + /// `plan[c][p]`: the highest layer class `c` lists for piece `p`, or [`FLOOR_ONLY`]. + pub plan: Vec>, + pub strings: Vec, +} + +/// Why an index, or an object against its entry, is refused (§12). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IndexReason { + pub code: &'static str, + pub detail: String, +} + +impl IndexReason { + fn new(code: &'static str, detail: impl Into) -> IndexReason { + IndexReason { code, detail: detail.into() } + } +} + +impl std::fmt::Display for IndexReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.code, self.detail) + } +} + +impl std::error::Error for IndexReason {} + +/// Lowercase hexadecimal. +fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() +} + +fn pad8(n: usize) -> usize { + n.div_ceil(8) * 8 +} + +fn u16_at(b: &[u8], i: usize) -> u16 { + u16::from_le_bytes([b[i], b[i + 1]]) +} +fn u32_at(b: &[u8], i: usize) -> u32 { + u32::from_le_bytes(b[i..i + 4].try_into().expect("four bytes")) +} +fn u64_at(b: &[u8], i: usize) -> u64 { + u64::from_le_bytes(b[i..i + 8].try_into().expect("eight bytes")) +} +fn h32(b: &[u8], i: usize) -> [u8; 32] { + b[i..i + 32].try_into().expect("32 bytes") +} + +impl EntryRec { + /// The entry's 128 bytes. + pub fn to_bytes(&self) -> [u8; ENTRY_LEN] { + let mut o = [0u8; ENTRY_LEN]; + o[0..32].copy_from_slice(&self.content_hash); + o[32..40].copy_from_slice(&self.byte_len.to_le_bytes()); + o[40..48].copy_from_slice(&self.piece.to_le_bytes()); + o[48..52].copy_from_slice(&self.refines.to_le_bytes()); + o[52..54].copy_from_slice(&self.capability.to_le_bytes()); + o[54] = self.role as u8; + o[55] = self.layer; + o[56] = self.prefix_tier; + o[57] = self.flags; + o[60..64].copy_from_slice(&self.deadline_ms.to_le_bytes()); + o[64..96].copy_from_slice(&self.prefix_digest); + o[96..104].copy_from_slice(&self.sse.to_le_bytes()); + o[104..108].copy_from_slice(&self.manifest_len.to_le_bytes()); + o[112..120].copy_from_slice(&self.payload_len.to_le_bytes()); + o + } + + fn parse(b: &[u8]) -> Result { + let role = Role::from_code(b[54]).ok_or_else(|| IndexReason::new("index-role", format!("role {}", b[54])))?; + if b[56] > TIER_DECODE { + return Err(IndexReason::new("index-tier", format!("prefix_tier {}", b[56]))); + } + Ok(EntryRec { + content_hash: h32(b, 0), + byte_len: u64_at(b, 32), + piece: u64_at(b, 40), + refines: u32_at(b, 48), + capability: u16_at(b, 52), + role, + layer: b[55], + prefix_tier: b[56], + flags: b[57], + deadline_ms: u32_at(b, 60), + prefix_digest: h32(b, 64), + sse: u64_at(b, 96), + manifest_len: u32_at(b, 104), + payload_len: u64_at(b, 112), + }) + } + + /// The entry's stage leaf, `BLAKE3("wai:pack-stage\x01" ‖ entry)`. + pub fn leaf(&self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_LEAF); + h.update(&self.to_bytes()); + *h.finalize().as_bytes() + } +} + +/// The plan digest of a list of piece starts (§4.2). +pub fn plan_digest(starts: &[u64]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_PLAN); + h.update(&(starts.len() as u64).to_le_bytes()); + for s in starts { + h.update(&s.to_le_bytes()); + } + *h.finalize().as_bytes() +} + +impl StageIndex { + /// The file. + pub fn to_bytes(&self) -> Vec { + let mut o = Vec::with_capacity(HEADER_LEN + PIECE_LEN * self.pieces.len() + ENTRY_LEN * self.entries.len()); + let h = &self.header; + o.extend_from_slice(MAGIC_WSIX); + o.extend_from_slice(&VERSION.to_le_bytes()); + o.extend_from_slice(&h.media_class.code().to_le_bytes()); + o.extend_from_slice(&h.tb_num.to_le_bytes()); + o.extend_from_slice(&h.tb_den.to_le_bytes()); + o.extend_from_slice(&(self.pieces.len() as u32).to_le_bytes()); + o.extend_from_slice(&(self.entries.len() as u32).to_le_bytes()); + o.extend_from_slice(&(self.strings.len() as u32).to_le_bytes()); + o.extend_from_slice(&(self.classes.len() as u32).to_le_bytes()); + o.extend_from_slice(&h.policy_digest); + o.extend_from_slice(&h.prev_index_digest); + o.extend_from_slice(&h.first_piece.to_le_bytes()); + o.extend_from_slice(&h.first_entry.to_le_bytes()); + o.extend_from_slice(&h.plan_digest); + o.extend_from_slice(&h.component.to_le_bytes()); + o.resize(HEADER_LEN, 0); + for p in &self.pieces { + o.extend_from_slice(&p.start.to_le_bytes()); + o.extend_from_slice(&p.duration.to_le_bytes()); + o.extend_from_slice(&p.first_entry.to_le_bytes()); + o.extend_from_slice(&p.n_entries.to_le_bytes()); + o.push(p.kind); + o.push(p.flags); + o.extend_from_slice(&p.rules.to_le_bytes()); + o.extend_from_slice(&p.moments.to_le_bytes()); + o.extend_from_slice(&[0u8; 4]); + } + for e in &self.entries { + o.extend_from_slice(&e.to_bytes()); + } + for c in &self.classes { + o.extend_from_slice(&c.name.to_le_bytes()); + o.push(c.max_layer); + o.push(c.flags); + o.extend_from_slice(&c.max_kbps.to_le_bytes()); + o.extend_from_slice(&[0u8; 8]); + } + let plan_start = o.len(); + for row in &self.plan { + o.extend_from_slice(row); + } + o.resize(plan_start + pad8(o.len() - plan_start), 0); + let strings_start = o.len(); + for s in &self.strings { + o.extend_from_slice(&(s.len() as u16).to_le_bytes()); + o.extend_from_slice(s.as_bytes()); + } + o.resize(strings_start + pad8(o.len() - strings_start), 0); + o + } + + /// Read a file (§6.2). Refused, the first that applies, in this order: + /// + /// 1. the first four bytes are not `WSIX`, a file of fewer included (`index-magic`); + /// 2. a file of fewer than six bytes (`index-truncated`), or a version other than 1 + /// (`index-version`); + /// 3. a file shorter than the header, or than the tables its counts give: the piece, + /// entry and class records, the class plan and its padding, each string's length + /// and bytes, and the string table's padding (`index-truncated`); every size is + /// taken from its count before anything is allocated; + /// 4. a reserved or padding byte that is not zero (`index-reserved`); + /// 5. a media class with no meaning (`index-media-class`); then, entry by entry, a + /// role, then a tier, with none (`index-role`, `index-tier`); + /// 6. a string that is empty or not UTF-8, then a string index past the table + /// (`index-string`); + /// 7. bytes after the string table's padding (`index-trailing`). + pub fn parse(b: &[u8]) -> Result { + let fail = |code: &'static str, detail: &str| Err(IndexReason::new(code, detail)); + if b.get(..4) != Some(MAGIC_WSIX.as_slice()) { + return fail("index-magic", "not a WSIX file"); + } + if b.len() < 6 { + return fail("index-truncated", "the header runs past the file's end"); + } + let version = u16_at(b, 4); + if version != VERSION { + return Err(IndexReason::new("index-version", format!("version {version}"))); + } + if b.len() < HEADER_LEN { + return fail("index-truncated", "the header runs past the file's end"); + } + let (n_pieces, n_entries, n_strings, n_classes) = (u32_at(b, 16) as usize, u32_at(b, 20) as usize, u32_at(b, 24) as usize, u32_at(b, 28) as usize); + // Every table's place from its count, before anything is read from it. + let size = || -> Option<(usize, usize, usize, usize, usize)> { + let at_e = HEADER_LEN.checked_add(n_pieces.checked_mul(PIECE_LEN)?)?; + let at_c = at_e.checked_add(n_entries.checked_mul(ENTRY_LEN)?)?; + let at_plan = at_c.checked_add(n_classes.checked_mul(CLASS_LEN)?)?; + let plan_len = n_classes.checked_mul(n_pieces)?; + let at_s = at_plan.checked_add(plan_len.checked_add(7)? / 8 * 8)?; + Some((at_e, at_c, at_plan, plan_len, at_s)) + }; + let Some((at_e, at_c, at_plan, plan_len, at_s)) = size().filter(|x| x.4 <= b.len()) else { + return fail("index-truncated", "a table runs past the file's end"); + }; + let mut raw: Vec<&[u8]> = Vec::with_capacity(n_strings.min(b.len() / 2)); + let mut pos = at_s; + for _ in 0..n_strings { + let Some(len) = b.get(pos..pos + 2).map(|x| u16::from_le_bytes([x[0], x[1]]) as usize) else { + return fail("index-truncated", "the string table runs past the file's end"); + }; + let Some(s) = b.get(pos + 2..pos + 2 + len) else { + return fail("index-truncated", "the string table runs past the file's end"); + }; + raw.push(s); + pos += 2 + len; + } + let end = at_s + pad8(pos - at_s); + if end > b.len() { + return fail("index-truncated", "the string table's padding runs past the file's end"); + } + let piece = |i: usize| &b[HEADER_LEN + PIECE_LEN * i..HEADER_LEN + PIECE_LEN * (i + 1)]; + let entry = |i: usize| &b[at_e + ENTRY_LEN * i..at_e + ENTRY_LEN * (i + 1)]; + let class = |i: usize| &b[at_c + CLASS_LEN * i..at_c + CLASS_LEN * (i + 1)]; + let nonzero = |x: &[u8]| x.iter().any(|&v| v != 0); + let reserved = nonzero(&b[146..HEADER_LEN]) + || nonzero(&b[at_plan + plan_len..at_s]) + || nonzero(&b[pos..end]) + || (0..n_pieces).any(|i| nonzero(&piece(i)[28..32])) + || (0..n_entries).any(|i| { let e = entry(i); nonzero(&e[58..60]) || nonzero(&e[108..112]) || nonzero(&e[120..128]) }) + || (0..n_classes).any(|i| nonzero(&class(i)[8..16])); + if reserved { + return fail("index-reserved", "a reserved or padding byte is not zero"); + } + let media_class = MediaClass::from_code(u16_at(b, 6)).ok_or_else(|| IndexReason::new("index-media-class", format!("media class {}", u16_at(b, 6))))?; + let mut entries = Vec::with_capacity(n_entries); + for i in 0..n_entries { + entries.push(EntryRec::parse(entry(i))?); + } + let mut strings = Vec::with_capacity(raw.len()); + for s in raw { + let s = std::str::from_utf8(s).ok().filter(|s| !s.is_empty()).ok_or_else(|| IndexReason::new("index-string", "a string that is empty or not UTF-8"))?; + strings.push(s.to_string()); + } + let header = Header { + media_class, + tb_num: u32_at(b, 8), + tb_den: u32_at(b, 12), + policy_digest: h32(b, 32), + prev_index_digest: h32(b, 64), + first_piece: u64_at(b, 96), + first_entry: u64_at(b, 104), + plan_digest: h32(b, 112), + component: u16_at(b, 144), + }; + let pieces = (0..n_pieces) + .map(|i| { + let p = piece(i); + PieceRec { start: u64_at(p, 0), duration: u32_at(p, 8), first_entry: u32_at(p, 12), n_entries: u16_at(p, 16), kind: p[18], flags: p[19], rules: u32_at(p, 20), moments: u32_at(p, 24) } + }) + .collect(); + let classes = (0..n_classes) + .map(|i| { + let c = class(i); + ClassRec { name: u16_at(c, 0), max_layer: c[2], flags: c[3], max_kbps: u32_at(c, 4) } + }) + .collect(); + let plan: Vec> = (0..n_classes).map(|k| b[at_plan + k * n_pieces..at_plan + (k + 1) * n_pieces].to_vec()).collect(); + let ix = StageIndex { header, pieces, entries, classes, plan, strings }; + let str_ok = |i: u16| (i as usize) < ix.strings.len(); + if !str_ok(ix.header.component) || !ix.entries.iter().all(|e| str_ok(e.capability)) || !ix.classes.iter().all(|c| str_ok(c.name)) { + return Err(IndexReason::new("index-string", "a string index past the string table")); + } + if end != b.len() { + return Err(IndexReason::new("index-trailing", format!("{} bytes follow the string table", b.len() - end))); + } + Ok(ix) + } + + /// `BLAKE3("wai:pack-stage-index\x01" ‖ file)`. + pub fn digest(&self) -> [u8; 32] { + digest_of(&self.to_bytes()) + } + + /// Every entry's stage leaf, in entry order. + pub fn leaves(&self) -> Vec<[u8; 32]> { + self.entries.iter().map(EntryRec::leaf).collect() + } + + /// The Merkle root over the leaves (jwp-receipts §2.2's shape). + pub fn stage_root(&self) -> [u8; 32] { + crate::world::receipt::merkle_root(&self.leaves()) + } + + /// The component's id. + pub fn component(&self) -> &str { + &self.strings[self.header.component as usize] + } + + /// An entry's capability. + pub fn capability(&self, e: &EntryRec) -> &str { + &self.strings[e.capability as usize] + } + + /// A class's id. + pub fn class_name(&self, c: &ClassRec) -> &str { + &self.strings[c.name as usize] + } + + /// The entries of piece `i` of the window. + pub fn piece_entries(&self, i: usize) -> &[EntryRec] { + let p = &self.pieces[i]; + let from = p.first_entry as usize; + &self.entries[from..from + p.n_entries as usize] + } + + /// The highest layer piece `i` has. + pub fn piece_max_layer(&self, i: usize) -> u8 { + self.piece_entries(i).iter().map(|e| e.layer).max().unwrap_or(0) + } + + /// Every rule of §6.3 the index alone decides, in §6.3's order; the first that fails + /// is the reason: + /// + /// 1. a timebase of zero (`index-timebase`); with no previous window, a `first_piece` + /// or `first_entry` other than 0, or a first piece that does not start at 0 + /// (`window-start`); then piece by piece: a piece that lasts no time or does not + /// start where the one before ended (`piece-order`), whose `first_entry` is not the + /// number of entries before it or that has none (`piece-entries`), of a kind or with + /// a flag that has no meaning (`piece-kind`); then pieces whose entries are not the + /// index's (`piece-entries`); + /// 2. entry by entry: an entry that does not name its piece's absolute number + /// (`piece-entries`); a flag with no meaning (`entry-flags`); out of the order base, + /// refinement, floor, others, or a refinement below the one before it + /// (`entry-order`); an entry that is not a refinement above layer 0, a refinement at + /// layer 0, or `refines` on any entry but a refinement (`layer-role`); for a + /// refinement, `refines` outside the window (`refines-outside-window`), not an + /// earlier entry of its piece (`refines-mismatch`), not one layer below + /// (`layer-gap`), entropy-consistency stated (`tier-entropy-passthrough`), + /// decode-equivalence over a prefix that does not hold it (`tier-weakest`); a tier + /// other than §6.4's (`tier-mismatch`); a non-zero prefix digest without + /// decode-equivalence (`digest-without-tier`); a deadline on an entry that is not a + /// refinement (`deadline-role`); a floor without `derived_from` + /// (`floor-derived-from`); a content hash an earlier entry has at another length + /// (`dup-length`); the duplicate flag other than set exactly when an earlier entry + /// has the content hash (`dup-flag`); + /// 3. class by class: a flag with no meaning (`class-flags`); a plan value other than + /// [`FLOOR_ONLY`] for a floor-only class, or above the class's `max_layer` or the + /// piece's highest layer (`class-plan`). (A plan of another shape than one row per + /// class and one value per piece, which no parsed file has, is `class-plan` too.) + pub fn check(&self) -> Result<(), IndexReason> { + let r = |code: &'static str, detail: String| Err(IndexReason::new(code, detail)); + let h = &self.header; + if h.tb_num == 0 || h.tb_den == 0 { + return r("index-timebase", "a timebase of zero".into()); + } + // An index with no previous window is on-demand content or a chain's first + // window: it starts the presentation timeline (§4.5) and the chain. + if h.prev_index_digest == [0u8; 32] && (h.first_piece != 0 || h.first_entry != 0 || self.pieces.first().is_some_and(|p| p.start != 0)) { + return r("window-start", format!("first_piece {}, first_entry {}, a first piece that does not start at 0", h.first_piece, h.first_entry)); + } + let mut next_entry = 0u64; + for (i, p) in self.pieces.iter().enumerate() { + if p.duration == 0 { + return r("piece-order", format!("piece {i} lasts no time")); + } + if i > 0 { + let q = &self.pieces[i - 1]; + if q.start.checked_add(q.duration as u64) != Some(p.start) { + return r("piece-order", format!("piece {i} does not start where piece {} ended", i - 1)); + } + } + if p.first_entry as u64 != next_entry || p.n_entries == 0 { + return r("piece-entries", format!("piece {i}'s entries do not follow the previous piece's")); + } + next_entry += p.n_entries as u64; + if p.kind > 5 || p.flags & !0x0F != 0 { + return r("piece-kind", format!("piece {i}: kind {} flags {:#x}", p.kind, p.flags)); + } + } + if next_entry != self.entries.len() as u64 { + return r("piece-entries", format!("the pieces hold {next_entry} entries, the index {}", self.entries.len())); + } + let mut seen: std::collections::BTreeMap<[u8; 32], u64> = std::collections::BTreeMap::new(); + for (pi, p) in self.pieces.iter().enumerate() { + let piece_no = h.first_piece.checked_add(pi as u64); + let from = p.first_entry as usize; + let to = from + p.n_entries as usize; + for k in from..to { + let e = &self.entries[k]; + if Some(e.piece) != piece_no { + return r("piece-entries", format!("entry {k} names piece {}, its piece is {pi}", e.piece)); + } + if e.flags & !(ENTRY_DEFERRABLE | ENTRY_DUP | ENTRY_DERIVED_FROM) != 0 { + return r("entry-flags", format!("entry {k}: flags {:#x}", e.flags)); + } + if k > from { + let q = &self.entries[k - 1]; + let out_of_order = e.role.rank() < q.role.rank() + || (e.role == Role::Refinement && q.role == Role::Refinement && e.layer < q.layer); + if out_of_order { + return r("entry-order", format!("entry {k} comes after entry {} in its piece", k - 1)); + } + } + let layered = e.role == Role::Refinement; + if (layered && e.layer == 0) || (!layered && e.layer != 0) || (layered == (e.refines == NO_REFINES)) { + return r("layer-role", format!("entry {k}: role {:?} at layer {} with refines {}", e.role, e.layer, e.refines)); + } + let cap = self.capability(e); + let tier = if layered { + let t = e.refines as usize; + if t >= self.entries.len() { + return r("refines-outside-window", format!("entry {k} refines entry {t}, past the window's {}", self.entries.len())); + } + if t < from || t >= k { + return r("refines-mismatch", format!("entry {k} refines entry {t}, not an earlier entry of its piece")); + } + let below = &self.entries[t]; + if below.layer.checked_add(1) != Some(e.layer) { + return r("layer-gap", format!("entry {k} at layer {} refines entry {t} at layer {}", e.layer, below.layer)); + } + if e.prefix_tier == TIER_ENTROPY { + return r("tier-entropy-passthrough", format!("entry {k} at layer {} states entropy-consistency", e.layer)); + } + let composed = if below.prefix_tier == TIER_DECODE && capability_prefix_tier(cap) == TIER_DECODE { TIER_DECODE } else { TIER_NONE }; + if e.prefix_tier == TIER_DECODE && composed != TIER_DECODE { + return r("tier-weakest", format!("entry {k} states decode-equivalence over a prefix that does not hold it")); + } + composed + } else if e.role == Role::Floor { + TIER_NONE + } else { + capability_prefix_tier(cap) + }; + if e.prefix_tier != tier { + return r("tier-mismatch", format!("entry {k} ({cap}) states tier {}, §6.3 gives {tier}", e.prefix_tier)); + } + if e.prefix_tier != TIER_DECODE && e.prefix_digest != [0u8; 32] { + return r("digest-without-tier", format!("entry {k} states a prefix digest without decode-equivalence")); + } + if !layered && e.deadline_ms != 0 { + return r("deadline-role", format!("entry {k}, not a refinement, states a deadline")); + } + if e.role == Role::Floor && e.flags & ENTRY_DERIVED_FROM == 0 { + return r("floor-derived-from", format!("entry {k}, a floor, does not carry derived_from")); + } + let dup = match seen.get(&e.content_hash) { + Some(&len) if len != e.byte_len => return r("dup-length", format!("entry {k} repeats a content hash at another length")), + Some(_) => true, + None => { + seen.insert(e.content_hash, e.byte_len); + false + } + }; + if dup != (e.flags & ENTRY_DUP != 0) { + return r("dup-flag", format!("entry {k}'s duplicate flag says {}, the index {dup}", e.flags & ENTRY_DUP != 0)); + } + } + } + if self.plan.len() != self.classes.len() { + return r("class-plan", format!("{} plan rows for {} classes", self.plan.len(), self.classes.len())); + } + for (ci, (c, row)) in self.classes.iter().zip(&self.plan).enumerate() { + if c.flags & !CLASS_FLOOR_ONLY != 0 { + return r("class-flags", format!("class {ci}: flags {:#x}", c.flags)); + } + if row.len() != self.pieces.len() { + return r("class-plan", format!("class {ci}'s plan has {} values for {} pieces", row.len(), self.pieces.len())); + } + for (pi, &v) in row.iter().enumerate() { + let ok = if c.flags & CLASS_FLOOR_ONLY != 0 { v == FLOOR_ONLY } else { v == FLOOR_ONLY || (v <= c.max_layer && v <= self.piece_max_layer(pi)) }; + if !ok { + return r("class-plan", format!("class {ci} lists layer {v} for piece {pi}")); + } + } + } + Ok(()) + } +} + +/// `BLAKE3("wai:pack-stage-index\x01" ‖ file)` of a file's bytes. +pub fn digest_of(file: &[u8]) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + h.update(DOMAIN_INDEX); + h.update(file); + *h.finalize().as_bytes() +} + +/// The rules of §5 and §6 that need the object entry `k` names, `bytes`, in an index +/// that has passed [`StageIndex::check`]: the bytes hash to the entry's content hash +/// (`object-hash`) at its length (`object-length`); they are one `WAI1` or `WAI2` envelope +/// and nothing else (`non-envelope-entry`) whose manifest and payload lengths are the +/// entry's (`object-lengths`) and whose capability is the entry's (`object-capability`); +/// a refinement's `refines` (staged-delivery §3) names its piece's base as `base`, the +/// entry's layer as `layer`, and as producers only objects of the layers below it, +/// among them the one its entry refines (`object-refines`); and `derived_from` is present, +/// well formed, exactly when the entry says so (`object-derived-from`). +pub fn check_object(ix: &StageIndex, k: usize, bytes: &[u8]) -> Result<(), IndexReason> { + let r = |code: &'static str, detail: String| Err(IndexReason::new(code, detail)); + let e = ix.entries.get(k).ok_or_else(|| IndexReason::new("object-hash", format!("no entry {k}")))?; + if bytes.len() as u64 != e.byte_len { + return r("object-length", format!("{} bytes, the entry says {}", bytes.len(), e.byte_len)); + } + if crate::jwp_hash::content_hash(bytes) != e.content_hash { + return r("object-hash", "the bytes do not hash to the entry's content hash".into()); + } + let man_len = bytes.get(4..8).map(|b| u32::from_le_bytes(b.try_into().expect("four bytes"))); + let (cap, refines, derived, payload_len) = match bytes.get(..4) { + Some(m) if m == MAGIC => { + let w = Wai::from_bytes(bytes).map_err(|x| IndexReason::new("non-envelope-entry", x.to_string()))?; + if 12 + man_len.unwrap_or(0) as u64 + w.payload.len() as u64 != e.byte_len { + return r("non-envelope-entry", "bytes follow the envelope".into()); + } + (w.manifest.model_requirement.capability.clone(), w.manifest.refines.clone(), w.manifest.derived_from.clone(), w.payload.len() as u64) + } + Some(m) if m == MAGIC_V2 => { + let w = WaiMulti::from_bytes(bytes).map_err(|x| IndexReason::new("non-envelope-entry", x.to_string()))?; + let payload: u64 = w.renditions.iter().map(|p| p.len() as u64).sum(); + if 8 + man_len.unwrap_or(0) as u64 + 2 + 8 * w.renditions.len() as u64 + payload != e.byte_len { + return r("non-envelope-entry", "bytes follow the envelope".into()); + } + let first = w.manifest.renditions.first().map(|m| m.capability.clone()).unwrap_or_default(); + // A WAI2 carries derived_from on its entries: present when any entry carries + // it, and well formed only when every one that does is. + let derived: Vec<&serde_json::Value> = w.manifest.renditions.iter().filter_map(|m| m.derived_from.as_ref()).collect(); + let derived = match derived.as_slice() { + [] => None, + [one, ..] if derived.iter().all(|v| DerivedFrom::from_value(v).is_ok()) => Some((*one).clone()), + _ => Some(serde_json::Value::Null), + }; + (first, None, derived, payload) + } + _ => return r("non-envelope-entry", "neither a WAI1 nor a WAI2 envelope".into()), + }; + if man_len != Some(e.manifest_len) || payload_len != e.payload_len { + return r("object-lengths", format!("manifest {:?} payload {payload_len}, the entry says {} and {}", man_len, e.manifest_len, e.payload_len)); + } + if cap != ix.capability(e) { + return r("object-capability", format!("the object's capability is {cap}, the entry's {}", ix.capability(e))); + } + if e.role == Role::Refinement { + // The chain below: the entry it refines, down to its piece's base. + let mut chain: Vec = Vec::new(); + let mut at = e.refines; + while let Some(x) = ix.entries.get(at as usize) { + chain.push(hex(&x.content_hash)); + if x.role != Role::Refinement || chain.len() > 256 { + break; + } + at = x.refines; + } + let base = chain.last().cloned().unwrap_or_default(); + let rf = refines.as_ref().and_then(|v| Refines::from_value(v, RefForm::ContentHash).ok()); + let ok = rf.is_some_and(|rf| { + rf.layer == e.layer && rf.base == base && rf.producers().all(|p| chain.iter().any(|c| c == p)) && rf.producers().any(|p| Some(p) == chain.first().map(String::as_str)) + }); + if !ok { + return r("object-refines", "the object's refines does not name its piece's base, its layer and the layers below".into()); + } + } + let has = derived.is_some(); + if has != (e.flags & ENTRY_DERIVED_FROM != 0) || derived.as_ref().is_some_and(|v| DerivedFrom::from_value(v).is_err()) { + // (`null` above marks a WAI2 entry whose derived_from is malformed.) + return r("object-derived-from", format!("the object {} derived_from, the entry says {}", if has { "carries" } else { "lacks" }, e.flags & ENTRY_DERIVED_FROM != 0)); + } + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/wai/wai-rs/src/stage_index/tests.rs b/wai/wai-rs/src/stage_index/tests.rs new file mode 100644 index 000000000..9f293a4d7 --- /dev/null +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -0,0 +1,281 @@ +use super::*; +use crate::container::{Conditioning, Manifest, ModelRequirement, Prev}; + +fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, payload: &[u8]) -> Vec { + let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x")); + if let Some((base, prev, layer)) = refines { + m = m.with_refines(&Refines { base: hex(base), prev: Prev::One(hex(prev)), layer, span: None }); + } + if derived { + m = m.with_derived_from(&DerivedFrom { motif: "ab".repeat(32), motif_capability: "wai.video.int_motion".into(), renderer: "r@1".into(), settings: "cd".repeat(32) }); + } + Wai::new(m, payload.to_vec()).to_bytes().unwrap() +} + +/// An entry describing `bytes`. +#[allow(clippy::too_many_arguments)] +fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32, tier: u8, flags: u8) -> EntryRec { + let man = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + EntryRec { + content_hash: crate::jwp_hash::content_hash(bytes), + byte_len: bytes.len() as u64, + piece, + refines, + capability: cap, + role, + layer, + prefix_tier: tier, + flags, + deadline_ms: if role == Role::Refinement { 1500 } else { 0 }, + prefix_digest: if tier == TIER_DECODE { [7u8; 32] } else { [0u8; 32] }, + sse: SSE_UNKNOWN, + manifest_len: man, + payload_len: bytes.len() as u64 - 12 - man as u64, + } +} + +/// Two pieces: the first a base, two refinements and a floor; the second the same base +/// again (a duplicate) and a floor. One class, `tv`, to layer 2 then 0. +fn fixture() -> (StageIndex, Vec>) { + let base = env("wai.video.int_motion", None, false, b"base"); + let bh = crate::jwp_hash::content_hash(&base); + let r1 = env("wai.video.int_refine", Some((&bh, &bh, 1)), false, b"r1"); + let r1h = crate::jwp_hash::content_hash(&r1); + let r2 = env("wai.video.int_refine", Some((&bh, &r1h, 2)), false, b"r2"); + let f0 = env("wai.video.av1", None, true, b"floor-0"); + let f1 = env("wai.video.av1", None, true, b"floor-1"); + let objects = vec![base.clone(), r1.clone(), r2.clone(), f0.clone(), base.clone(), f1.clone()]; + let entries = vec![ + entry(&base, 0, 2, Role::Base, 0, NO_REFINES, TIER_DECODE, 0), + entry(&r1, 0, 3, Role::Refinement, 1, 0, TIER_DECODE, 0), + entry(&r2, 0, 3, Role::Refinement, 2, 1, TIER_DECODE, 0), + entry(&f0, 0, 4, Role::Floor, 0, NO_REFINES, TIER_NONE, ENTRY_DERIVED_FROM), + entry(&base, 1, 2, Role::Base, 0, NO_REFINES, TIER_DECODE, ENTRY_DUP), + entry(&f1, 1, 4, Role::Floor, 0, NO_REFINES, TIER_NONE, ENTRY_DERIVED_FROM), + ]; + let ix = StageIndex { + header: Header { + media_class: MediaClass::Video, + tb_num: 25, + tb_den: 1, + policy_digest: [1u8; 32], + prev_index_digest: [0u8; 32], + first_piece: 0, + first_entry: 0, + plan_digest: plan_digest(&[0, 50]), + component: 0, + }, + pieces: vec![ + PieceRec { start: 0, duration: 50, first_entry: 0, n_entries: 4, kind: 0, flags: 0, rules: 0, moments: 0 }, + PieceRec { start: 50, duration: 25, first_entry: 4, n_entries: 2, kind: 1, flags: PIECE_SCENE_CUT, rules: 1, moments: 0 }, + ], + entries, + classes: vec![ClassRec { name: 1, max_layer: 2, flags: 0, max_kbps: 0 }], + plan: vec![vec![2, 0]], + strings: vec!["v".into(), "tv".into(), "wai.video.int_motion".into(), "wai.video.int_refine".into(), "wai.video.av1".into()], + }; + (ix, objects) +} + +fn code(ix: &StageIndex) -> &'static str { + match ix.check() { + Ok(()) => "ok", + Err(r) => r.code, + } +} + +#[test] +fn an_index_round_trips_and_checks() { + let (ix, objects) = fixture(); + let b = ix.to_bytes(); + assert_eq!(b.len() % 8, 0); + assert_eq!(b.len(), HEADER_LEN + 2 * PIECE_LEN + 6 * ENTRY_LEN + CLASS_LEN + 8 + 72); + let back = StageIndex::parse(&b).unwrap(); + assert_eq!(back, ix); + assert_eq!(back.to_bytes(), b); + assert_eq!(code(&ix), "ok"); + assert_eq!(ix.digest(), digest_of(&b)); + assert_eq!(ix.leaves().len(), 6); + assert_eq!(ix.stage_root(), crate::world::receipt::merkle_root(&ix.leaves())); + for (k, o) in objects.iter().enumerate() { + check_object(&ix, k, o).unwrap_or_else(|r| panic!("entry {k}: {r}")); + } +} + +#[test] +fn the_plan_digest_is_over_the_starts() { + let mut want = blake3::Hasher::new(); + want.update(b"wai:pack-plan\x01"); + want.update(&2u64.to_le_bytes()); + want.update(&0u64.to_le_bytes()); + want.update(&50u64.to_le_bytes()); + assert_eq!(plan_digest(&[0, 50]), *want.finalize().as_bytes()); + assert_ne!(plan_digest(&[0, 50]), plan_digest(&[0, 51])); +} + +/// Test 6 (the index rules): each mutation is refused with its named reason. +#[test] +fn every_index_mutation_names_its_reason() { + type Mutation = fn(&mut StageIndex); + let cases: &[(&str, Mutation)] = &[ + // Layer 2 refines the floor, a later entry of its piece. + ("refines-mismatch", |ix| ix.entries[2].refines = 3), + // Layer 2 refines the base: a layer skipped. + ("layer-gap", |ix| ix.entries[2].refines = 0), + ("refines-outside-window", |ix| ix.entries[2].refines = 99), + ("tier-entropy-passthrough", |ix| { + ix.strings[2] = "wai.image.jpegai".into(); + ix.entries[0].prefix_tier = TIER_ENTROPY; + ix.entries[0].prefix_digest = [0; 32]; + ix.entries[4].prefix_tier = TIER_ENTROPY; + ix.entries[4].prefix_digest = [0; 32]; + ix.entries[1].prefix_tier = TIER_ENTROPY; + ix.entries[1].prefix_digest = [0; 32]; + }), + ("tier-weakest", |ix| { + ix.strings[2] = "wai.image.jpegai".into(); + ix.entries[0].prefix_tier = TIER_ENTROPY; + ix.entries[0].prefix_digest = [0; 32]; + ix.entries[4].prefix_tier = TIER_ENTROPY; + ix.entries[4].prefix_digest = [0; 32]; + }), + ("tier-mismatch", |ix| { + ix.entries[3].prefix_tier = TIER_DECODE; + ix.entries[3].prefix_digest = [7; 32]; + }), + ("digest-without-tier", |ix| ix.entries[3].prefix_digest = [9; 32]), + ("dup-flag", |ix| ix.entries[4].flags = 0), + ("dup-flag", |ix| ix.entries[5].flags |= ENTRY_DUP), + ("dup-length", |ix| ix.entries[4].byte_len += 1), + ("layer-role", |ix| ix.entries[3].layer = 1), + ("layer-role", |ix| ix.entries[0].refines = 1), + ("entry-order", |ix| ix.entries.swap(2, 3)), + ("deadline-role", |ix| ix.entries[0].deadline_ms = 5), + ("floor-derived-from", |ix| ix.entries[3].flags = 0), + ("entry-flags", |ix| ix.entries[0].flags |= 0x80), + ("piece-order", |ix| ix.pieces[1].start = 51), + ("piece-order", |ix| ix.pieces[0].duration = 0), + ("piece-entries", |ix| ix.pieces[1].first_entry = 3), + ("piece-entries", |ix| ix.entries[5].piece = 0), + ("piece-kind", |ix| ix.pieces[0].kind = 9), + ("class-plan", |ix| ix.plan[0][1] = 1), + ("class-plan", |ix| ix.plan[0][0] = 3), + ("class-plan", |ix| { + ix.classes[0].flags = CLASS_FLOOR_ONLY; + }), + ("class-flags", |ix| ix.classes[0].flags = 2), + ("index-timebase", |ix| ix.header.tb_den = 0), + // An on-demand index (no previous window) opens at piece 0, entry 0, time 0. + ("window-start", |ix| ix.header.first_piece = 1), + ("window-start", |ix| ix.header.first_entry = 1), + ("window-start", |ix| { + for p in ix.pieces.iter_mut() { + p.start += 50; + } + }), + ]; + for (want, m) in cases { + let (mut ix, _) = fixture(); + m(&mut ix); + // Through the bytes, as a verifier reads the file. + let back = StageIndex::parse(&ix.to_bytes()).unwrap_or_else(|r| panic!("{want}: the mutation does not parse: {r}")); + assert_eq!(code(&back), *want, "{want}"); + } +} + +/// A file that is not an index's layout is refused before any rule is applied. +#[test] +fn a_malformed_file_is_refused_with_its_reason() { + let (ix, _) = fixture(); + let b = ix.to_bytes(); + // Every strict prefix is refused: one too short to hold the magic has none; any + // other is truncated (the header, a table, a padding). + for n in 0..b.len() { + let r = StageIndex::parse(&b[..n]).unwrap_err(); + assert_eq!(r.code, if n < 4 { "index-magic" } else { "index-truncated" }, "prefix {n}: {r}"); + } + // The refusals come in §6.2's order: a reserved byte before a media class, a media + // class before a role. + let mut two = b.clone(); + two[6] = 7; + two[150] = 1; + assert_eq!(StageIndex::parse(&two).unwrap_err().code, "index-reserved"); + let mut two = b.clone(); + two[6] = 7; + two[HEADER_LEN + 2 * PIECE_LEN + 54] = 7; + assert_eq!(StageIndex::parse(&two).unwrap_err().code, "index-media-class"); + let mut t = b.clone(); + t.extend_from_slice(&[0u8; 8]); + assert_eq!(StageIndex::parse(&t).unwrap_err().code, "index-trailing"); + let mutate = |at: usize, v: u8| { + let mut m = b.clone(); + m[at] = v; + StageIndex::parse(&m).map(|_| "ok").unwrap_or_else(|r| r.code) + }; + assert_eq!(mutate(0, b'X'), "index-magic"); + assert_eq!(mutate(4, 2), "index-version"); + assert_eq!(mutate(6, 7), "index-media-class"); + assert_eq!(mutate(150, 1), "index-reserved"); + assert_eq!(mutate(HEADER_LEN + 28, 1), "index-reserved"); + let e0 = HEADER_LEN + 2 * PIECE_LEN; + assert_eq!(mutate(e0 + 54, 7), "index-role"); + assert_eq!(mutate(e0 + 56, 3), "index-tier"); + assert_eq!(mutate(e0 + 58, 1), "index-reserved"); + assert_eq!(mutate(e0 + 52, 40), "index-string"); + assert_eq!(mutate(144, 9), "index-string"); + // A count the file cannot hold is refused before anything is sized from it. + let mut big = b.clone(); + big[20..24].copy_from_slice(&u32::MAX.to_le_bytes()); + assert_eq!(StageIndex::parse(&big).unwrap_err().code, "index-truncated"); +} + +/// Test 6 (the object rules): an entry naming a file that is not a WAI envelope (a +/// fragmented MP4 file), and an object that is not what its entry says. +#[test] +fn every_object_mutation_names_its_reason() { + let (ix, objects) = fixture(); + let cmaf = b"\x00\x00\x00\x18ftypcmfc\x00\x00\x00\x00cmfciso6".to_vec(); + let r = |k: usize, bytes: &[u8], f: &dyn Fn(&mut StageIndex)| { + let mut ix = ix.clone(); + f(&mut ix); + check_object(&ix, k, bytes).map(|_| "ok").unwrap_or_else(|r| r.code) + }; + assert_eq!(r(3, &cmaf, &|ix| { + ix.entries[3].content_hash = crate::jwp_hash::content_hash(&cmaf); + ix.entries[3].byte_len = cmaf.len() as u64; + }), "non-envelope-entry"); + assert_eq!(r(0, &objects[0], &|_| {}), "ok"); + assert_eq!(r(0, &objects[1], &|_| {}), "object-length"); + let mut flipped = objects[0].clone(); + *flipped.last_mut().unwrap() ^= 1; + assert_eq!(r(0, &flipped, &|_| {}), "object-hash"); + let mut trailing = objects[0].clone(); + trailing.push(0); + assert_eq!(r(0, &trailing, &|ix| { + ix.entries[0].byte_len += 1; + ix.entries[0].content_hash = crate::jwp_hash::content_hash(&trailing); + }), "non-envelope-entry"); + assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].payload_len += 1), "object-lengths"); + assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].capability = 4), "object-capability"); + // Layer 2's refines names the base and layer 1; read under layer 1's entry, its + // producer is not the entry below. + assert_eq!(r(2, &objects[2], &|ix| ix.entries[2].refines = 0), "object-refines"); + assert_eq!(r(1, &objects[2], &|ix| { + ix.entries[1].content_hash = ix.entries[2].content_hash; + ix.entries[1].byte_len = ix.entries[2].byte_len; + ix.entries[1].manifest_len = ix.entries[2].manifest_len; + ix.entries[1].payload_len = ix.entries[2].payload_len; + }), "object-refines"); + assert_eq!(r(3, &objects[3], &|ix| ix.entries[3].flags = 0), "object-derived-from"); + assert_eq!(r(0, &objects[0], &|ix| ix.entries[0].flags = ENTRY_DERIVED_FROM), "object-derived-from"); +} + +#[test] +fn a_capability_s_tier_is_its_registry_class_s() { + assert_eq!(capability_prefix_tier("wai.video.int_motion"), TIER_DECODE); + assert_eq!(capability_prefix_tier("wai.image.png"), TIER_DECODE); + assert_eq!(capability_prefix_tier("wai.video.int_refine"), TIER_DECODE); + assert_eq!(capability_prefix_tier("wai.image.jpegai"), TIER_ENTROPY); + assert_eq!(capability_prefix_tier("wai.video.av1"), TIER_NONE); + assert_eq!(capability_prefix_tier("wai.video.nope"), TIER_NONE); +} diff --git a/wai/wai-rs/src/wai_video_package.rs b/wai/wai-rs/src/wai_video_package.rs index ae2a07e91..bf2a13879 100644 --- a/wai/wai-rs/src/wai_video_package.rs +++ b/wai/wai-rs/src/wai_video_package.rs @@ -81,7 +81,7 @@ pub fn segment(gops: &[Gop], target_ms: u64) -> Vec { let mut cur: Vec = Vec::new(); let mut dur = 0u64; let mut bytes = 0u64; - let mut close = |cur: &mut Vec, dur: &mut u64, bytes: &mut u64, out: &mut Vec| { + let close = |cur: &mut Vec, dur: &mut u64, bytes: &mut u64, out: &mut Vec| { if !cur.is_empty() { out.push(SegmentEntry { index: out.len() as u64,