From 86b9fd7dc91269e1a3b8a6dcab7b0f93752a4819 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 05:35:41 -0400 Subject: [PATCH 01/28] wai-rs: the stage index (WSIX), the version-2 package receipt with JSON, v1 JSON, the V2 figure view, and derived_from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - stage_index (video_package): the WSIX layout, parse, digest, stage leaves and root, the plan digest, every rule the index alone decides (piece order and entries, role and layer, refines, the tier composition of derived-tracks §3, the duplicate flag, the class plan), and the object rules (non-envelope-entry, lengths, capability, refines, derived_from). - package (video_package): PackageReceiptV2 under wai:pack-receipt, sealed over the indexes with counts every verifier recomputes; verify_detailed with named reasons; RFC 8785 JSON both ways; v1 PackageReceipt gains the same JSON form (the legacy pin unchanged); AnyPackageReceipt dispatches on version. - energy_figure: the V2 receipt's figure under wai:video-package-receipt-id, viewed only with the indexes it verifies against. - container: derived_from on a WAI1 manifest and a WAI2 entry, kept as JSON like refines, with a typed reader. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/Cargo.toml | 6 +- wai/wai-rs/include/wai.h | 102 ++++ wai/wai-rs/src/container.rs | 127 +++++ wai/wai-rs/src/energy_figure.rs | 13 + wai/wai-rs/src/lib.rs | 10 + wai/wai-rs/src/package.rs | 697 ++++++++++++++++++++++++ wai/wai-rs/src/package/tests.rs | 273 ++++++++++ wai/wai-rs/src/stage_index.rs | 791 ++++++++++++++++++++++++++++ wai/wai-rs/src/stage_index/tests.rs | 256 +++++++++ 9 files changed, 2273 insertions(+), 2 deletions(-) create mode 100644 wai/wai-rs/src/package.rs create mode 100644 wai/wai-rs/src/package/tests.rs create mode 100644 wai/wai-rs/src/stage_index.rs create mode 100644 wai/wai-rs/src/stage_index/tests.rs 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 57e905f88..eadcef21f 100644 --- a/wai/wai-rs/include/wai.h +++ b/wai/wai-rs/include/wai.h @@ -344,11 +344,111 @@ */ #define TRITS_PER_BYTE 5 +/** + * The version this module reads and writes. + */ +#define VERSION 1 + +/** + * The header's length. + */ +#define HEADER_LEN 160 + +/** + * A piece record's length. + */ +#define PIECE_LEN 32 + +/** + * An entry record's length. + */ +#define ENTRY_LEN 128 + +/** + * A class record's length. + */ +#define CLASS_LEN 16 + +/** + * `refines` of an entry that refines nothing. + */ +#define NO_REFINES UINT32_MAX + +/** + * A class plan value: the piece is floor only for the class. + */ +#define FLOOR_ONLY 255 + +/** + * `sse` not computed. + */ +#define SSE_UNKNOWN UINT64_MAX + +/** + * Piece flag: the piece's first frame follows a scene cut. + */ +#define PIECE_SCENE_CUT 1 + +/** + * Piece flag: the piece was started at `max`. + */ +#define PIECE_FORCED 2 + +/** + * Piece flag: the piece was started by a cue. + */ +#define PIECE_CUE 4 + +/** + * Piece flag: the piece starts at a lead component's boundary (§4.5). + */ +#define PIECE_LEAD_ALIGNED 8 + +/** + * Entry flag: the object may be delivered after its piece's presentation instant. + */ +#define ENTRY_DEFERRABLE 1 + +/** + * Entry flag: an earlier entry of this index has the same content hash. + */ +#define ENTRY_DUP 2 + +/** + * Entry flag: the object's manifest carries `derived_from`. + */ +#define ENTRY_DERIVED_FROM 4 + +/** + * Class flag: the class receives the floor only. + */ +#define CLASS_FLOOR_ONLY 1 + +/** + * A prefix's determinism tier as an entry states it (§6.3). + */ +#define TIER_NONE 0 + +/** + * `entropy-consistency`, admitted on layer 0 only. + */ +#define TIER_ENTROPY 1 + +/** + * `decode-equivalence`. + */ +#define TIER_DECODE 2 + /** * Q32.32 fixed-point value. */ typedef struct Fx Fx; +/** + * A component's media class (§6, the header's `media_class`). + */ +typedef struct MediaClass MediaClass; + typedef struct wai_buffer_t { uint8_t *data; uintptr_t len; @@ -404,6 +504,8 @@ typedef struct wai_decoded_t { + + #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index aeb5d2352..0d2ce7004 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"); + 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..5e47f4544 --- /dev/null +++ b/wai/wai-rs/src/package.rs @@ -0,0 +1,697 @@ +//! 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}; +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. +pub const DOMAIN_RECEIPT_V2: &[u8] = b"wai:pack-receipt\x01"; +/// The identity domain both versions share. +pub const DOMAIN_RECEIPT_ID: &[u8] = b"wai:video-package-receipt-id\x01"; +/// The `kind` member of a package receipt's JSON. +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> { + use crate::energy_class::{JSON_CALIBRATION_REF, JSON_CLASS, JSON_FILTERING, JSON_UNCERTAINTY}; + 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(()) +} + +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 { + 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. one index per component (`receipt-index-count`); 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 indexes.len() != self.components.len() { + return err("receipt-index-count", format!("{} indexes for {} components", 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, + "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 adds its fields (`energy_provenance` and its evidence); an unmetered receipt + /// has none. + 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. Refused (`receipt-json`): JSON that does not parse, + /// a member this version does not define or one it requires missing, a value of the + /// wrong form, 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. + pub fn from_json(text: &str) -> Result { + let bad = |w: String| PackageReason::new("receipt-json", w); + let v: Value = serde_json::from_str(text).map_err(|e| bad(e.to_string()))?; + let o = v.as_object().ok_or_else(|| bad("not an object".into()))?; + exact_members( + o, + &[ + "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", + ], + ) + .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:?}")))?; + 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; 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: Value = serde_json::from_str(text).map_err(|e| bad(e.to_string()))?; + 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: Value = serde_json::from_str(text).map_err(|e| PackageReason::new("receipt-json", e.to_string()))?; + 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..0caa39ff4 --- /dev/null +++ b/wai/wai-rs/src/package/tests.rs @@ -0,0 +1,273 @@ +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"); + 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(&[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.get("energy_provenance").is_some(), r.energy_provenance.is_some()); + 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: two keys swapped is not this receipt's JSON. + 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}"); + let swapped = j.replacen("\"asset_id\":\"asset-01\",", "", 1).replacen("{\"components\"", "{\"components\"", 1); + assert_ne!(swapped, 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"); +} + +/// 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. +#[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..c11abb2b5 --- /dev/null +++ b/wai/wai-rs/src/stage_index.rs @@ -0,0 +1,791 @@ +//! 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"`. +pub const MAGIC_WSIX: &[u8; 4] = b"WSIX"; +/// The version this module reads and writes. +pub const VERSION: u16 = 1; +/// The header's length. +pub const HEADER_LEN: usize = 160; +/// A piece record's length. +pub const PIECE_LEN: usize = 32; +/// An entry record's length. +pub const ENTRY_LEN: usize = 128; +/// A class record's length. +pub const CLASS_LEN: usize = 16; +/// The index digest's domain. +pub const DOMAIN_INDEX: &[u8] = b"wai:pack-stage-index\x01"; +/// A stage leaf's domain. +pub const DOMAIN_LEAF: &[u8] = b"wai:pack-stage\x01"; +/// The plan digest's domain (§4.2). +pub const DOMAIN_PLAN: &[u8] = b"wai:pack-plan\x01"; +/// `refines` of an entry that refines nothing. +pub const NO_REFINES: u32 = u32::MAX; +/// A class plan value: the piece is floor only for the class. +pub const FLOOR_ONLY: u8 = 0xFF; +/// `sse` not computed. +pub const SSE_UNKNOWN: u64 = u64::MAX; + +/// Piece flag: the piece's first frame follows a scene cut. +pub const PIECE_SCENE_CUT: u8 = 1; +/// Piece flag: the piece was started at `max`. +pub const PIECE_FORCED: u8 = 2; +/// Piece flag: the piece was started by a cue. +pub const PIECE_CUE: u8 = 4; +/// Piece flag: the piece starts at a lead component's boundary (§4.5). +pub const PIECE_LEAD_ALIGNED: u8 = 8; + +/// Entry flag: the object may be delivered after its piece's presentation instant. +pub const ENTRY_DEFERRABLE: u8 = 1; +/// Entry flag: an earlier entry of this index has the same content hash. +pub const ENTRY_DUP: u8 = 2; +/// Entry flag: the object's manifest carries `derived_from`. +pub const ENTRY_DERIVED_FROM: u8 = 4; + +/// Class flag: the class receives the floor only. +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 const 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). +pub const TIER_NONE: u8 = 0; +/// `entropy-consistency`, admitted on layer 0 only. +pub const TIER_ENTROPY: u8 = 1; +/// `decode-equivalence`. +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 +} + +struct Reader<'a> { + b: &'a [u8], + at: usize, +} + +impl<'a> Reader<'a> { + fn take(&mut self, n: usize, what: &str) -> Result<&'a [u8], IndexReason> { + let end = self.at.checked_add(n).filter(|&e| e <= self.b.len()).ok_or_else(|| IndexReason::new("index-truncated", format!("the {what} runs past the file's end")))?; + let s = &self.b[self.at..end]; + self.at = end; + Ok(s) + } +} + +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") +} + +fn zero(b: &[u8], what: &str) -> Result<(), IndexReason> { + if b.iter().all(|&x| x == 0) { + Ok(()) + } else { + Err(IndexReason::new("index-reserved", format!("{what} is not zero"))) + } +} + +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]))); + } + zero(&b[58..60], "an entry's reserved bytes")?; + zero(&b[108..112], "an entry's reserved bytes")?; + zero(&b[120..128], "an entry's reserved bytes")?; + 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). Refused: another magic (`index-magic`) or version + /// (`index-version`); a table that runs past the end (`index-truncated`); bytes after + /// the string table (`index-trailing`); reserved or padding bytes that are not zero + /// (`index-reserved`); a media class, role or tier with no meaning (`index-media-class`, + /// `index-role`, `index-tier`); a string that is empty, longer than its file allows or + /// not UTF-8, or a string index past the table (`index-string`). + pub fn parse(b: &[u8]) -> Result { + let mut r = Reader { b, at: 0 }; + let hd = r.take(HEADER_LEN, "header")?; + if &hd[0..4] != MAGIC_WSIX { + return Err(IndexReason::new("index-magic", "not a WSIX file")); + } + let version = u16_at(hd, 4); + if version != VERSION { + return Err(IndexReason::new("index-version", format!("version {version}"))); + } + let media_class = MediaClass::from_code(u16_at(hd, 6)).ok_or_else(|| IndexReason::new("index-media-class", format!("media class {}", u16_at(hd, 6))))?; + let (n_pieces, n_entries, n_strings, n_classes) = (u32_at(hd, 16) as usize, u32_at(hd, 20) as usize, u32_at(hd, 24) as usize, u32_at(hd, 28) as usize); + zero(&hd[146..160], "the header's reserved bytes")?; + let header = Header { + media_class, + tb_num: u32_at(hd, 8), + tb_den: u32_at(hd, 12), + policy_digest: h32(hd, 32), + prev_index_digest: h32(hd, 64), + first_piece: u64_at(hd, 96), + first_entry: u64_at(hd, 104), + plan_digest: h32(hd, 112), + component: u16_at(hd, 144), + }; + // Every table is sized from a count before anything is allocated: a count the + // file cannot hold is refused as truncated. + let need = |n: usize, len: usize| n.checked_mul(len); + let rest = b.len().saturating_sub(HEADER_LEN); + for (n, len, what) in [(n_pieces, PIECE_LEN, "piece table"), (n_entries, ENTRY_LEN, "entry table"), (n_classes, CLASS_LEN, "class table")] { + if need(n, len).is_none_or(|x| x > rest) { + return Err(IndexReason::new("index-truncated", format!("the {what} runs past the file's end"))); + } + } + let mut pieces = Vec::with_capacity(n_pieces); + for _ in 0..n_pieces { + let p = r.take(PIECE_LEN, "piece table")?; + zero(&p[28..32], "a piece's reserved bytes")?; + pieces.push(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), + }); + } + let mut entries = Vec::with_capacity(n_entries); + for _ in 0..n_entries { + entries.push(EntryRec::parse(r.take(ENTRY_LEN, "entry table")?)?); + } + let mut classes = Vec::with_capacity(n_classes); + for _ in 0..n_classes { + let c = r.take(CLASS_LEN, "class table")?; + zero(&c[8..16], "a class's reserved bytes")?; + classes.push(ClassRec { name: u16_at(c, 0), max_layer: c[2], flags: c[3], max_kbps: u32_at(c, 4) }); + } + let plan_len = n_classes.checked_mul(n_pieces).filter(|&x| x <= b.len()).ok_or_else(|| IndexReason::new("index-truncated", "the class plan runs past the file's end"))?; + let plan_bytes = r.take(plan_len, "class plan")?; + let plan: Vec> = if n_pieces == 0 { vec![Vec::new(); n_classes] } else { plan_bytes.chunks(n_pieces).map(<[u8]>::to_vec).collect() }; + zero(r.take(pad8(plan_len) - plan_len, "class plan's padding")?, "the class plan's padding")?; + if n_strings > b.len() / 2 { + return Err(IndexReason::new("index-truncated", "the string table runs past the file's end")); + } + let strings_start = r.at; + let mut strings = Vec::with_capacity(n_strings); + for _ in 0..n_strings { + let len = u16_at(r.take(2, "string table")?, 0) as usize; + let s = r.take(len, "string table")?; + 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 used = r.at - strings_start; + zero(r.take(pad8(used) - used, "string table's padding")?, "the string table's padding")?; + if r.at != b.len() { + return Err(IndexReason::new("index-trailing", format!("{} bytes follow the string table", b.len() - r.at))); + } + 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")); + } + 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 the index alone decides, in this order; the first that fails is + /// the reason: + /// + /// 1. the timebase is not zero (`index-timebase`); pieces follow each other, each + /// starting where the one before ended and lasting at least one unit + /// (`piece-order`); the pieces' entries are the index's entries, in order, at + /// least one each, every entry naming its piece's absolute number + /// (`piece-entries`); an unknown piece kind or flag (`piece-kind`); + /// 2. per entry, in order: an unknown flag (`entry-flags`); within its piece, roles + /// in the order base, refinement, floor, others, and refinements in layer order + /// (`entry-order`); a base or floor at layer 0, a refinement above it, and + /// `refines` exactly on a refinement (`layer-role`); `refines` inside the window + /// (`refines-outside-window`) and naming an earlier entry of the same piece + /// (`refines-mismatch`) one layer below (`layer-gap`); a deadline only on a + /// refinement (`deadline-role`); a floor carrying `derived_from` (`floor-derived-from`); + /// `entropy-consistency` only on layer 0 (`tier-entropy-passthrough`); + /// decode-equivalence on a refinement only over a decode-equivalent prefix by a + /// decode-equivalent step (`tier-weakest`); otherwise the tier §6.3 gives + /// (`tier-mismatch`); a non-zero prefix digest only with decode-equivalence + /// (`digest-without-tier`); the duplicate flag exactly when an earlier entry has + /// the content hash (`dup-flag`), at the same length (`dup-length`); + /// 3. the classes: an unknown flag (`class-flags`); one plan row per class and one + /// value per piece, each [`FLOOR_ONLY`] for a floor-only class, otherwise at most + /// the class's `max_layer` and the piece's highest layer (`class-plan`). + 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()); + } + 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 an entry names, `bytes`, and, for a +/// refinement, the entry it refines (`below`): 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` names the object of `below` at the +/// entry's layer (`object-refines`); and `derived_from` is present, well formed, exactly +/// when the entry says so (`object-derived-from`). +pub fn check_object(ix: &StageIndex, e: &EntryRec, below: Option<&EntryRec>, bytes: &[u8]) -> Result<(), IndexReason> { + let r = |code: &'static str, detail: String| Err(IndexReason::new(code, detail)); + 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(); + (first, None, None, 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 { + let below = below.ok_or_else(|| IndexReason::new("object-refines", "a refinement with no entry below"))?; + 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 && hex(&below.content_hash) == rf.base); + if !ok { + return r("object-refines", "the object's refines does not name the entry below at the entry's layer".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()) { + 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..53a81e5ca --- /dev/null +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -0,0 +1,256 @@ +use super::*; +use crate::container::{Conditioning, Manifest, ModelRequirement, Prev}; + +fn env(cap: &str, refines: Option<(&[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, layer)) = refines { + let b = hex(base); + m = m.with_refines(&Refines { base: b.clone(), prev: Prev::One(b), 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`. +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, 1)), false, b"r1"); + let r1h = crate::jwp_hash::content_hash(&r1); + let r2 = env("wai.video.int_refine", Some((&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, e) in ix.entries.iter().enumerate() { + let below = (e.refines != NO_REFINES).then(|| &ix.entries[e.refines as usize]); + check_object(&ix, e, below, &objects[k]).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), + ]; + 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 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, "index-truncated", "prefix {n}: {r}"); + } + 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 mut e = ix.entries[0].clone(); + e.content_hash = crate::jwp_hash::content_hash(&cmaf); + e.byte_len = cmaf.len() as u64; + assert_eq!(check_object(&ix, &e, None, &cmaf).unwrap_err().code, "non-envelope-entry"); + let r = |k: usize, bytes: &[u8], f: &dyn Fn(&mut EntryRec)| { + let mut e = ix.entries[k].clone(); + f(&mut e); + let below = (e.refines != NO_REFINES).then(|| &ix.entries[e.refines as usize]); + check_object(&ix, &e, below, bytes).map(|_| "ok").unwrap_or_else(|r| r.code) + }; + 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, &|e| { + e.byte_len += 1; + e.content_hash = crate::jwp_hash::content_hash(&trailing); + }), "non-envelope-entry"); + assert_eq!(r(0, &objects[0], &|e| e.payload_len += 1), "object-lengths"); + assert_eq!(r(0, &objects[0], &|e| e.capability = 4), "object-capability"); + assert_eq!(r(2, &objects[2], &|e| e.refines = 0), "object-refines"); + assert_eq!(r(3, &objects[3], &|e| e.flags = 0), "object-derived-from"); + assert_eq!(r(0, &objects[0], &|e| e.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); +} -- 2.54.0 (Apple Git-157) From a8539781f4869ba0731c937a7d216ff42e32f4ce Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 05:36:37 -0400 Subject: [PATCH 02/28] wai-rs: keep the stage index's and package receipt's constants out of the C header Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/include/wai.h | 102 ---------------------------------- wai/wai-rs/src/package.rs | 6 ++ wai/wai-rs/src/stage_index.rs | 55 ++++++++++++++++-- 3 files changed, 57 insertions(+), 106 deletions(-) diff --git a/wai/wai-rs/include/wai.h b/wai/wai-rs/include/wai.h index eadcef21f..57e905f88 100644 --- a/wai/wai-rs/include/wai.h +++ b/wai/wai-rs/include/wai.h @@ -344,111 +344,11 @@ */ #define TRITS_PER_BYTE 5 -/** - * The version this module reads and writes. - */ -#define VERSION 1 - -/** - * The header's length. - */ -#define HEADER_LEN 160 - -/** - * A piece record's length. - */ -#define PIECE_LEN 32 - -/** - * An entry record's length. - */ -#define ENTRY_LEN 128 - -/** - * A class record's length. - */ -#define CLASS_LEN 16 - -/** - * `refines` of an entry that refines nothing. - */ -#define NO_REFINES UINT32_MAX - -/** - * A class plan value: the piece is floor only for the class. - */ -#define FLOOR_ONLY 255 - -/** - * `sse` not computed. - */ -#define SSE_UNKNOWN UINT64_MAX - -/** - * Piece flag: the piece's first frame follows a scene cut. - */ -#define PIECE_SCENE_CUT 1 - -/** - * Piece flag: the piece was started at `max`. - */ -#define PIECE_FORCED 2 - -/** - * Piece flag: the piece was started by a cue. - */ -#define PIECE_CUE 4 - -/** - * Piece flag: the piece starts at a lead component's boundary (§4.5). - */ -#define PIECE_LEAD_ALIGNED 8 - -/** - * Entry flag: the object may be delivered after its piece's presentation instant. - */ -#define ENTRY_DEFERRABLE 1 - -/** - * Entry flag: an earlier entry of this index has the same content hash. - */ -#define ENTRY_DUP 2 - -/** - * Entry flag: the object's manifest carries `derived_from`. - */ -#define ENTRY_DERIVED_FROM 4 - -/** - * Class flag: the class receives the floor only. - */ -#define CLASS_FLOOR_ONLY 1 - -/** - * A prefix's determinism tier as an entry states it (§6.3). - */ -#define TIER_NONE 0 - -/** - * `entropy-consistency`, admitted on layer 0 only. - */ -#define TIER_ENTROPY 1 - -/** - * `decode-equivalence`. - */ -#define TIER_DECODE 2 - /** * Q32.32 fixed-point value. */ typedef struct Fx Fx; -/** - * A component's media class (§6, the header's `media_class`). - */ -typedef struct MediaClass MediaClass; - typedef struct wai_buffer_t { uint8_t *data; uintptr_t len; @@ -504,8 +404,6 @@ typedef struct wai_decoded_t { - - #ifdef __cplusplus extern "C" { #endif // __cplusplus diff --git a/wai/wai-rs/src/package.rs b/wai/wai-rs/src/package.rs index 5e47f4544..e1a85f792 100644 --- a/wai/wai-rs/src/package.rs +++ b/wai/wai-rs/src/package.rs @@ -37,10 +37,16 @@ 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 diff --git a/wai/wai-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs index c11abb2b5..0aea1ff0f 100644 --- a/wai/wai-rs/src/stage_index.rs +++ b/wai/wai-rs/src/stage_index.rs @@ -24,47 +24,87 @@ 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`). @@ -81,12 +121,13 @@ pub enum MediaClass { impl MediaClass { /// Every class, by its code. - pub const ALL: [MediaClass; 7] = - [MediaClass::Video, MediaClass::Image, MediaClass::Audio, MediaClass::World, MediaClass::Feed, MediaClass::Avatar, MediaClass::Film]; + 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() + Self::all().get(c as usize).copied() } /// The class's code. @@ -110,7 +151,7 @@ impl MediaClass { /// The class a label names. pub fn from_label(s: &str) -> Option { - Self::ALL.into_iter().find(|m| m.label() == s) + Self::all().into_iter().find(|m| m.label() == s) } } @@ -144,10 +185,16 @@ impl Role { } /// 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 -- 2.54.0 (Apple Git-157) From 411966b67254f3bdecc298dbcc73266dac7178bd Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 05:52:22 -0400 Subject: [PATCH 03/28] wai-pack: the packer and its verifier, with a supplied AV1 floor, CMAF, and HLS, DASH, MoQ and stage-view catalogs - av1: the low-overhead OBU stream split into temporal units, shown key frames, the sequence header's fields, the codecs string and the av1C record. - floor: a supplied floor checked against its pinned listing and the keyframe plan; the reference AV1 framing of a floor object's payload. - cmaf: init.mp4 (cmfc, av01 + av1C) and one fragment per piece, pure functions of the samples. - objects: base and floor envelopes (derived_from on the floor), named by content hash. - plan: moments, rules and the class plan (max_layer, rules, max_kbps, floor only). - catalog: on http-delivery's Mode N, the classical master and floor playlist, each class's master (one added SESSION-DATA line), its WAI playlist with the codecs union and X-WAI-FLOOR, its WAI media playlist with #WAI-LAYER lines and #EXT-X-GAP for floor-only pieces; classical and per-class WAI MPDs; the MoQ catalog; stage views. - pack: one video component, an int_motion base whose step choose_q picks per piece, a supplied floor; refuses by name what this revision does not pack. - verify: LISTING, the receipt and policy, the indexes, every object and prefix digest, the floor's CMAF rebuilt, the catalogs re-rendered, the measurement claim. - CLI: plan writes the keyframe plan; pack, verify, listing. - fixture video-base-01: three shots and a bumper played twice. Co-Authored-By: Claude Opus 5.5 --- wai/wai-pack/Cargo.lock | 211 ++++++++++++- wai/wai-pack/Cargo.toml | 10 +- wai/wai-pack/src/av1.rs | 419 ++++++++++++++++++++++++++ wai/wai-pack/src/bin/wai_pack.rs | 104 ++++++- wai/wai-pack/src/catalog.rs | 445 ++++++++++++++++++++++++++++ wai/wai-pack/src/cmaf.rs | 210 +++++++++++++ wai/wai-pack/src/conformance.rs | 6 +- wai/wai-pack/src/fixture.rs | 39 ++- wai/wai-pack/src/floor.rs | 140 +++++++++ wai/wai-pack/src/lib.rs | 8 + wai/wai-pack/src/objects.rs | 68 +++++ wai/wai-pack/src/pack.rs | 415 ++++++++++++++++++++++++++ wai/wai-pack/src/plan.rs | 128 ++++++++ wai/wai-pack/src/verify.rs | 295 ++++++++++++++++++ wai/wai-rs/src/wai_video_package.rs | 2 +- 15 files changed, 2481 insertions(+), 19 deletions(-) create mode 100644 wai/wai-pack/src/av1.rs create mode 100644 wai/wai-pack/src/catalog.rs create mode 100644 wai/wai-pack/src/cmaf.rs create mode 100644 wai/wai-pack/src/floor.rs create mode 100644 wai/wai-pack/src/objects.rs create mode 100644 wai/wai-pack/src/pack.rs create mode 100644 wai/wai-pack/src/plan.rs create mode 100644 wai/wai-pack/src/verify.rs 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..21cad1c87 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); 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", "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..d7de8f0a1 100644 --- a/wai/wai-pack/src/bin/wai_pack.rs +++ b/wai/wai-pack/src/bin/wai_pack.rs @@ -6,7 +6,20 @@ //! 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; +//! 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 +//! 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 +51,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 \ @@ -93,14 +109,91 @@ 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")?; + 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").then_some(true) }; + 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 +307,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..44d307200 --- /dev/null +++ b/wai/wai-pack/src/catalog.rs @@ -0,0 +1,445 @@ +//! 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`. +fn timeline(ix: &StageIndex, ps: &[usize], tb: Timebase, indent: &str) -> String { + let mut o = format!("{indent}\n"); + for &p in ps { + let piece = &ix.pieces[p]; + let _ = writeln!(o, "{indent} ", (piece.start - ix.pieces[0].start) * tb.den as u64, piece.duration as u64 * tb.den as u64); + } + let _ = writeln!(o, "{indent}"); + o +} + +/// `dash/classic.mpd`. +pub fn render_dash_classic(v: &CompView) -> String { + 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); + 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`. +pub fn render_moq_catalog(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64) -> String { + let id = v.id(); + let mut tracks: Vec = Vec::new(); + let max_layer = v.index.entries.iter().map(|e| e.layer).max().unwrap_or(0); + for k in 0..=max_layer { + let layer: Vec<&EntryRec> = v.index.entries.iter().filter(|e| e.layer == k && matches!(e.role, Role::Base | Role::Refinement)).collect(); + let mut deadlines: Vec = layer.iter().map(|e| e.deadline_ms).collect(); + deadlines.sort_unstable(); + deadlines.dedup(); + for d in deadlines { + let on: Vec<&&EntryRec> = layer.iter().filter(|e| e.deadline_ms == d).collect(); + let mut caps: Vec = Vec::new(); + for e in &on { + let c = v.index.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 name = if k == 0 || d as u64 == default_deadline { format!("{id}/l{k}") } else { format!("{id}/l{k}@{d}") }; + let mut t = json!({ + "name": name, + "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 { + t["depends"] = json!([format!("{id}/l{}", k - 1)]); + 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})).expect("a catalog is in the canonical subset") + "\n" +} + +/// `stages/.json`: RFC 8785 canonical. +pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> String { + 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 mut o = json!({"dur": piece.duration, "p": v.index.header.first_piece + p as u64, "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 comp = json!({"media_playlist": format!("../hls/{class}/wai-{}.m3u8", v.id()), "pieces": pieces}); + 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)})).expect("a stage view is in the canonical subset") + "\n" +} + +/// 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) +} 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..677778c2c 100644 --- a/wai/wai-pack/src/conformance.rs +++ b/wai/wai-pack/src/conformance.rs @@ -256,7 +256,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 +308,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 +374,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)); 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..cf63e9120 --- /dev/null +++ b/wai/wai-pack/src/floor.rs @@ -0,0 +1,140 @@ +//! 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() { + if 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 +} + +/// The temporal units of a `wai.video.av1` payload in the reference framing, and its +/// header's `(h, w, fps_num, fps_den)`; `None` when it is not that framing. +pub fn av1_units(payload: &[u8]) -> Option<(Vec>, (u32, u32, u32, u32))> { + 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/lib.rs b/wai/wai-pack/src/lib.rs index 7fec099e5..7864bb5c3 100644 --- a/wai/wai-pack/src/lib.rs +++ b/wai/wai-pack/src/lib.rs @@ -13,14 +13,22 @@ #![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 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..25e3c3b0e --- /dev/null +++ b/wai/wai-pack/src/pack.rs @@ -0,0 +1,415 @@ +//! 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 base of another capability +//! (`pack-capability-unsupported`). +//! +//! 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 `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(sha2::Sha256::digest(f.as_ref())); + } + h.finalize().into() +} + +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) -> EntryRec { + let man = u32::from_le_bytes(env[4..8].try_into().expect("an envelope")); + 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: env.len() as u64 - 12 - man as u64, + } +} + +/// 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<(String, Vec, Vec<(String, Vec)>), String> { + 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 f = crate::cmaf::write_fragment(p as u32 + 1, starts[p] * tb_den as u64, &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); + if let Err(v) = crate::capcheck::check(&policy) { + let r = &v[0]; + return Err(PackError::new(r.code.clone(), format!("ladder.{}", r.rung))); + } + 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()))); + } + let classes_ok = policy.classes.iter().all(|c| id_is_path_segment(&c.id) && c.id != "classic"); + let folded: std::collections::BTreeSet = policy.classes.iter().map(|c| c.id.to_ascii_lowercase()).collect(); + if !id_is_path_segment(&comp.id) || !classes_ok || folded.len() != policy.classes.len() { + return Err(PackError::new("pack-id", "a component or class id that cannot name a file, or a class named classic")); + } + 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 { + return Err(PackError::new("pack-capability-unsupported", "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(); + let energy = match opts.meter.as_mut().and_then(|m| m.stop()) { + Some(r) => r.as_seal_args(), + None => wai::energy_meter::EnergyReading::unmetered(), + }; + 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/verify.rs b/wai/wai-pack/src/verify.rs new file mode 100644 index 000000000..3bd5cee57 --- /dev/null +++ b/wai/wai-pack/src/verify.rs @@ -0,0 +1,295 @@ +//! A package's verifier ([packaging] §9, §11): everything a package states that its own +//! files decide, recomputed from those files. It reads no source: a statement only a +//! source decides (an entry's `sse`) is the producer's. +//! +//! In order, each failure a reason of §12: +//! +//! 1. `LISTING` lists every other file with its SHA-256 (`listing-missing`, +//! `listing-mismatch`); no file is outside the layout (`unexpected-file`). +//! 2. The receipt reads as version 2 (`receipt-json`, `receipt-version`); `policy.json` +//! hashes to its policy digest (`policy-digest-mismatch`) and is the canonical form of +//! `policy.toml` with its `royalty_free_only` (`policy-json-mismatch`). +//! 3. The receipt verifies against the indexes (`index-missing`, and +//! `PackageReceiptV2::verify_detailed`'s reasons, the index rules among them). +//! 4. Per component: the plan file holds the pieces' starts and hashes to the index's plan +//! digest (`plan-mismatch`); every entry's object is held (`object-missing`) and is +//! what its entry says (the object rules of `stage_index::check_object`); every +//! decode-equivalent prefix decodes to its digest (`prefix-digest`); every object file +//! is some entry's (`object-unlisted`). +//! 5. The floor: each floor object presents its piece's base (`floor-motif-mismatch`) and +//! its CMAF files are the ones its samples give (`floor-cmaf-mismatch`). +//! 6. The catalogs are the ones the index, objects and floor give (`catalog-mismatch`, +//! `catalog-unexpected`). +//! 7. A metered receipt has its measurement claim (`measurement-claim-missing`), which +//! verifies and checks against the receipt (`measurement-claim-invalid`). +//! +//! [packaging]: ../../../extensions/packaging.md + +use crate::catalog::{self, CompView, FloorView}; +use crate::policy::{Overrides, Policy, 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, +} + +/// Verify the package in `dir`. +pub fn verify_dir(dir: &std::path::Path) -> Result> { + let files = wai::pin::read_dir_set(dir).map_err(|e| vec![reason("unexpected-file", e.to_string())])?; + verify_files(&files) +} + +/// Verify a package given as its files, by path. +pub fn verify_files(files: &BTreeMap>) -> Result> { + let one = |r: Reason| vec![r]; + // 1. 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"))); + } + // 2. Receipt and policy. + let text = files.get("package.receipt.json").ok_or_else(|| one(reason("receipt-json", "no package.receipt.json")))?; + let text = std::str::from_utf8(text).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"))), + }; + 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())))?; + let rfo: Option = serde_json::from_slice::(pj).ok().and_then(|v| v.get("royalty_free_only").and_then(|b| b.as_bool())); + 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"))); + } + // 3. Indexes against the receipt. + 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)))?; + let mut errs: Vec = Vec::new(); + 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) in r.components.iter().zip(&index_bytes) { + expected.insert(c.index_file()); + let ix = StageIndex::parse(bytes).expect("verified above"); + let comp = policy.component(&c.id).ok_or_else(|| one(reason("receipt-component", format!("the policy has no component {}", c.id))))?; + let tb = comp.timebase; + if (ix.header.tb_num, ix.header.tb_den) != (tb.num, tb.den) { + errs.push(reason("index-timebase", format!("component {}: the index's timebase is not the policy's", c.id))); + } + // 4. Plan. + let plan_path = format!("plan-{}.txt", c.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"))), + } + // Objects. + 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; + }; + let below = (e.refines != wsix::NO_REFINES).then(|| &ix.entries[e.refines as usize]); + if let Err(x) = wsix::check_object(&ix, e, below, b) { + errs.push(reason(x.code, format!("entry {k}: {}", x.detail))); + 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"))); + } + } + // 5. Floor. + let floor = match floor_view(&c.id, &ix, &objects, tb) { + Ok((view, produced)) => { + 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 samples give"))); + } + } + view + } + Err(e) => { + errs.push(e); + None + } + }; + if !errs.is_empty() { + return Err(errs); + } + // 6. Catalogs. + 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 }; + match catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &r.policy_digest) { + Ok(m) => catalog_files.extend(m), + Err(e) => errs.push(reason("catalog-mismatch", e.0)), + } + } + for (p, b) in &catalog_files { + expected.insert(p.clone()); + if files.get(p) != Some(b) { + errs.push(reason("catalog-mismatch", p.clone())); + } + } + // 7. Measurement claim. + 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(), + }) +} + +/// 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: each floor object presents its +/// piece's base, and its CMAF files are rebuilt from the objects' temporal units. +fn floor_view(comp: &str, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>, tb: Timebase) -> Result { + let mut pieces = Vec::new(); + let mut seq: Option<(crate::av1::SequenceHeader, Vec)> = None; + for p in 0..ix.pieces.len() { + let es = ix.piece_entries(p); + let Some(fe) = es.iter().find(|e| e.role == Role::Floor) else { return Ok((None, Vec::new())) }; + let base = es.iter().find(|e| e.role == Role::Base).ok_or_else(|| reason("floor-motif-mismatch", format!("piece {p} has no base")))?; + let w = wai::container::Wai::from_bytes(&objects[&fe.content_hash]).map_err(|e| reason("floor-cmaf-mismatch", 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"))); + } + let (units, _) = crate::floor::av1_units(&w.payload).ok_or_else(|| reason("floor-cmaf-mismatch", format!("piece {p}: the floor's payload is not the reference AV1 framing")))?; + if units.len() as u64 != ix.pieces[p].duration as u64 { + return Err(reason("floor-cmaf-mismatch", format!("piece {p}: {} units for {} frames", units.len(), ix.pieces[p].duration))); + } + let mut key = Vec::new(); + for (i, u) in units.iter().enumerate() { + let tus = crate::av1::temporal_units(u).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?; + let [tu] = tus.as_slice() else { return Err(reason("floor-cmaf-mismatch", format!("piece {p} unit {i} is not one temporal unit"))) }; + if seq.is_none() { + let s = tu.sequence_header().ok_or_else(|| reason("floor-cmaf-mismatch", "the floor's first unit has no sequence header"))?; + seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?, s.bytes.to_vec())); + } + key.push(crate::av1::starts_with_shown_key_frame(tu, &seq.as_ref().expect("set").0).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?); + } + 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, &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-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, -- 2.54.0 (Apple Git-157) From d561d372f8db53585788f47e37b9c7f5b6c320e4 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 05:55:26 -0400 Subject: [PATCH 04/28] pack-conformance: the golden video-base-01, the index, object and receipt mutations, and version-1 receipts as JSON wai-pack conformance regen writes, and check recomputes: - packs/video-base-01: policy, supplied AV1 floor, test key, and the whole package the packer writes from the generated fixture, byte for byte, which the verifier also verifies; - mutations: stage indexes, objects and receipts, each with the reason it gets; - receipts: the legacy version-1 receipt and an unmetered one as JSON. Co-Authored-By: Claude Opus 5.5 --- wai/pack-conformance/mutations/expected.json | 402 +++++++++++++ .../mutations/index/base-with-refines.wsix | Bin 0 -> 1088 bytes .../mutations/index/class-flag-unknown.wsix | Bin 0 -> 1088 bytes .../mutations/index/deadline-on-base.wsix | Bin 0 -> 1088 bytes .../mutations/index/digest-without-tier.wsix | Bin 0 -> 1088 bytes .../mutations/index/dup-flag-lie.wsix | Bin 0 -> 1088 bytes .../mutations/index/dup-flag-missing.wsix | Bin 0 -> 1088 bytes .../mutations/index/dup-length.wsix | Bin 0 -> 1088 bytes .../mutations/index/entry-flag-unknown.wsix | Bin 0 -> 1088 bytes .../index/entry-in-another-piece.wsix | Bin 0 -> 1088 bytes .../mutations/index/floor-at-layer-1.wsix | Bin 0 -> 1088 bytes .../index/floor-before-refinement.wsix | Bin 0 -> 1088 bytes .../index/floor-without-derived-from.wsix | Bin 0 -> 1088 bytes .../mutations/index/layer-gap.wsix | Bin 0 -> 1088 bytes .../mutations/index/magic.wsix | Bin 0 -> 1088 bytes .../mutations/index/media-class.wsix | Bin 0 -> 1088 bytes wai/pack-conformance/mutations/index/ok.wsix | Bin 0 -> 1088 bytes .../mutations/index/piece-empty.wsix | Bin 0 -> 1088 bytes .../index/piece-entries-overlap.wsix | Bin 0 -> 1088 bytes .../mutations/index/piece-gap.wsix | Bin 0 -> 1088 bytes .../mutations/index/piece-kind-unknown.wsix | Bin 0 -> 1088 bytes .../index/plan-floor-only-class.wsix | Bin 0 -> 1088 bytes .../mutations/index/plan-past-class.wsix | Bin 0 -> 1088 bytes .../mutations/index/plan-past-piece.wsix | Bin 0 -> 1088 bytes .../mutations/index/refines-flipped.wsix | Bin 0 -> 1088 bytes .../index/refines-outside-window.wsix | Bin 0 -> 1088 bytes .../mutations/index/reserved.wsix | Bin 0 -> 1088 bytes .../mutations/index/role.wsix | Bin 0 -> 1088 bytes .../mutations/index/string-index.wsix | Bin 0 -> 1088 bytes .../index/tier-decode-over-entropy.wsix | Bin 0 -> 1080 bytes .../index/tier-entropy-over-entropy.wsix | Bin 0 -> 1080 bytes .../mutations/index/tier-on-floor.wsix | Bin 0 -> 1088 bytes .../mutations/index/tier.wsix | Bin 0 -> 1088 bytes .../mutations/index/timebase-zero.wsix | Bin 0 -> 1088 bytes .../mutations/index/trailing.wsix | Bin 0 -> 1096 bytes .../mutations/index/truncated-table.wsix | Bin 0 -> 613 bytes .../mutations/index/version.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/capability.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/cmaf-listed.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/derived-from.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/length.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/lengths.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/missing.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/ok.wsix | Bin 0 -> 1088 bytes .../objects-index/refines-names-another.wsix | Bin 0 -> 1088 bytes ...4f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai | Bin 0 -> 334 bytes ...d593c8442644263d45ffed1f416040b9de7198.wai | Bin 0 -> 166 bytes ...3d6b9679c567f7f320397a4c412568835e332a.wai | Bin 0 -> 391 bytes ...bd4f15a39476cffa55f70484478ab6d857d2d1.wai | Bin 0 -> 32 bytes ...bebc4a9f78b0afceb9277b1227bbd4294f51e2.wai | Bin 0 -> 334 bytes ...2e45a374d03c87b5268bfa56ec5591f38afa3e.wai | Bin 0 -> 391 bytes ...ae792f5aee3b32a03539f6b6d56feaff43f06f.wai | Bin 0 -> 334 bytes ...a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai | Bin 0 -> 166 bytes .../mutations/receipt/counts.json | 1 + .../mutations/receipt/energy.json | 1 + .../mutations/receipt/index-digest.json | 1 + .../mutations/receipt/index-v.wsix | Bin 0 -> 1088 bytes .../mutations/receipt/live/index-v.wsix | Bin 0 -> 1088 bytes .../mutations/receipt/media-class.json | 1 + .../mutations/receipt/ok-labelled.json | 1 + .../mutations/receipt/ok-live-window.json | 1 + .../mutations/receipt/ok.json | 1 + .../mutations/receipt/parent-kind-live.json | 1 + .../mutations/receipt/parent-kind.json | 1 + .../mutations/receipt/policy-digest.json | 1 + .../mutations/receipt/receipt-hash.json | 1 + .../mutations/receipt/signature.json | 1 + .../mutations/receipt/stage-root.json | 1 + .../mutations/receipt/unknown-member.json | 1 + .../packs/video-base-01/floor/v.obu | Bin 0 -> 6546 bytes .../packs/video-base-01/key.hex | 1 + .../packs/video-base-01/package/LISTING | 35 ++ .../video-base-01/package/dash/classic.mpd | 24 + .../video-base-01/package/dash/mobile.mpd | 21 + .../packs/video-base-01/package/dash/tv.mpd | 27 + ...2a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s | Bin 0 -> 981 bytes ...5de0902f941a3c6a000295714f7cab193aa6cf.m4s | Bin 0 -> 981 bytes ...b33fd445037d6538b445bcc76431f9c9f91c5b.m4s | Bin 0 -> 2400 bytes ...01dc69170ecb4240dffd2e86808ddf87baa12a.m4s | Bin 0 -> 2605 bytes ...d61b222e458b1f93b8082026a9874aeb9f4c15.m4s | Bin 0 -> 2059 bytes .../video-base-01/package/floor/v/init.mp4 | Bin 0 -> 628 bytes .../package/hls/classic/floor-v.m3u8 | 18 + .../package/hls/classic/master.m3u8 | 5 + .../package/hls/legacy/master.m3u8 | 5 + .../package/hls/mobile/master.m3u8 | 6 + .../package/hls/mobile/wai-v.m3u8 | 20 + .../video-base-01/package/hls/mobile/wai.m3u8 | 7 + .../video-base-01/package/hls/tv/master.m3u8 | 6 + .../video-base-01/package/hls/tv/wai-v.m3u8 | 17 + .../video-base-01/package/hls/tv/wai.m3u8 | 7 + .../packs/video-base-01/package/index-v.wsix | Bin 0 -> 1728 bytes .../video-base-01/package/moq/catalog.json | 1 + ...371966b50573602a200dd7bb04e0d90889326d.wai | Bin 0 -> 39826 bytes ...5f35cc55fabd2a1954759aebe4b95dbc76361f.wai | Bin 0 -> 2648 bytes ...19c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai | Bin 0 -> 25514 bytes ...a0240598b914dbf2cabe545aa001e8de12d365.wai | Bin 0 -> 9950 bytes ...775765b209ee80d2f4fc1cf040838c6724aa2d.wai | Bin 0 -> 2222 bytes ...190311b6a2348842e571684b021a213341b94a.wai | Bin 0 -> 1234 bytes ...8559277f926322dd6ddc345306b8231f2d1752.wai | Bin 0 -> 2503 bytes ...d007436368e8d20467fdec2a1eebbb84b03bf2.wai | Bin 0 -> 35882 bytes .../package/package.receipt.json | 1 + .../packs/video-base-01/package/plan-v.txt | 5 + .../packs/video-base-01/package/policy.json | 1 + .../packs/video-base-01/package/policy.toml | 47 ++ .../video-base-01/package/stages/legacy.json | 1 + .../video-base-01/package/stages/mobile.json | 1 + .../video-base-01/package/stages/tv.json | 1 + .../packs/video-base-01/policy.toml | 47 ++ wai/pack-conformance/receipts/expected.json | 12 + wai/pack-conformance/receipts/v1-legacy.json | 1 + .../receipts/v1-unmetered.json | 1 + wai/wai-pack/src/conformance.rs | 6 +- wai/wai-pack/src/golden.rs | 532 ++++++++++++++++++ wai/wai-pack/src/lib.rs | 1 + 114 files changed, 1272 insertions(+), 1 deletion(-) create mode 100644 wai/pack-conformance/mutations/expected.json create mode 100644 wai/pack-conformance/mutations/index/base-with-refines.wsix create mode 100644 wai/pack-conformance/mutations/index/class-flag-unknown.wsix create mode 100644 wai/pack-conformance/mutations/index/deadline-on-base.wsix create mode 100644 wai/pack-conformance/mutations/index/digest-without-tier.wsix create mode 100644 wai/pack-conformance/mutations/index/dup-flag-lie.wsix create mode 100644 wai/pack-conformance/mutations/index/dup-flag-missing.wsix create mode 100644 wai/pack-conformance/mutations/index/dup-length.wsix create mode 100644 wai/pack-conformance/mutations/index/entry-flag-unknown.wsix create mode 100644 wai/pack-conformance/mutations/index/entry-in-another-piece.wsix create mode 100644 wai/pack-conformance/mutations/index/floor-at-layer-1.wsix create mode 100644 wai/pack-conformance/mutations/index/floor-before-refinement.wsix create mode 100644 wai/pack-conformance/mutations/index/floor-without-derived-from.wsix create mode 100644 wai/pack-conformance/mutations/index/layer-gap.wsix create mode 100644 wai/pack-conformance/mutations/index/magic.wsix create mode 100644 wai/pack-conformance/mutations/index/media-class.wsix create mode 100644 wai/pack-conformance/mutations/index/ok.wsix create mode 100644 wai/pack-conformance/mutations/index/piece-empty.wsix create mode 100644 wai/pack-conformance/mutations/index/piece-entries-overlap.wsix create mode 100644 wai/pack-conformance/mutations/index/piece-gap.wsix create mode 100644 wai/pack-conformance/mutations/index/piece-kind-unknown.wsix create mode 100644 wai/pack-conformance/mutations/index/plan-floor-only-class.wsix create mode 100644 wai/pack-conformance/mutations/index/plan-past-class.wsix create mode 100644 wai/pack-conformance/mutations/index/plan-past-piece.wsix create mode 100644 wai/pack-conformance/mutations/index/refines-flipped.wsix create mode 100644 wai/pack-conformance/mutations/index/refines-outside-window.wsix create mode 100644 wai/pack-conformance/mutations/index/reserved.wsix create mode 100644 wai/pack-conformance/mutations/index/role.wsix create mode 100644 wai/pack-conformance/mutations/index/string-index.wsix create mode 100644 wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix create mode 100644 wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix create mode 100644 wai/pack-conformance/mutations/index/tier-on-floor.wsix create mode 100644 wai/pack-conformance/mutations/index/tier.wsix create mode 100644 wai/pack-conformance/mutations/index/timebase-zero.wsix create mode 100644 wai/pack-conformance/mutations/index/trailing.wsix create mode 100644 wai/pack-conformance/mutations/index/truncated-table.wsix create mode 100644 wai/pack-conformance/mutations/index/version.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/capability.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/derived-from.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/length.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/lengths.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/missing.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/ok.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/refines-names-another.wsix create mode 100644 wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai create mode 100644 wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai create mode 100644 wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai create mode 100644 wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai create mode 100644 wai/pack-conformance/mutations/objects/97627b882afe8e0512011d2c45bebc4a9f78b0afceb9277b1227bbd4294f51e2.wai create mode 100644 wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai create mode 100644 wai/pack-conformance/mutations/objects/b94c6b554acac94b32163312dbae792f5aee3b32a03539f6b6d56feaff43f06f.wai create mode 100644 wai/pack-conformance/mutations/objects/fae6cf7e7d39c8a77e06f86929a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai create mode 100644 wai/pack-conformance/mutations/receipt/counts.json create mode 100644 wai/pack-conformance/mutations/receipt/energy.json create mode 100644 wai/pack-conformance/mutations/receipt/index-digest.json create mode 100644 wai/pack-conformance/mutations/receipt/index-v.wsix create mode 100644 wai/pack-conformance/mutations/receipt/live/index-v.wsix create mode 100644 wai/pack-conformance/mutations/receipt/media-class.json create mode 100644 wai/pack-conformance/mutations/receipt/ok-labelled.json create mode 100644 wai/pack-conformance/mutations/receipt/ok-live-window.json create mode 100644 wai/pack-conformance/mutations/receipt/ok.json create mode 100644 wai/pack-conformance/mutations/receipt/parent-kind-live.json create mode 100644 wai/pack-conformance/mutations/receipt/parent-kind.json create mode 100644 wai/pack-conformance/mutations/receipt/policy-digest.json create mode 100644 wai/pack-conformance/mutations/receipt/receipt-hash.json create mode 100644 wai/pack-conformance/mutations/receipt/signature.json create mode 100644 wai/pack-conformance/mutations/receipt/stage-root.json create mode 100644 wai/pack-conformance/mutations/receipt/unknown-member.json create mode 100644 wai/pack-conformance/packs/video-base-01/floor/v.obu create mode 100644 wai/pack-conformance/packs/video-base-01/key.hex create mode 100644 wai/pack-conformance/packs/video-base-01/package/LISTING create mode 100644 wai/pack-conformance/packs/video-base-01/package/dash/classic.mpd create mode 100644 wai/pack-conformance/packs/video-base-01/package/dash/mobile.mpd create mode 100644 wai/pack-conformance/packs/video-base-01/package/dash/tv.mpd create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/2f93f0ad449f9cc881b7816c6ab33fd445037d6538b445bcc76431f9c9f91c5b.m4s create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/420ae4c941bba6e12a6c7fdb7d01dc69170ecb4240dffd2e86808ddf87baa12a.m4s create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/99a24de04f7be9fc21097e69d2d61b222e458b1f93b8082026a9874aeb9f4c15.m4s create mode 100644 wai/pack-conformance/packs/video-base-01/package/floor/v/init.mp4 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/classic/floor-v.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/classic/master.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/legacy/master.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/mobile/master.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai-v.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/mobile/wai.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/tv/master.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/tv/wai-v.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/hls/tv/wai.m3u8 create mode 100644 wai/pack-conformance/packs/video-base-01/package/index-v.wsix create mode 100644 wai/pack-conformance/packs/video-base-01/package/moq/catalog.json create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/24761a434c91c627f11abd6265371966b50573602a200dd7bb04e0d90889326d.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/35c7a45323b7afe979c123fa885f35cc55fabd2a1954759aebe4b95dbc76361f.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/618e0a89be8c7e556b380326b219c2f6fbfc5a0cb97754e871c1c23a99bdb481.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/61c7c3b7ccab07ed01df5b0d7ca0240598b914dbf2cabe545aa001e8de12d365.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/761c4649427c51796a2b699568775765b209ee80d2f4fc1cf040838c6724aa2d.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai create mode 100644 wai/pack-conformance/packs/video-base-01/package/package.receipt.json create mode 100644 wai/pack-conformance/packs/video-base-01/package/plan-v.txt create mode 100644 wai/pack-conformance/packs/video-base-01/package/policy.json create mode 100644 wai/pack-conformance/packs/video-base-01/package/policy.toml create mode 100644 wai/pack-conformance/packs/video-base-01/package/stages/legacy.json create mode 100644 wai/pack-conformance/packs/video-base-01/package/stages/mobile.json create mode 100644 wai/pack-conformance/packs/video-base-01/package/stages/tv.json create mode 100644 wai/pack-conformance/packs/video-base-01/policy.toml create mode 100644 wai/pack-conformance/receipts/expected.json create mode 100644 wai/pack-conformance/receipts/v1-legacy.json create mode 100644 wai/pack-conformance/receipts/v1-unmetered.json create mode 100644 wai/wai-pack/src/golden.rs diff --git a/wai/pack-conformance/mutations/expected.json b/wai/pack-conformance/mutations/expected.json new file mode 100644 index 000000000..e0bd0fcee --- /dev/null +++ b/wai/pack-conformance/mutations/expected.json @@ -0,0 +1,402 @@ +{ + "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": "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-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-receipt-hash", + "kind": "receipt", + "receipt": "receipt/receipt-hash.json", + "indexes": [ + "receipt/index-v.wsix" + ], + "reason": "receipt-json" + } + ] +} 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 0000000000000000000000000000000000000000..5b6e5a5812d007aa05c28e84513f14018074f3da GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_K}=W}fWiR;*a?9DP%sUm9P9=t6HJNj&HG>ccR}?L^P^AF z{C)b?&pWfP>cA?;Fs?IGPv3Rx@I!ViR3kIc4KQq#L;yQVE)?3@`X_ZIQzkgP+tnzVG&1jA@)`IyT*?wyJ zpAnp+5rG0rAB+rTObjJuA`In;nR;cJDXIB-nRzAgx%nlT`FZG~MX70-d8xceY7@&0 HA!Y&qD+JDh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a6595f23364395820d4a9c9a1669c8e9486d502d GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3^y1QIZ?^ufqb#>7xkCc;pjn5kEmnUb2Xmzh@*pPOHjnV*L)T9lfWnU~6o Lq&Bh45Mmwx)`-%7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bb56f6478be188d7e46159f57c15cfb361043d1e GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T2897T0RU1D0@HwMK>*!-qI>iH7yn&QeZ>6e zlQe&yzV-9Y?5jGk$}x=V%+%9&-8%e`9Sbpnff?urko|Xn4j^DXJH+!2WDL-LCTiKgqb_sS3#}XL zJsb{7J=1etTynvteXCmcudp|v6F+zTvTH~4Kgd5UpwLGTNMuhTv0?r%fttgPBnM&= z8~-q8fXoB=9UK5an%MBi75|PqJnug<(t6@Ld3V-Z+w5tTN7KK5RwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=uV5zR>e!2DkVHHRJMaVSk} z{KK39G7sc;Z~y>lV#6O-{5$UOy#LHd>xt{+-C1vKv!_)aP5=H`!LrK7Q8lAE&R7fP ze`NcS{g37y3@#!6Gs06cL@O+PFfx=eF_e^vFq9`|>Xl`tq~_~o=9R?f=9gsV=b?)h SrKV-(rSc-FO)N8nmwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN46jAe^4eQ0ub^)BRnNT41=W)MusvbhLSQ7hVsNry|T=d)O@|nyps6b{F2Q4Jao~b R)U?dJR9+;tiDiZm^8nWz(tiK| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d14069fe4cb28aeaa46556e4e2afd6eb4951491b GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV%wg(I=yk1PHicX-}^W~BARb@J}4x3<~SDvzdr|EyqH<>RQD(Hv*21@k|$ z{nYe7BitvDfPwjok)e!wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!3;O ze>qe=%o!l_Kz;`Y0FWj&{Bgy<;||aJ&y2L5xK7@k_0~3fTIJF7@1GSct9%?)Gn(U! zwP5~7wx62*XN0F@h(}@RgOQ<(iJ_!SgrPh!Q?D#DB{g3!Gp{5*H@_q^KM!5BC^aoJ OFO?TbZDN@r#5@4k4$^)A literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0c8d14ae2424bcbbc2b90ce5cb9d9dc929f83326 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015-91`v;dft>&VsRw~+5anPuK$&1lbZ_4O;=c>3 zkC-2QlIHKzw|?H4eN_imIfikanR@!JTZbRAW1$+EfqnqF;SMko2w2Y!vHkyl2dFt9 zeaK*XQgw&czdlwWMp+%#eS5s-S8P~+Zl`*+koxW`n*M>0(Cr5q1GJxsTK4a#%bfK> z>&AKyhl5hj^jsI0T(D{1s@DB0>`myz&t1Rl+R^+E@(&9r6{1HpvZs*PF#nf8&0$B9 z12Kt>f0#2s=7IbU4gercZ204ff5#o3_n#SQJ#n49JL|1&_O!~Q>EAyqSXTKss%A9D z8Ee7(k8D3R{m%%_(V&ok09g89WGG`|C@B+RC{N7PE6Yqt&DYD!D~Zp|FUic$Ll-Sd RP0P$nwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ0U( ze@1vph6D^OeK0bVF)@^si7=EWX6lt?rljWUW#*N{=jNAW=I5b{7Nw?T=B4r?sZA_1 HgqQ~a)fUox literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b92cdb7fd1f930841ff8faf950edcead58e25438 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3j-4)MnJ+mghs>sUjj9U9ZjCt z_=h2d;n*RN>f@PJDqiRNToUs`NWrh&*0M~cYegFUf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dee335a824c70707f9166688403a7832cc382193 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVMkr&tQT50)_XV{lzOJ;y13+mP5V~0?q6YVLMMLi`eoOSWJF`ceXK%^vO2E&_IS;&*s%WGPW5Ub_1#xA{R1DN`yXTs(Em)-@;|Zh z4+{p6e?WGF9RQ?>4S!tm@3_PB{xc)3C$5usXT7z}o>qA@{rhJH%PJp7)r{siV=Y+t zBim2S@MnalWJtuo(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*(0kH- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..93b7f283d860cecc52ed310bd2e15081aeaaa0fc GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3q15;zQ9ex{9gh!haF8RvGEUc z2FN^+-@yR@q=^lGT=DO?!}I<#BdsT{lXqvmwauPZc{Kg|X9deDA4k=U<~U<5nE#RO z2m2pAp`-Z=#wFx`MzpYmr4L4iGA4$SG7*OI#7w=i%#_r8z0ACl_}u)G%=|ob(W2C} Q%)C@yB(;fUh7cD30M9bgc>n+a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7a4bbdfc5c9e3add065dfeb8cc5b52f56baaac31 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsb(5&qOWzchqIhdZBe=y@$g= zsb_kwi%Txpv~N}G{uTBnbmHf(Uv}+i{s;Mo1r++|5e@SY8V&P*3Dg{RG4TA>jESM7OoX94F;lNBGbJ@&FEg(sJ~zK4Gd~Yqv?w(#GcT1FNo``8 HA;dfY)1K0N literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/magic.wsix b/wai/pack-conformance/mutations/index/magic.wsix new file mode 100644 index 0000000000000000000000000000000000000000..4975e932c774ef64fc1a94e884df1f00e1d5b58a GIT binary patch literal 1088 zcmazD_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*+78lw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9c6d2a9e168490fdeb4aca7d1621c65780a2c093 GIT binary patch literal 1088 zcmWF!_KaX;U}unIU|?VbVkRJF17cPvpOF9nDkBM)D4)oiKDjq>(zT3ftqfraNpYM~ zy1RFLZ(>>Zv+;Hz4=%%upb{)-3SiAXIoqWPY z%|*@D_5WLW#{`F+_X=k$LsAQ3{s#h37=SPcuoD0v^&l_}q8#i7C=*PH?#=sO{C7e1 z5%Z%@()@k;*3Ub$uj;@m$1tulQ%~P@>+nN%EL0;i&<`N{?*Jo#fc5MU+yDP}fSLny zH_Q#wld3zk{`Ij6G0N(=?%U%vzhcAsb34_mh17Rn(ew{|gl<2`7@+-3)Utm^UFNJ8 zS~u2vI2@FErsul28F#nf8&0$BA zCpP|J&H$MQ@;f*HfHbk;k1PHicX-}^W~BARb@J}4x3<~SDvzdr|EyqH<>RQD(Hv*2 z1@k|${nYe7BRnNT0tS{o7#Yf#7)r`S7|Ihf^~y3+QuFmP^Gf1#^Gh=G^Uy_$QqwZ? OQhAZoCYBjO%mV=PWYUBH literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/ok.wsix b/wai/pack-conformance/mutations/index/ok.wsix new file mode 100644 index 0000000000000000000000000000000000000000..6a58529b043411f3aecd6940494f0637cf0b10c6 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*)y~p= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e5cd23ed823c0eeae953083005f747bd009ec500 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF61F(7z>&LBPbu_evlj!1JJ=pY83D0O?Yjg@i-&E<;ljYlTWy)xv1H? z{(md)nBcJUUg3;oNNPdM|3Cl=0}uuQb^-vT9t5UAl!M&>Wr8Wuy?OtO|1PLLVt({V zn!iuq`gv#eRUKI67{+yG>gl^~9e&7;g=%C5`T=DB9bhC7u$~=a`~UwAP;+4JhPh#S zQgw&czdlwWMp+%#eS5s-S8P~+Zl`*+koxW`n*M>0(Cr5q1GJxsTK4a#%bfK>>&AKy zhl5hj^jsI0T(D{1s@DB0>`myz&t1Rl+R^+E@(&9r^wA?4<{>m1=Km6?IqYci#Ku3& z86fjOeg_8tkR~?#amBym4$u3~jI^G(PTrmM);4=u<mCXn&XVMVE#w8 zpPK$>gr{Ukz`)W6BSRSzLrIwkLwRDRURh>JYQA1(UP*jzeo1D29=d2zYFcJqDld}S J#4p3F(n|mU literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..84a0d23b7d712a1b2a66d8fbea9dfa44154dfd5a GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQmxAZB6!Iv7cf;@!LnuPrnlX9T!B*?4vG2^TdN zHCxyJZ{-~m9CqF-oUsf^Er|Ia2tZ)~!XUs-0D#nkz%+<*26Xp{?#=sO{C7e15%Z%@ z()@k;*3Ub$uj;@m$1tulQ%~P@>+nN%EYu8UpdUc?-vLGf0qfZzw*UX{05u2ZZm3ZV z)03(@wEp$63Ngy+xbEBIHNRrR`g1$gtA*5eU(xgre1vX4$QYpgOw_V}M_uNu7g{&g zdpI1FdZy>Pxa5LO`&PB?Utw=TCw}hwW!H}8e~^DzK%tKwkT4IS(J=p)K+R!ClP5O* zVa@=V2l6{O0Dv^H;g2i+9d~%%e`cih#C7uSthct=(<+aqfB&ptS>@xXn$a9*tOfHw zvi;QbKO;OPLjnetJ{TFwm>5dRL>S5wGxf?cQ&RKwGV@B}bMs3w^YhR}i&E1v^HO<{ L)Fzf0Ld*jI(pu7d literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bee1164ba4ce5a534caae6fc7c82a52d0caffee1 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x;4t?CUoNGu3vWTX#NNJhXoY+=n)O`5E>2he+kqab~Jfn z;~(Y>ka-}#g989a6C3`x;@@$H=ly3!T2EXj@6LK_n?0@aX!`fh3YJwqj;a~WamHFO z|0CN^P5(2(Q!*rAVCjRAp^S;4q)ddNJTX(REHfoFUoSJSBtAF4Br`t`U9>1QEi*5b M7fEemnIXhH0NGB`egFUf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..bcbb25b6690cfba4e5ecd4bad1e32c8e7768687e GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQhg@F@A2}~U1eik5RVgNcANrmFwya}%@G#+OJxIEc-b@B-p zH5WBo*Z*(j9TOaO-YcB33`s4B`5y>CVF1D)z)k>w)Puk@h;pzSpiD3&x;O8C@!tj2 zN6e2tN%QyVTR-p2zN!PO9K*QIOg(+qt-}x5u~3c7KtF)&zXOZ}0@kxbZ2$k?0csA+ z-7q&yPpalV#6O-{5$UOy#LHd>xt{+-C1vKv!_)aP5=H`!LrK7Q8lAE&R7fP ze`Ncq>3>FeN`?dsEPXIClrb@sl!-8uCuZuEWu~O&>t*JZ#OLOhWaj6gix#D(W#*;w MBB@O*GlZB20QJn$h5!Hn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d9e768261270719a4277b27beb23cbff96c2e551 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%2%hf*B1C8Cd#YWGG`|C@B+RC{N7PE6Yqt&DYD!D~Zp|FUic$Ll-SdP0P$n NwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtg;L&JxWp^S;4q)ddNJTX(REHfoFUoSJSBtAF4Br`t`U9>1QEi*5b M7fEemnIXg=0M;JTegFUf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..825be28cc61dd630420b34e2bf85c1e8900c6dee GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtg;K`mlrC}UzMDHCBRPt4RS%S=hl*UQW+iOwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGbX9j8n+RsES`*+l3&U&GB zW4(vNL8)hYu8T`9*tBm|>;4t?CUoNGu3vWTX#NNJhXoY+=n)O`5E>2he+kqab~Jfn z;~(Y>ka-}#g989a6C3`x;@@$H=ly3!T2EXj@6LK_n?0@aX!`fh3YJwqj;a~WamHFO z|0CN^P5(2(Q!*rAVCjRAp^S;4q)ddNJTX(REHfoFUoSJSBtAF4Br`t`U9>1QEi*5b M7fEemnIXhH0NO6ne*gdg literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7f9e747ba612d7e9d4fb94e6d77845636f887747 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGbPX=lP+RsES`*+l3&U&GB zW4(vNL8)hYu8T`9*tBm|>;4t?CUoNGu3vWTX#NNJhXoY+=n)O`5E>2he+kqab~Jfn z;~(Y>ka-}#g989a6C3`x;@@$H=ly3!T2EXj@6LK_n?0@aX!`fh3YJwqj;a~WamHFO z|0CN^P5(2(Q!*rAVCjRAp^S;4q)ddNJTX(REHfoFUoSJSBtAF4Br`t`U9>1QEi*5b M7fEemnIXhH0IWUI-v9sr literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/reserved.wsix b/wai/pack-conformance/mutations/index/reserved.wsix new file mode 100644 index 0000000000000000000000000000000000000000..211e8f1f405c380a3a9cfe7c5b4cbf1aec35f76d GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF64n226721FoJSfP=y&_;vn~fhn@EdXDmZf3u68U0#F!$FbJ>{03h`sFb$#{>;@=4`k|960z z19Lab4bzjVJGB1wu?jKD>bUOP<2Aox!}@bO)vJZncVE%;4}64fKgbxM{Y=!de@9*B ztQT50)_XV{lzOJ;y13+mP5V~0?q6YVLMMLi`eoOS=6{fXSU{nV9?>ulq0un^mq5*7 zN0TQu{$b7lnFsPaH~@e&vEh#^{vCIC-hXDK^~81Z?yR@A+0!bIrhoseU|HqksG89n zXRHPDKeGMQ^gkm!B|`!RmOdC6%9t2R%0w8-6EpS7GE-9X^)mBH;&by$GV}A$MT=6? QGV@Y-k<=!Z8A8kh0NHrbegFUf literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/role.wsix b/wai/pack-conformance/mutations/index/role.wsix new file mode 100644 index 0000000000000000000000000000000000000000..c9d84577e038279c9844fd6e7905188858aff484 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^z{J1~4hImQod5u-2Z3oI#Xx}WKGD5-|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}{=!N3gk11KKu03(5b_3RMa|NnP@ngh~@ z45lYlcWC|VV-;eQ)p6ao$7_DYhV|!ms#goC@4lkxANUB}evmOh`*U>8Z*8-uRUS?M{#n7Y%EwVPqdCr4 z3+8`h`>E-FMtDjFg$5*lgJ?#EGA4$SG7*OI#7w=i%#_r8z0ACl_}u)G%=|ob(W2C} R%)C@yB(;fUh9FZI7y#?^(u4p2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a2450f01e4ee323f70f097a75dc8ab60b0b38518 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^paG;o;Q#{c1OP}q2uy=02fG2v1XH4W^ZpnAT~K|* z{OFT3f1kef^Umz6IJF`ceXK%^vO2E&_IS;&*s%WGPW5Ub_1#xA{R1DN+Yd4ZXg?FR?B7wBIqQYi zjrAT52c@3rxh^ibVAH-;t@~Hlo6w1$yMEcVqxm1?9~MyPqenE%LufS2|0Pg!*wN&P zjenRkK<0t`4h{eyO>Fq%ihsu)p7);_X+3eBygTcyZT7Uvqv_v2D_BdhB79Gk}?s7^2AKNvdom!e7(%PlK9;GlFa-(bkU;Jw9LFz MUL>`NWrh&*08VAnqW}N^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4da7d9d6cbcae2613adbff0408af5858995f288e GIT binary patch literal 1080 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015+SGtiWva6#%pU>Z~u-F>2a^ZpnAT~K|*{OFT3 zf1kef^Umz6I?tHR%>N}&bJ&sOKulueALa~@ zc_6=oBLK}TY+RWAxZ>Y&hv)rgMp{o?C-2UBYnwf-@@V?^&kB}RK8~sx&2h$BF#jXl zPfhwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015+SGtiWva6#%pU>Z~u-F>2a^ZpnAT~K|*{OFT3 zf1kef^Umz6IbkT^{AMZ`4t=1pWCTkEu_Btil%?yBXs*g#sKYS0+|D{pPc|8WdDx3%vmqAZmjok zI4Jc@&vkLh1)KJ*YTduW-h@v4-1W<@9nJqB^H@Ns5IrD~J%z-E*wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3o!JVpb<^1|4X3eu){nKrHPGy zm@`23gZvH-03c1Q{kY=aafj#qXGU62Tqp0&dTX0Kt@3F4_s2O~on6GKUv2t#>dre0ZQN@~7dW?o5rZhlE- Wejd7LQEFOdUMeq=+Qc$LhwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015+Us5m%1=UB)k3LEB z_vu?d@65ic1FIavxXw&Heb=qS581I$Gnj#X0J-4~FcJt@&knKu|9=OlIUs$=V0uz@ zht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$dJl(# zQqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n&$$2PCqmkk~N)mq5*7N0I|EiH(1l zGeG8n{0Za@*bPu7m=fKa_rLh>g6bpY zN1vqm`}D1!cV=JJfmM!STxX`9zU$WEhwNCWMrNQNK=$7OMgjrr*&(+7|L*`b2j*^= z8>S~!cWC|VV-;eQ)p6ao$7_DYhV|!ms#goC@4lkxANUB}evmOh`lV#6O-{5$UOy#LHd>xt{+-C1vKv!_)aP5=H`!LrK7Q8lAE&R7fP ze`Ncq>3>FeN`?dsEPXIClrb@sl!-8uCuZuEWu~O&>t*JZ#OLOhWaj6gix#D(W#*;w MBB@O*GlZB20MVM#d;kCd literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/trailing.wsix b/wai/pack-conformance/mutations/index/trailing.wsix new file mode 100644 index 0000000000000000000000000000000000000000..9cd540c29ad4178e39200acc709f3687277e7854 GIT binary patch literal 1096 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U MlG?;FL)3r<0BRr7eEwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^#TA9 C&Svib literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/index/version.wsix b/wai/pack-conformance/mutations/index/version.wsix new file mode 100644 index 0000000000000000000000000000000000000000..0c4ffd4fde70d15eb31017669bb5872c2c023d8d GIT binary patch literal 1088 zcmWF!_KaX+U|^60Vn!eavDtu_70PEM0D#I!0w&5Q@}^JjO`LQsV_GXiSVB@9r&(>CcilStkR1!v$PDxY$o@OPNFZQ6JH+kIRam^=cvY-B&dI10SK=4>AU5KNGd=-%*!2>xI^h z^&SofrJm`zE-txX)4o-$`&ZbT(21YBe%ZC7`5)vT7EtJ;M>NbsXf(|KB~WwN(d3DZ zf0#2s=7IbU4gercZ204ff5#o3_n#SQJ#n49JL|1&_O!~Q>EAyqSXTKss%A9D8Ee7( zk8D3R{m%$b$&i47r4L4iGA4$SG7*OI#7w=i%#_r8z0ACl_}u)G%=|ob(W2C}%)C@y LB(;fUh7j`r+6vNs literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..844f5d7aa593489f563dcf523bb81fb7130065f9 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}(-G&46bJryik Sl$w^Am&%KzHnGeQVjcj~4$^4= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a898cbe190a812f69f3aea20778973e816f5852 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%byGEs0rN`F? zv-F(3dUCJ7=;A45=YNHMXK8Wo+IA!S(nSTBL!mUtKP;foM~`SUbucc>|0PiWv%@5y zG_mmyiw2N+AiskH07w%X{7xkCc;pjn5kEmnUb2Xmzh@*pPOHjnV*L)T9lfW PnU~6oq&Bh45Mmwxa@)}~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d33ce0e9ec8aed81fb09473dadc1e6f2e254060a GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3pfSBe1M;Z`M(5e4m+B1V&fm? z43K#szk>q+ND~|WxZ>Y&hv)rgMp{o?C-2UBYnwf-@@V?^&kB}RK8~sx&2h$BF#jXl z&jLz)=m{OoUob8q|1-iwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjO9pbLCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*+D_7c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..979f7479dfcef0321b88245fdb844dc71e1ad6ec GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0!wt#=Q=)tG{ulpUP<_Pw z=#w;mpT70;&g`o?u*xxv>&(>CcilStkR1!v$PDxY$o@OPNFZQ6JH+kIRam^=cvY-B&dI10SK=4>AU5KNGd=-%*!2>xI^h z^&SofrJm`zE-txX)4o-$`&ZbT(21YBe%ZC7`5)vT7EtJ;M>NbsXf(|KB~WwN(d3DZ zf0#2s=7IbU4gercZ204ff5#o3_n#SQJ#n49JL|1&_O!~Q>EAyqSXTKss%A9D8Ee7( zk8D3R{m%$b$&i47r4L4iGA4$SG7*OI#7w=i%#_r8z0ACl_}u)G%=|ob(W2C}%)C@y LB(;fUh7j`r*{;%m literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cf28149e745da8a678c2179361ab389859a16ac4 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2Rsdof`AF^Yi8kvE9 z0NH;B7zqTdXNTDS|Gxv&9GJUdZkV1_-J$ibk5!0KR>yVU9ydrvDk?DH#$lu=K&mP{zbiQYOMso|vgumYI^8 gua}uu5}%u2l9``}E?ShDmYJ8zi=;NO%n)K80AAeQ4FCWD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a58529b043411f3aecd6940494f0637cf0b10c6 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*)y~p= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..88c4adcd8be093e8ca3cfc94d6fe08dfe00ad9d8 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVW&@asMo2J-bP}^Lbumd>PNk^HdAU5KNGd=-%*!2>xI^h z^&SofrJm`zE-txX)4o-$`&ZbT(21YBe%ZC7`5)vT7EtJ;M>NbsXf(|KB~WwN(d3DZ zf0#2s=7IbU4gercZ204ff5#o3_n#SQJ#n49JL|1&_O!~Q>EAyqSXTKss%A9D8Ee7( zk8D3R{m%$b$&i47r4L4iGA4$SG7*OI#7w=i%#_r8z0ACl_}u)G%=|ob(W2C}%)C@y LB(;fUh7j`r$I{d| literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai b/wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai new file mode 100644 index 0000000000000000000000000000000000000000..917b58f633f07195137570c140319bfe88fb4b56 GIT binary patch literal 334 zcmb`Dv2MdK3`Es_3g^I(Y}u7PwPU`ZV7 z{Y9x2Zk}NPQ)$@2k&wNpDmU1EH^Dy6dd E1-+YNga7~l literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai b/wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai new file mode 100644 index 0000000000000000000000000000000000000000..1be50f23e074b9ea61a2eef7c2269be8fb3074db GIT binary patch literal 166 zcmW-bJr08~423iFB%V=JR<@2kK}Tc~3t3GZnwtJ7;u<|~4dZ+7(`Vl<*Z$|6Yw$*f z0eXLi4l+eXOD~R8EVC$yT2X0998hV8N~APadaPV2AA5s<3r?I^zebzV9|~`RF&E_` iuyqiMMBW3-3`2uEOEmnmZ;Ev$J0)vM+sQtg@JWAw>o*<% literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai b/wai/pack-conformance/mutations/objects/41b849dfe6322ae44593bb6aed3d6b9679c567f7f320397a4c412568835e332a.wai new file mode 100644 index 0000000000000000000000000000000000000000..928d19b4a7d9c22e1e91699366422a15df39b7f6 GIT binary patch literal 391 zcmbtQF%H5o3=9+h@C+4MnPB7vj8$k{WRconOaWE=9j7}J^5nbI`Oe4vA=)B@37&~y z3$fY23I@!Sd>bjF_)cud=78e0P?rp=NHUgI@%A9aVdf|3(lwo^kiA{)md$L@q%p#0 z1)XpvBVpn*Y$v!7XV<+dmr-fN-1aI%?T={Qt*y6&U8%^P1GQG& hMF;L^=3(y+bosxjB`xV0{gM1A<+BVS^hHG1@e4&Fh;IM@ literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai b/wai/pack-conformance/mutations/objects/455a643a675f7f53048ccdd5c9bd4f15a39476cffa55f70484478ab6d857d2d1.wai new file mode 100644 index 0000000000000000000000000000000000000000..c67a8adc762edd3766ff0aac63d75ff25ac798f8 GIT binary patch literal 32 icmZQzV30^FsVqp&O-p6~0uY^9oNoqXa^$8YmH+^N@d!Nt literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/97627b882afe8e0512011d2c45bebc4a9f78b0afceb9277b1227bbd4294f51e2.wai b/wai/pack-conformance/mutations/objects/97627b882afe8e0512011d2c45bebc4a9f78b0afceb9277b1227bbd4294f51e2.wai new file mode 100644 index 0000000000000000000000000000000000000000..e4814f03b8661828f80a15266e2de3fabd0d525c GIT binary patch literal 334 zcmb`DL2kk@5Jl4-W!5N4z;QxW?Xo9eS&heIG^%k@W1uMF?w!ypbou{0J$=pL{iA(* zsj3BTC@?}>H?V;ddqGWC@jPjkR5%qaGS5*63$M`RIeupTzDVZN^S%J#j83AIyV9zx zpNd)qN(M;`j}6e2ycC&|q$7+Ad`a?(|E#w^>b%wt&#ZOZId3_HeiuA8y(6>hsbgcU z35Ev7uxBF2JyL^c+RipewC~7yZ|pGW)H(C@e@qeYoMGJR(t`QO`jm8u@$p(!Ro?yr D(^qJ+ literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai b/wai/pack-conformance/mutations/objects/b87e699ae82ad8af4840c11ae62e45a374d03c87b5268bfa56ec5591f38afa3e.wai new file mode 100644 index 0000000000000000000000000000000000000000..0ed3a0c19547e7feeab889c2fa4be580be2154c0 GIT binary patch literal 391 zcmbtQF%H5&3_B+Nkr^twc7l-?Fjhw|MG@BoLMWiBe<#60Xp1$V&^-aAz zRn-RH$S{Gb8#us;0;8lW2U;X^DwK*8nbyc23+>PnD1K!6x^Sk` literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/fae6cf7e7d39c8a77e06f86929a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai b/wai/pack-conformance/mutations/objects/fae6cf7e7d39c8a77e06f86929a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai new file mode 100644 index 0000000000000000000000000000000000000000..e4660e9d10b7e3f2fcba40ed8e07558b16e7fe6c GIT binary patch literal 166 zcmW-bJr08~423iFB%V=JR<@2kK}Td_3t3GZnuh);;u<|~4dZ+7(`Vl<*Z$|6Yw$*a z0eXLi4ib}~rI!$A%c7OFT2Xk360q_PW#Tv%eymbB9eaa-3r-THevLMzKNQ{sW6D*s i!q!2^O0oyh8HNUTQ8N6qZxzeTc1qV6x08J~;gkM=>Ngz# literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/receipt/counts.json b/wai/pack-conformance/mutations/receipt/counts.json new file mode 100644 index 000000000..b4fc3c69f --- /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":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"26ba2dde0421e51269a65b373dde28ee0f4753bc1f1fd7d13982d9efbbde2a44","sig":"fe179e3d07f1297ae9469a0015aa58f18629ade88231d215f1ed5e66798fa9abb8e9fa3e557b01f93e4139aa9c9bceb36bcf70c1c8a7dad0c584eddf54917e06","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1606,"total_bytes":1782,"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/energy.json b/wai/pack-conformance/mutations/receipt/energy.json new file mode 100644 index 000000000..3f1e925d8 --- /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":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"559b8d960c48c96f09a2593768ce9a0f8fe55a230477ac8810e26f1e6bee5dcd","sig":"8f9cbc3beb0cf4cd7250a7465ccd0441c59fc82e558751e5e95974bd0988df6c659045f2a61295e340f90ff094153833a3844e7a51cb45223a6e0954a1b79106","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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..491bd1150 --- /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":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"8536a656c2ce069e3f67e54534ba20b73724c52b399b6f36dbabdec22da2eba4","sig":"56e1429c1083bf9187c3b7f5b88e8033091320f139c90fa55d14f2a762765dd9b5245a09d5d9e7143a6989249db9e4d8e18926cc32c7fc2acdce5555481cbf02","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 0000000000000000000000000000000000000000..6a58529b043411f3aecd6940494f0637cf0b10c6 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANQp@8(T-ZK3fvBf#a!#;cQ0xTv|P z*}DFJEAN=#u=8HwjAck_LCpU^015*T1_5>g0HhuSra_d0-2i2RDbc-o|BL@Fs6JwT z^huh(Pv81^XZBScSmhYTb!O`6yKWtR$c}|-WCr>HWd9vtBoMHk9b)_c{|-=dVD5&w zVR}+^ht|J7Rv|`N9oKz(yyjPISbuJ(dbN=H?kk%9fsfGb2N?sjpNU%b@2Jb1^+M~$ zdJl(#QqS~U7nfYHY2T{W{VVKE=)})mzwFx4{15UE3n=u_BO2x*G#cjr5~w-sX!69y zKg<~*^FV$F2LO;JHvDnLzvB+i`_GKDp14llo%Pl>ds^ku^zWY)EUSDRRWq97jJ06? zN4B4u{%3@zWJtik(g!0$852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*JXi;ifW?m{U LlG?;FLx_0*)y~p= literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1601279d28b53c30c5a4b24b60f8ced4c24ade1 GIT binary patch literal 1088 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 + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..7e5a29203e5ad8907861e9d0bba3c074652aaabe GIT binary patch literal 981 zcmZQzU~I|F&rf4uU=YYn%SZvzAi%`Hz?f4~ln53t0f_@efPfK*MM~0AN`ZD>)ul4GzYNU-fRG!?;WVklSNXUqZ zp$V!GsKJ4u>kCkfnUTc-q8TFA02E_kV(UN>Yi}?TG-74pxS#;G1gx$VD8$CWE#ZO? zY61$e3-BgnG=rQ1Rs|B`VDMkji%?Z#q+-O$(EMQ|Se5GmpgS%J)G54YWIT4KVrj#d z%2VghNAk!6HF7bsR9r)7%mE5<10D7YVLV8P2k0()gsKdn5HHYO?g&*NAwE=hfrR)` z-IW3Kx4;9KyTGoW^T9~VNRW~F(*~gV0uBr!z*Ng{Wj&M1)4RscZ2gZ{3+?Z?FM9X> z1Gz+-I98y3Attt}D{!?e3=u#vVHS>4Kaj)%fMOyX+)_4ZVxj`PNhxSzVhsMPdeFqg SQQX7A5MU&1B!On{J_i6~LX$%P literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3e21af5bfbf09877f851ca6abe777d4380b6af59 GIT binary patch literal 981 zcmZQzU~I|F&rf4uU=YYn%SZvzAi%=Fz?f4~ln53t0f{q##TkL5NJ&~s35Wp&?Lf5- zB}Juq3=HfbVM!pr2S|h1#t>~_wk9J)6NnTcf-Q`sh8VUQiRy_l6WN_QNaiDpQ^FR7 zn8_5Fo03?<#RUu*ru83Z#~W}MsfsWtJ1{Ub0ELVsY`6?w#U^-VKiU5G+e>L?^@sQ7 z%Ffa;+NNNnZp8QisEj!S7~ZZ8j1COvpWLszzD)k{YrQ(F8foJVl_z&I8Lka75;9_9 zXo4yPYH(oa`T`VVW@K@IXoiS20L56C*gBBJ+8c}njaXSYE+~L40jp~T3bAo;OSm9} znt(#=0=x+s%^;_MRe^*!82nfCB2?8FsTgrGG=JC#R^@sC=#EPQbqenp8IRqmSlaNV z^3?hBkv#H1ja-Z@71t0NbAUqJK!^Q87!MNS0lLc`p(+C?#0zwnJ35N03S2DSG@1F;!I7$KTKq!JPvSzH-OTo{Qhjl?EKTpovdSsdaj zIK<_M5Jy%ci9hQl0O;eg9t^&G831-aG8%wy#kV%$aZ0I58H;^ zl*AG)E?_ERTK{o&ya9*NR2Bh-*+4Eg1JhkSsZ(8N8l=i*ER)^%Ke~)_P5uS1Zn4&` zIcJNyKE0EFs%8Jm*Fd&OJ9XEL`faTNexYicUhLQ(Yn!p43}x zZr*LnxJ=OW=tqw|A66x`-zYCW60gMYl;?7Ko7Bv=aVPVOO?9doL)3Sjd%y2RAs>g7 zzhb5Na+7s4b0*p~)O==JU|Rhk*GoidLz`%;gv$3ES!wxgOQ!hSZ~mje`r*NfRmVSN zsPbvJ{16DLIlX7%^{>Y*t_2u>US+&3`jBhx+`TR9lCI?LI-DStcsN=2!s7)Gr4t`V zAKx%H%EGNMR%T*1m*-v6i{+1QALJ5{n6TaIU;0Cx2MfMEUYQ!&<5K$jg_QZX$BP~` z@4fohS=-_A<3PWRX{iUka&;-Q{+(MMJEh&VKCm|ShSrB&;%lxbbWOHqE@MpGDyeH` z>Hp?$*}^EZuKTa5RWA9@YO8zyGfZ33?b6?MhTh9;e%7SH8izH-NOsp}IN3NX9_% zAc|XWyE*GK9^_`c`|@2EP=gW^`_lx3J#9b@Dl9x-SAd1OL9ujCa{FZ&Lx~-mP3J= z!h?n9FOm@lwHO>sTc3sk6?<_A&P6gw0Vw2yYM+9Un1LUveVg9$CoXXS#zp|DRqJo{ z3YwXNf-MNuLIt3kL*O=bgR=6n*yK76>k}z%$EO3;hoRdkZV-WL=N8HRdy5l5p&W&3 zse+N5K@8kFa4O%_q`g8>b9UPDr*$8WMF6$L!R-d81W=G9CZL4s>4P7`X1&b^xjl(t Ln*9Vs_%#6l=bT(r literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a246e72beae3b37d64c24c831a41f58e23f0cb74 GIT binary patch literal 2605 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0b3ade2588318c8374e2813c379f92328998bc05 GIT binary patch literal 628 zcmaJ(|3il#OZDrXHF*9Xt4iVyflo2Lr+O&i_bGHW)? zR{&~y95}iJXIX&p%jQpQGF?B~?~xj%c7FFx1BxQB@bmZMan=XA(#B}71zfv-;*cAH bzj}3s&}4iCt=&kQ??7UsT;x3O>uhg-8t^^c literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a5370aa2a0b23a021ff61b05f239f4e538509921 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurfXl`tq~_~o=9R?f=9gsV=kX$mCYBk3tN{RUG?8rp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b15b5310886cf0f3076b32c2b77264e58fd7b1d0 GIT binary patch literal 39826 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

YmH;T^6RA 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 U?L5P-W6PyzT3Xh={;8MwKYBvv82|tP literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..04de3f1885449c337a118064bc77123ef3814311 GIT binary patch literal 25514 zcmaI62{cw;^f!KoXC8!75s5OCDJ4Y`A*76z%$YJo#t0FjGK2_;R5Z|J2$jf~$Pguw zq(tUqc;?~$uXnxH>i=E8_h+rM&OK-Ev(LHr?BTQb!&=u^wF&@$2L;4qXM6{-AxL@1*IoP7O)z5LEdM=DCY`kis}S{`_v@sQSxkUs5o#tmiv z|1$+U2YR@pdqzl~3Uu~$50chY+pZ|>DXqCvT~RtzT2oyuGIGKN0IZFVtAZc^$fIwu z1#LQL6a2r8h9HO#`n>!HqYaBT9NK)*MnKmTqD>nDfC01xqYMHV08keIfO|^-V2Ysg zXhYBj04U@CZ4kgimfOl0L^+n*%J{$iGHsb3L6%Pd0?W@U{3~-SYgXo0`v1=Vmw$!6 zB7bGC<^F%-T{-uE`<4CvyJqFAEAm(9E9;l{UzuBeUfKU&`tsazTju+hcZGkYU#6|> zv&_FD``=3kNfFaN*tSI)Y^w<7O<<6q`kk-seK-+h*8|C9V> zc`NhF&;O2B_EfY30CxauIr>N#?df)rfOGj7V9R_D5UZXChVG36ckLVoAD`I?K|m+S z$To$XP8UN8dxT(e#c9|hc?j~9T!axK4Zd8fi8}@0=IE|>nI$n5h#IjC3Al6}Nvv}K z#@#1?^>kn0z8oK5o7jV+$8td@gGDfFtPI>6UI)4uoP!q3k3bImDd^F43_O!J0p(go zzyT*C;iQ*qsJFkzbdTjka%)d}BKvp0M(pt;$b*^*z|ib95GG;>J~I*m-IMo#8Dui# zqay)5Gm3!z{tAH(5>7#9mjq$SZbdk*jfe^FDS&Hz>F~8ZhgdxG0L``LHqyK0G7^2}E3#`11(@{fXU5r z;Px2~SX%i6T&!h+Id$F88woq8K5;cHa^O3pc}E=<_Kbr){2s!G*FC{oO-~aZ9m8^^ zHSZ!uS0a&7${4aFW)38n;X$9L6~MufD1gcl1%F16!9TyGA*&%7C_-c%g!R>fn%$bA z*Lr%eLGFGyw{9DJ<`axb+%p3&NEj1)lAIMlySGd2)ddLc85cPaCJTt(Tnl_}@I~)G z0?-V%0P%*?;NZI9_G= zRcuwc9Q zJn2bVJz>QzzL$mE-tY+N@g79pPfGzk!3IFTpFi;EOfTS;Y>%EZ42+x$MDOVb7;t$F z)a7&p`V|odg?4p7IhMbnu&w~ucFQn~Bt*f%c9rmIwyV+=pQ z+>~_qZ8vfdK4KGY8^$zkzReX@I%^V_=%N{0;O#nRz8tW+%9NXC1@}uz}}7Ea1J=!|*%p z6xemC41R)hz!W#~$d{thc$-^1k@8wQ)a&dADpj>myPrno4FC_pC;-Sf3!1@K!O1)} z*yThArPmQ4^DG1CgHsZ8{E!c+ouyc8cYRHFSRgDK-O4L!QXC zAvtlk&~Hlw`dSVHUs~pYCq^9f{#C&X_IcpQ#6i?fw?a1+F_5yb9dy;&8BUB8g?HIJ zf)8ikf@^2{VD|dglt13SoZ-uHh_(NFWQSrrYE!0wk&HUvT9*Vww?(7&WCiZ95e47J z*MNKOuu>B{joP{3~+#DP>&tk#F2}7wb@87l&95d_}%=s{xlDoB(9TQ-QYld{mb-z+lvb zTi0Jh?_L8sAG--UcBld3nG{0@ln+5Mb2#{v<(({L)TZ(I8OSl<+&TWAg%Y=wiCf`|Qc~!{ChI%AK;wut%RR%EMqzWAFng&E& z(}5G$wu0EN!eHRiHZZ*C0ce)>2GnT?flSO2AlohGaHI$h?phE>@9I136gCcPD~MvN zRi9HPnK3-8!#eDN^>&Eh+gaq9)Ed-}>Hr7NUjRlXFkqmwBKRipB)GUu0~}H#Lyyl0 zKyH$^p!WD9sNc(ivd#@dFS%HF%er(}s(+Cb|2S{2QKvtW=lmMI{})L8eO(~jW*0!< zv;(Ga6;N1x2Z)-B0e7}+0EY$mV0&1-$DH0v5KN2f8|J!2JzM;9qkA)czHq z9t{knHU1V_#YuxK;0ow)&2}`7h`^!FE%2uPU^r(>8+<&S#_mrKM2=?!qjv9%418Zg zvc4Dq&(tRY&G80+!g-J0lO))&pbs{_SV_MK?(R*+vB!s4 zOu-^#v%Vdo{`&%AHQ)p=F>S!6b$-AF`#Zo(J!3TH8-OqRNKifwhRm|=gHw6eAfjP2 zWOr>h8dIF$@naJZEZc_q>^AuQfI88iL?z4LCUSokH6vRGucP+BLSA3eKy~&I5PLKq z{SK3;Txak`9TmkZM^N8FL9vP?B;UU5Tm7NOpf}9I>_u_Fe)Qg&;R6qTp>nrini~7a zmxZ=71mmlay^#yZXypdLtC|E{J9HDp6z_pNTnq^N>VoHEh0)kj1meDSfsUthAldmW zNX;e?D)=D)Cr^z-x>CJfi;^`mj%9sJ(r z8?0%picM~6;D_`*AgEf$x;~y|D2Zp}i z^+0jOaR`*vguPfn=)2ECy&8An^k;*xTo<0w%KnMH5M9ox9gaZs<_nR7kEKwoDg~4c z{|4TzUj%lvqyTHhO~KPTVyKV7LA|RdQ2&KN-~R*Dx$p>T(Wb(OMm*tP5uvb|@O3o4 z&ZB-t0DI~OOn1KPfy%szWC&`Y_WBa2V{`)EI`>gM*oXSp9I(TQ3Y~e1hY%SKc(pkj z)u}uvP4zGw;%oqmB~W4Q@M-w+zE~8iZYKn$ZNS?e-$&n+mWq6RosNu6jv(i#L_k5q z4M<|%1+*K+fDanK06YFR@ReQ^sC+>M5~btO@9-S@sBsXI>~2K;mMRR&PD8s}lHlW4 z{-80T0j7iixIUgJD`+nkkvLg_;-DoY!+HWeXF7^iLV=EjAW%oy1C=)iDhuBSnV4QM zJ--6lKz|BRQx;L%l7hRPn&IPwBsg;`8VktlFntble07c)>(`5Jgs6N9Ay_OS2UzP+ zn>_?Ltp!20$yXF#Wuh2(H~2hN11!4U2C@Ri(Olp%iY@w}no=D|X5BcnXwHUq&Q!t$ znYpN5eJ9lVe)7WkO|2;6%p1Kzv+85vOKI}4zReu4A*8QjrtOm{DSWs%6Jmhui7Z@4a2)apzqyDfFii%}I#d6;8Ck;i^ zZdqtN{0ZB@r>Vl90|)|z+i3z*E-1cwf#OyJpfPGE&=&R-^()^21z`)2W0C?Y-k$N_I#o&dOc9z-#F6%Y}d3>>OyMlm}U#Q>9_ zL^lXE_ZUK9doMw2O9W6qWsS--hCAb%;P9R_I8SmTQSH=u+@Z`z%!^GY5vS4nC>97t zI0k0GDWT0k!Q?qWW#$1uPO$_bz!TiK&lkmOTOrNxSD+>}0s5|X8TErM&^KES^!b4! zYFFiO?rjRD#+QKEX?2I1kjP^?MZZS5a?Kd`sjFI=xz|| z2h%~;cWF>pc?|qD@futwUJ6Qf`#=qe-=K6=X*6G(ha(RcqL}R&KIyLlA?&pfS726` zUnKigzb~<0=~=>2;-GxpWh%Pp43Ox(#LI zuS0#RJzQ$E2>bsiL(e$}f9+Mk^}0RdbLxwc2dSr#(jW7PLz5hkvFHTc{F8^`m1{up znPJpsSEKl12bx<5LjGctD0b;WV~H6g@8}C1sZoOa9wegvX50WOwY8jWq zvd}yz6u}4Y1?Eobpf!sCK=?osaQDbPAUDSejsGMxK9zwy6GbR5=N)+Bg&m~0rxMZ% z-wlrlc%pbk2!>@V&|GaC?l!xQDPKG&m1JGT9LR4$PS2e}B)VIWYJn}NPuT%5?CJpN zxFrDcy$v48BBB0I2dqGSmHN&Xs2%u1o7dfjWa4f?g+GGe?6G8+E0BWffE!N4R14YA z)sE^VK(U^sWb&mSNeXCh;%lCc6&08kd zQ=3lk5=$zP%FsyU5_S%Ge|j%kQ=kL*iU?rq9zKBix}2|ULNUbz*xfM&CQkK&H!Pn( zlPaH~H3#OPpLaoc&rTKi{bM3(_o1j=?Z)=T`4etAe@1b2HS+vs4Pr4cjnv}oflm^` zK&Nypphsv$b4z8^rYzC;ua4F}T0v696Etu8fcohiv}Oatyziq>OV1%VKr9??cFjQb z^*f=u`5j9x)&tp;_!L>E!A1&dEP)G;RZvXz0gcz)Kx_Q6?Wcj4BVT~B^KuZ^qz+u4 z@D9z{(;!lX3R?R)hSpS$!vZPKQC;PtEdggD_M7_m;%=_ZrT|oKI~uFI&^Sm0&XuYG zrlI3#UQI)D!PQ`oh86hKXa*c_oJPOFTNGm-MdRN|v<9FFV}U~`{y7X^E*yoAUCDw~ z>tdkBQQ z22};J*>VW2gJeNh1oxr(`V%S$(uJdAV^Q2bhRT+w#w)yfPo9rO^EEqE?ki;2vJ$BZ zl}E9A2XIyO9w2n!EMVcUkNRqDQ22lfbZ&1I*rL4z)|MJV?0}1q)FT~OPuUf{vneRP zEesAlUW8(IHH_;!!qU)43^TX>GxA51fy6JYL;cliVE3f~;Ey>M;JmB?4&<7FA3C;! z%m6l;XT1Q=i?l$oZMRTAm=7u4mqca$giLGZ;A4b%m>BT_R@R5eKi0hC#qRS(^R^2} zs00I%E3pCYr5^$0)BI3uISC-?Hvw96SDE?ErjhU4g@(SqOocTy{~FuI>a2s zffP{t=qvDrR|j}m5QZ+cMWNrI1I>%~!>$J3q1!niEVFJFeiirt4!3wfRiOD2Y;rA8 zJFr7!rMi)-qYNa|+8gktNCGW#y+CbZG8zl~K|%!?jRU(Ohtp%A%@z%4+n!fw?Lr^T zi4MZmeX6KGl!g1FpTdJWaVTzmj1SmnL9X6X#JK7nfN%}IBJHOgP``Bo@DPdsN~LpA zKNybU>s6q_{hy%W**S27>msUeMX1k8f&#>>;rtvbyymkV+!5ab3(mL03%6Pc`?o8S zZTr$_>)|M5^@TI&`}2_v@2%0iFcXLu^hIOLEShVXg1;L>(0uDFTEo}`O%o}oZ#jVa zgL<@fDTbca4ZeS55L)Y zfab3zXgrVr+tzo1XP$0`Wadw!_f(4dEjr};35VX*E)-`l;MFsK(A-Umq*o$Ocs>9! z{-%CFbu}9Gg)_+A8U<9Z4ggaE*MT|nX~6T75%^a7E|{8s28@~$hE65pptH{J(O6)G z)-InxhJ;OMy-66hzg7oN%Qd4qkOE5t)Zzsv_wXC5251v0Z;+{ICeqj42K=00pta>} z;Nt@~6k8q!59EGCYYYm|*s&V0_dyd>#eIwVn1g73H3rp72%-0116MJMVCCj??9Myc z=Ly?sygptJiUq!)`Gpn$7jFe#e*FrhpDP0bmXy&P(-QTuTA)jLBk0Mh1uqq*p}w^p zt>0BamM{r^Qz{ATD}O=nsT<7)Zoxa76^L7`rx@eN8KkrHChE6dq4;7N8Lt#UvEVwO zzhy0or43NObqB2p?}ds%3Y4n)9E~k*C~h%=Jd^EEtg4LWLuqIpcn(%*dkdcmt0Y~~ z59SE4+R@s2H&QdrNAA=J0t>|LfZ_R9z;5a^iUUr8;UZ>e9Jqnn>l@T>wV}1v$Iz{u zcqoJX6FOgHhSr*>Ff%&?#aDH(@7N+H;-@$H=Bq?bQ1fwS+`(ta!qP89FH8XV(7pjM z=_&!%CO{zfMM!pk**1VI2cP_BYchJA!{CD)MF zM5w)cb@K;f*H9di>--RXcRI3rjW-}M(+5}#-9~F7et?d$B>MY;4|t9I9Q~$GP>d~s z=5Eoby%s{-FVLaQkK|$X76th6^}lGHxgKtEJ&t`O5=R(${g$g)eFOcbM^Wth7g@aN z2|&e%0lVf3Ak#7g#jc?!9wahMC$F+4z8ZF%9`2pVJbsLE<(f#wbHl%k2WJj(H{wj% zi*v1+31ZhsWgAC7>iTT>`?C?pFH{$xR~)2lSV(z7m0Aq)7Iy5Vce;$0w(WU4-tJd$j4Y!GX68dro_YIf+)SA>A!2&p>fkXq?z-ez z=A7kyy1LIr%*{(~fd_6m;vXIz#&(ABc507}uzMav?CBK7t|_?gl-{eVfN0kCYl>y$ z=bwqcLVr=|j$^B*XtMo6s3Fpi)q;;<7R6$Y6K6+76?a>Hk$S#Q73+UyH7ou_-PCY! zErB-D=_@#^$j~7Sk*;TRXtEyXI8F(_HQ%jg%U+TFz)b2}Q#4A?!3lob^|evhIF0(iJNj_=MUGIu?a_0u>L4+Ru>esM2;mkHW(I!phvo~K$QZ1v4 z=-9nQJpb&fYJ+n-qC<@I>6w;}#BsXCq_L46w`Jya0^@pJjoow(A$3meVG{O$gsq9% z_9Q;9d;jX_dnNW}Q#SE|{r>S)R8f8;N!l~M)RbICYE!zWyu19QW6_#a5`jPWs=5qe z%|+&RTwWC;7|KvDz{&N~&GBb4<*r3>0$;vlm#ZeqpKV^inuX;=eKA?H9UU8tN|M=4#OVwXO5mWek2XBw4aY7!(vYrc2fq`*%)VuQDkuO6@8 zdg^lZw~TG1z9Rh4swqc5BrWTCYHcJ|Uz3BCoG{H9@XWlDqZJt88oGLq@aOWbPUjZI0i?`z-4V zY!uzWND#i6s$O@uV_ZkfEqkL5 zUgG-4!KFrlYo+EhjP*w>SQ5WOC?OE>8;RmyTptHuYnLqzBlfuo= zn10V{m2?m-RF_K(mvX@zdMLyPrOfBEQncBP+na=olf7eeuI}E)YrtFd3bAkrH`cG8c#AW6P-lAJ?`_V+ zwK>i}#_rA6Tva@&4_1?Ab#|T_%_w-9y&+3D_@=^XWdTphi}m$~|MX^KLgqHnMh~?T z>^U2o$)77DcQ&|C<>~rt>D@_N)y&V|RQaV>S-tl;FHn2_mkhWLd8IhUB|Tf0d)BBu z{6e+`qtpTrzFVHj_@%QzSWMv>8)bJIN?K{sET3K@x9_CWm?X>Kp6T5RuGSNy&r-X- z>4%PPcM5%s8E&d#zI+xyePEn|v%6rbj^x0%z%_?TP2)W2$7f03hA%we-rUQ3B+Rxm%0 z3~l_`co@IhGpC_{^|qVzTe4gI55L7GGpkvq9ag+r&tOvE-}uYN)=x4YzvVv3I;+3V z@1mW4sP!Dz^4q=Hd95>)1Lo;GVyw#g-GCz@irTDjU@Y!q#%~wCA0dKS(Ej>OP3a+e zmj|g;C17Kwl|if5;ce*>r`8Lx+)oWLN_L)&-QwrZ&tyq*0y!o{qXTxV6po z3XNJ_TL2slieeYTL=*B&#^T1oC5$NpKO;VH(eV^_K)wP!`?GYBX>Y>z`eP$-i%?5gANMso7b@eOIBx6IXGv%^0rG z7Z~@tsrj16DiX0JX+P={N;$c<%>8INcmK~rfdaf^Wlkl|6gBfV0r82CBzAf;CytL(wzG?~+r@glnL2bOo3QgvnB|`n zOT5W*M6$&WmG7@NsN$co9wIl}x>5F$F9r8Ja?*-a zxXkr_*hAg$;0F0Y0`Rt_!(g57bh)BN{yLHo z5h*2aOgcGcp>{`0J?R@(dulVz?hpTEPW4#xD7TPkGaz?k3}`>2Amzifet4Ub{nW5R z;NVaEW%6Nmq-!O=n7zWgwqrbxlkuX;v|>ujPYo^a z+B^;-k}&z^9PsbW=u4Xh60b!UH{K1&SNqa}m;$j?kK z%g6S#>+fiq_+rlPB13sjdTBHxEb46aS==bI?nh=s`VVbZcFaEN!}FPO#dHrJ zW2=v`RZ=>DXDrA!v^1clkuA^&S+C$H*V|YE$p$N1pcaIdOsF&d)7@ANZR~n>4@lOk0Rr1 zwDeAoMo7{RN z;^s$Lzuc*FFE*#P+B5{@a|YiQjZ)Ia38G3iHC~3Q%*?M!&oXXKJq#;wpzRUu#yo9* z6ZWRu0KZS+j?C33b4w=(;P%bCtX?&l2QbzySyQsZN5~mcm264=jht)yuMpN1F$1^8J26t~wJ-Enr*HfqcnQ|ms(;Km?)JNRy>+d?amc1j)P%Bv_F)5tpp31mOWflx6@yl}_#fH^+H!^G!n+C9o z`$yk|`-X9qg*Nhzl*{(#Un{vjnsb-3#XyF2%cBc#exbl)=64fs-_Iq!aJfbAr;Vn-fPBwYHnMh!3*AnRG8bo1Z*(Hf`cky=r^b zL{aja01-nniy->Pu~_sI@=%VxWBZtBxx+g8zQLa&8j2M*=Xdcj{O4FiX53EiCBaMc zPy8XBXE$Dug;u4tg_Xup9TSzhcfPXYZOU@;ki6CS2@^BD%g-tm8}Fq$twR=ApCeMl zlovb5!5oby)jGUBH@@+*-KX{Pyh?|gIG))ff{eqizd`T%t|rGn=5wets%}r5h`GNn z&i;Ha2GmyGDlq`D=#$$>4k;vov4tBLQQs_|#=p5(1=%T_mtxT7;P0=}c4x;BMb2Xh zwS;`?5g?@ zbR|iVOM!xH_-iZey+R6SVY~k=?0IvGnLR?>&f4L|iB=xwYhUm=Yi+!;tHMdT!1%(p z%sEZ&;g*?M_A=G#Z#ZU@Ayzht24p03?Ysh?+j@8d;aZm1h^Z=6xJnm;W?uJA43jokuSy`T8h z^nol=pWOrOvn4Hx!xG6x6P8*-QbH0)%{P0Js#sdFpK228(`)7py zrZ!ETR>42=!~(Z4m7f_>J;Z3oe@1?P3(5L1NN)#so{$i6m=9^kTkKXM4F4=+^?yIX zTUfV&%X8Ej+4A5(;rcuJ<)+5l5c8bkbvs`F;V)SyFy&%5d>{_j(^)whyq8YU6@`ig zj&{h7j)m?K){8KE7Ja6zP1HsUhnZ>TKbZ|>Z9d`0x3}Lpq&j-%r0JN;s^|kov$qYR zUe5_XSxQ$r$qv4ue^-3-fWLOxK^o03VElnB6m>CCw)9cei^AqVM@R{zzl_3div>SZ z?wwP~YG&nS-yi#9e~uzK_IHnX#lwMbZ`%2#zQ1czxIw2azU^+y-T3f8&H0Dqlgd-_ zH=Fv>vvmZ;_r5&;GH6@Yi4(p14#s&1VD9^Uy%}~d5Oen1?*|8Xf~{>iJ2irMMi#{~ zo$h#%dcq#dwTHF_9sep`dXyi1@?M5&Tmr7tRhZ>ka|OG`=;$bL_Gu9u(D_v`H&VO@mu{ZW7_N?loJu^m9)ESFyErrg} zw|0ElO*s3_jeqPyRuZ%Cv~Oo;UIFj>f(dmxYTUK%+k!xSxDL;rLK4{H zxfhp6isZTvjoWl3ld2eBU|tr>pIO>{hEbm$LHr^}{BrKb&eEcuEsPgJ1LwxkeL>UMTFl8?75w#7*G zE86JnlOUaIRoZ*<@I~3?FLVqq-H4zZTft7;T*W-vWz1B$X~@2A@)6fjHT;#}@B8D2pFAaxd|wLk z+{O&pG{>61cjbHHw?DYr872CIk~eC99hx7Ov~(^>AI%E0)7eN@pRCtl%jlTgD(PC*i>V7FkU zJU=y1bu*?ouJ#1(%{eMz(C@~o^T(^1jUJ1E-5MnF{1b&3s)Aq1cuiUITCok0A<`3Q zZDtGhioQnVkyS<I+Y)H-yP}_gTG%4&Fhy zzdja}!&X{rc(RMH>VDCm^cc?^&{HE73ZgTY7{jJn(mtf)_oRC8tr|bp#Ju{* ztaR>Mb8kxihO_;t;I^&0r2NSt*2uGkgEwU#X~q5d6MM0W$SQ1jC_MQyj&j@a23ha^ zy-WT-sajdu3y*sY7-a@t&E&d1LF&Z1M^^pJX`TTwk-qvx1Xp$u-!P-+t@FNnJ#Epm z36p#Hc4S)P;+C55rzE?rA;kyw86U{HKgaNy=96Dl4>6N;`{?_|pW0*eYwMHn8oWSd z0_k0-efQ;A7UdykicpGA=DVZ+Go|=xAsjz_l43>vq*L;GNAzItRi%*jw>JqnAu}f< z_EwfQkw^#CLi}~wdK?K`^Oa&t_tDN9ttS7O{?4nt=R}yrES+vyNY$!nAh8WRA!pUi zlY|ED1nE&xqSi?CLsgrY3nY^gd{l{K`p5cjacR(NRaVDIJNmSC2W~h~Gv848)y$s3 zjc0>8#H1>cju8-1VPOyVF{Z!B8{UJZt1&{)4r9tf1z~ZWQRbX=LEK))ra=9 zt;0iaFR(_2+-B3r z56SCJ;M;w7oW67IE#Rt0poO~o*`#y>}n;fzx($c;UUqy+K`7E zzjI9L)B=7dj4{-IzoI&xy*QE~A3+)@me!WWthbNam6>gsHA=Wz&!VYDY@mz=?W$9= znk6ULZ$@iZV(jjsx!-HwMbO-+BlqbV(&S6(bFc8u&(hiDCq9)rHSR;6$K4}x zH4Yiz?g7*VV#}*KeSxnHGrVL|OLy|nwI?oDFNiJtt?@j6#m3g`2L0n>d&j+#{5yU; zQuTRk-;m;)bxWVjS;KVz=c6zruCMfa4I?B7F<9laI z;THJsE%x8{wui6!R0E5%Y{G3P;C}9>ikyp6+JR3$?R>X0X~swC zo@9?~6s4?jh(^3|Rpvnp>DVd%8xHtf&SvJ$AJ6w_24eaXL@D_(`k8@|}Tx!Sl)pmR;^w>zy#K>_6 zTR)!H1Mirh^fPg}+Fs6*64kOSJVn`fa3&rrdk4o{pECKw^@#(8u&}M|ENxV+G4eUt z+(>2b)2Ei4k?JBk_plmCQsxl<`d3?;KwO0Gs-oSakf*r77n(k8cU0Tix!!{u_{&D? z>82B;@-!t}*v@p~=xW+%qabB_|D+h7+(gt#dNM}oUi?G9LRPai_6xf>86WcKuPP(? z_-cy{VQ)-mMyqlO`mw8WZA!X2_MdN6&$aAc|Ll_t^{&Pi@`uDX9IjcWS&)y+zRq`% zq!v&ca|2k5y2gRk_xO!7 z81aE>c7%b!yl$s4A@tK1Uj97baxANv z<}eY8pEMZwVod=08by!aDJ$*cN%A(RYS*a!lBp0ttcx9gq`lp;WtEpmXY%&ug`dvH zZH_^g2>lyxV+Ttr)q6`06Xaj0ayHcM<4utovc=Rk(?9J!%{vn`qiG{8&cI`&d29H4 zy(#jH^HY3G0`|f2z4dNw&;^}!om4MGg&XJaspe&P3+}J^UrLZjM;eJB zLS5GD0N9N+!+gqyPH)+namMncxj(~3b$Ifom;1@4`m3wZ)SfwzqK2_kV~ z6=~?5C!7269A=}@WnAm1Gu`~El8w|WhnM0Tn#gd+z>|W54KYa?PLaL4H z3w9O_366=+5FM|or&KQnJm~ujvaRFql820TNa=_DB<~rUr75X{=Zr%s)bagRnLpWq z6FZZ~PO!zU?wTAv`R-cg50+o7l!BaUP{v<1m}Pu-vj-r4bI++xRaU6Q5TzB<=y;eK zz=i`@7}oi!t2yiOggUGjy>-;>UW*W&Vgeqe-HX1;kN@e83pZUSTNJ*X001+bfnPe_ zqLRP(n%nvGwLV9-5@ktTtXO|cGEan5YY?>m$iR#l_w z%IrS{_UL}lVFo-n%ltGkZ1~h8)O-CCY$9JJa{B51;AXYlBUr;bow0tCF7(W!>*{_} zWU;vN0BrQ9`80ZiyGakfNm8|MHR}4Gb>PiKuT9+ix(#cQ{)*Of z9cFkqgw6|nGhhF@1d|!lF+8j#KwBco{$M2Tt0k3f62sIDZ8}N}T#d8@iVDPCuWZ}x zuFW&bJXy3)asB#pIX<+@wK1JaThr-^`Xvjxo6g_me|*XhGWIs%`p>x~WNle;CkFm>?to=9C zI-QK{3*VC!^{?f65$|O3o}TTp9@CptKN-KBO?0Xpl=gl`D6!plk;=zBDlJqYs(!_B zdWV(pNk?G1hImgs`~85FwP|@tul&W22X^ft$(C;7J+GI&wm&?N9JpQT>WMOCf@)k6 zw$@&b(dQM4`E4!5cwDvSqha+=o6o^zoWx!8c~y6+-h3#xX9$z`;`&>Y()Q>^laoH0 z(VteT;T}#N9(C6Gz9}~RhnKc>C}ZPX!JmSL7u5+fCiT$n4~p5Q{^XPz3v9sEznP=w zj#8(!vjdqurB2#nW|+U``iK=`+GqVbiD7j#9wX5;C56?EAto3{CapXBmJ?yJi+p2I z`&0*2N%#@nnf!9~4Vtp+b5>e1gQE9=a$4@|92uT*!Gx!nl?DH~->VU&#o@*rmw0!i zEwkSB4pF%|!BoBG8>L!5hms&R{^@7%=(e=ibb8XIrQ;|4>+nAfZ&1dBV?Mub_(5B0 zpW42+p^tsI-HJIHy;{pNeVv*x^UKnaba!o^^aJ?%_6w^$1eE z_p_|N40|wRr*#Q0UGBF!(vRWU4dJg2{WT-ke_QwXRoi3yJqAU*uczCy9heVSP^RUUU8P-^`_%1JF3{5?* zdhAY)+XfX`MmM2bZt>qwaJH#z_+nIaQmm+@ z;I4YA@x&?_0bXwXv?x^Y{C1#5JzmRg8~Q7#YIFC*1!jHveqzU~b`pbQy4r4f!Nkm? z$dCqilyP{_o@5CUEVvqc3GGDwFyjlIyO0%PssXH_q1%23)!D$Jxji%^6^A# zDy{&z%d|?me8N%8tmge`Ez-8PC#YOtCv8frZ^C~mto35AE8mwEz25d(E?qllkayMg zQxGgcV9qJ6jmUtyXkS9BmiD?m`*SH1xeG?VCsgTLjb9Xu!e5Batodem7(d{jz;Mo= zF_n$DpwXREOB>XfWtQa$TJJb;mz(czyi+MhNO!E2UwZG|kyMdw>7Y8gG zK1dK>BfmA{+wFUuS@JaPk&%ST$K~9R;3~|y&Do?~qNTXORc$x6DeDc?=6RBg*M#m& z#zmz}zu(8fgBvCF%u{KBB|SXbH?nSD&){%NsmBQ_F@^j0ng^dc(KtvbElmuF&Dvio z#EdvuQ2r)5S}uf8<4rQmVdxU>U))0Z_-O+*`C}ITo_}0scVG*FZ1J!}wN4iCy;3J| zBYbBo?oteKSn5ISH5>M`e$zhME%JwklbNZ!@)xQb?0inNKB{TGnRva6*W9u7s&tjb z@t-s`Vy@Sjp;*RQW?h70rmc%zL5uH6;vH2HcJL&eLM{uS+U1eA?5IiR-z_E~`i%jr zVwEe&d*@Zz0~_1dpZby|Y(^$f_ufq}4j-HAEI-JZi@UMej=C zuMNqoeZ7roL_m35(CQ}UOG^)G?(jX%l8xrYjqA6-Sl=0Jziuag zWW=TM1$i*ZfpYq#0XEP)eym+R^LPQ?o5B6y>h^X>K!Prx$e!a(E6A^g_i<`(VGbGo z$kiRYBV<2VP2Ll_ui-+D3nP?$mFq8~$*i@hvNHYQLUhra$uZ(^xbHvQW49V6&GW^! zR$b0)&1c1xPm%C!AAu>{3kSpI<2wF|fNZ{wX)UR=w4+|CV% ze7f->R`=k^C)3>N#T^?iyU;M6?vt3$y{!+)t?!o{ICm)8kMb|gX4v z&x5OYy{S98O1-#}_(k%>+o|&Ot%>xzcQ$dfR_p6|=|>RqU%oxoEOjV;pZQ65M$Knd z{kaaEGM!3}F{h5Hc+ZIWF5<>sIoo3-tZZAsL&_j7T)>l2TdcGFvsPe*Z)zv~x9g{m z!lfrlIlcr+t)Oexb-ADFUR_%c!@upz*jFuxx3CG%7wEqg?p^O9%Hnr3J2Jm9|BA%N zea`wdI@X0;X`mXvx>GAl~SD#s(dC%UdH)o915 zjEprTaBzNZ&Re$EJX-6RL>PGXXlZ^xo_&ykd0@3(-1=hRu}+!n9Ad1h%)0d%@pSXV z0H;eY*5XqIHsRdF?lyJ3bF4k8(}fi;o}lkKH#Fn9!HLwHy`BI2?HhW=D_-V7^_>>X z(nzDJ@lNVxq2FoI>)OaQL$(au+*a<*>}X2#E89q1@H@i70FD$gavFc#SxYo#Cl;Hu zU6tglt7O(^EX037p!7{_^{$P2HN9kW^El~dWM?0tebFfiU(56hv3Y*Wu8HF)a+7VV zl0#Q3_Zdc}-|?cw>)lpg>5Hv0*pJs_)zRbixAEn#%VP(;5H@f-(Nw+ryw%0q!Q6Y2 z*&jq&O4(%m7LpCS z-f2HNx=WmJgh0!!^(k6k?=t(bH}Z3z)82Cvtu}-^j$O2}9G&-iPM4_{tcDq*<0sbb zvZ5m8yA5&u_dX9C6s$4Ku2@f5rMJ$v%XI#*GMM{XZuPINeK))u{CBXXy5qQ+4qkWv_w4QJqjWvQN6>!T(UA1)S1u7+;Z+6I$eyDw-t*uD!>C==cV?GB3;?L#W z39U88ZSMu7PNs;{gPRey$^TcudH7?&uu)if_A-)9WTuc^c2@QtA!LheO4++)WhP~o zjO>xU_Z}r9lJ%Mi&tAU1zv4dk?_AgEv@tQ{;zhdhw35?XZYG2uW54Lso8hU~3LgyU zGZ4N1<3kacB3ZBWPF4Foy?-$mkzA8(N0qFh{Pe_D{=_6TD3_P*l|cgnt0~#70n`$O zR;=FPZ$kO+0A$3Y)%p4>0moj&bCF6TEeO`gpN#Q;8G*8QEweM0o2PED`>NaR^usQj zcdn?UH$evXmKXPh_#vFnS@D4ytg}2?24R~r`IP)vd%VA@Afcog@5iVejXEmJf-Y!; z;ANpgkGN8+srwa-&Lx83VsurUkmE<@=62%uOTV;Sy6C*LqqKK<3*8x`jI2J$G%uBt zf?cf%3Vhb7B|@4jf<$aEbrx1|xc0mpB-!4pjm4aljJh4UU|-zcwCP8BmTdSl@Koym zxY3nf^2)Dh5kL>Org*4)g1I$Tk<#|zQM%Sk6uuXCVpDq3h_ej(#FwxCPCxnZqF7x( z9|56>Z204o^U#_tnOvfB6zb`bK>ZIT16vl3qxKA^g3Fj`SyDx8_8HzW-by*)V566- z4VOsFg4_vk#gADBlAPN7AT+A$chGN*AUfaL-W`T~r-+&i7wT-e51ME%fUlfpBC4~9 za}lt4>Z-CD@}A`dHkJjJOo|FZubZKNTy-$dsvC(0Qq5+_Mp<0ugO8VW_5WPOG#=i0 zp!&0`3QaiyWYR+IkKc=vdZOsgBkn`Y+T=0dNw=0$Jpo3_FW3gY$;iAnfU}cwKh_+b zed zypL-wxKzI%5>%0w6PTga&aChL4m*$p&-)Rsk7Nm>KaEx0ZqFW1kpn=fFE~y6cb+~!_;xZ^y{VI@RdYjzk zB#%i9Y|fQfS&)OgM8IhlKQ<+7`WiQN+#u}D0I2fvAS$71XtyC)2Y*3Uz!tq`yuGL? zG_o7Rgs*Rh3f*~{@+Vz3nZ zgxOuXZ-63d<2v(61g1vm&(AFHQV2_*<5HjRQfwsSA&uqM?vdF)uB!9T6w`3p9%t!0 z;hzQpWaFV{pcPvfu4TJl*|5#c>b?KqSkagcGErMe4AkqV`gaV1O# z;h2q}Q57ycFaOM`T7E|S{Yfq7=iofD?KB<14@12fouo3zuZDqReCS*kmPbd*o@n4T znTHVz(L3avgNpJMvX~z5 zfM6gYVGb^pWg^#*P(57H8A#quPauFdZ6pjS?bn8Xe#_Z=c4#zX0B5UF^XMGu5KfI( zA)Sdj9-e<~A~6}CqWS6qLl&DT-Lt=1U{;VQ#T4Cn@YG$q>n%#UG9k|Uki^h9tX8=+ z^N+#55IU^(LYDx2<@4lCxL-MPLy7&A_=bM~gh}o^IyJLY_It=Y! z&zsQPz7GzfU04>ZMvvgdaf?ojVQY&u}#jYND(%_hvC%mPz;rg<_9|8bxxy#cxw za6_e=yPcH%2!;fJoMPER$)G&5pn0}=iuzrQKLi|d=IU2nmhk1OAc(^%pJXnUcNFld zCnKSHdFhtZO>*MpLDEnlSOznQNw>5UA@$rd19eZ+U&sOEZd#$OGDhxQ!W&{dc%&kl ztmk~+l4PZN(7U{z;dex#9uJQ&lITg+7>Vr*dE9pm@Zm$O(%ebzT9_zsg7 zKe%QgT1U1&8E1Q+DX#Y7RpNt!eQ;ZL=ZcS!-C%P>>n|+Aknj_NqP-pyGhq23<^q$w zz#`EIitf18e7s2mEk&`SIT9R)%e02!@hGe|xLs=?Y<%^xVyT5^M zQWiK@J9gm+IX<~x3f_PofYLX%oB#tOx4WRvK9ge`n~jL=1W}3WDok?9jqLy))^V)F z(p~B(ZWfztaGt;L{7GHgv^SzvPfqvU_j5>^(H685cFj1qazd!&Q85f-QJM!=5Xtb| zU_$oyXS`6&fx@{ZTVQnKpBi>A>A*TzSRkCpLSOBBv=(7G;}rkRUSlT99rR1!2>JSy zFXKhJ5EZ%2*~oDG_N8;fF`*g34O$~cK!J?huq?+o8;&Wv9)Fh4_>s#6I0EY>1fJJ| zT6#~ZQQ^jGLV>pzBoD$97n8a{^vDbudp1|Y#%>@}kmSlvzOadU^g(;VBZaGrwZcY# zWKwvDHXaT55$`5eVAH;j8>c+)k3<*(5A|BPELgwOp~I@4QM|uUB_rv#wm~^mS2t8& z{NKAc_S=nCMzTV1_Wu-Nt4_B71+JH9>?F8wG-*k3qkKoinfgWQOj|FBQ8Ev2ZjpGb z%r1iv!kTC|e11G$c-%{&csml;U#Z<3&?ebS3~m!?p1ycryW#%4#qBrM7~6*xB6A|{ z-XVxZW$sO<>Y#8rjR3-p7LRCc3+{MH0MyV z)@liY$B~Gbl>&&leT(kW0~LsrmF?*5H!?6(X%AY*bAdDxO@&0IL<(zmAL1t3QOUD0 z2x{I0vsv0d-_BcJvf4_km|h z2Eq$}fr%^%R-g>zVT8q6AH7NKCrD3FAg@Cya7?!-rr^K^|2_HJwC~s$0iKM)ORn%j zX5&?xu4)!gIAO$;!_Rr7IExnr)dJELvh#I>YUfOO^H)Q5PrlM%W9FQDLw<>5_($3)0a6060CK-egaAD-q7>9cofC$r&Fqk`=5yiBrr&xBz(k#i|m>Oy* z-_d%fzNxu0;yy+!THg6RxTalx4TBB)h&)7PyMy-Gz~gB>$vDew zn4~vXD$XvIte7qe8DpoRv9wT-mbl=3HE^W}xthX(*t8r3?*;7Qjrcey6@7;b8npju z*^vx@c%F6g>pot{vNt{AwQmcF--12jw&mCG4dA#*jgk=JP-_|;vpInOw)l3%J)}dO zl9fqyBLzA|{lOJ#mz9!o>Uj|9bh$CvI=3JPkxE$X6=hFXqV-uGMqaYaixzgG)KueA zo|1LBcYOgyYeNYC%s0itv|kn0+W;PQc!g0uen2g}=-_6X^r9qn1jvqnGbka8QXw== z3ZTY4DVSd07*suM(NM8u7VI*ZJg2-6NNs7fBc;Z6VlBQ)i>*kfT_2Ju$L!HpNju8B zJMp<{m;0HzEGm`yG|-hmXxN~h@~@jY7N!D5tW!S{>;{m$Zb}!y;@67pXoE8uj&WIE zWTEUW&V9&?j&G-v-Mc1FW-md8b%O!s#??@x$;+(#E!+3+#F#(~RvL)!sWXU_3Ng43 ze|VM1d<;b3Kn?5W8-C~w3kFi9z{pzp)YV^9Lt7I%eyD_@q_}8J<(uld{Gtex?&1JD zdQ5_!DageuICD{OfcW6eOh-nqOLP9ogy{Rwuq&gre-64YH^xOn*)A@(XN1zmjo=mM z|E{zLitmfHML@eU$PXP;Kk}vU?!*4Z8s1HUWx04*Xu$#(`$dFDSn$hlSN}P18eaxf z8L)FqZy;uCaa5xeiPm9%HNw>EODoNx>XfrO0qI{bdiy8(3b=IVMI5W56P205L!7yI z_VUaw1{NRUAHDs8H-<|i4T&-4Ko;0U9v@rY!x(I$uzId2RaU`6=#k36R0BLYdpU0! z{@CMh~ zc^^9Ri^ui0Z_0<$IseanZ#_@=EW<63jgC*g^HKWmUcFE{st4yzmB8IL{^)d|e!;m- zbM}0%9#vMHeZx(t9x^ncZ^$q>$$k#DyiAO_X`-U4W+wCI$O#Wpe>Wg(tP6UZkq)nQ zB8iK*W6@?o(3u!*b0@>KN#q^b9J5l05uhmIM{lfDh4R|_EFaBMdo0AM0NG!gN;Vry z2JCR%r7H}G>!y0k6$}m5crP!w3N?<5IA+(aqa|-Q4u#XQ0Uw>RE}v?AU^aT+zo?$n z0{^>ph_JgZuwzsDl)7-A;%}vQ6w*P7fG}}f2YXJ3AaCuhgO=18H6FJsT>JtETu9qw zg-Kdnc$zNN5Fp>yF|t8KT;fw_fU{)gY%xa^LUh%GJ3(lBwQK}M_6r?HcSlFV7ww)x zICj(670g>9>N5i%&y1x?7vcF16{rcMtf82GU4MPZ1$*t&lNU$#9IaZeeQ4yPF8z~7 zS9CYSR2b&aFZ+#baka184tqaG;{S9>(H=Y0wURD zLe&OtMfxhqAy$!WPv@WwIicN_7k8^92X@HX#c=Zk-Q)z=AH|}5TzB;Cwc7zBKi}~& z71)1q=e~;!k_t?FGJX@{TD^ck-G4`DYPH8Yf8e61=qSRbk^`UL5cNTVY$0I1YTEW$ zqRd6SBE93XOqUI*W@WTxH>Qs{M4H}%gBzmwGfGR-?UqQr)Djg z?#ztb5W(&XDHCcFDI`pWn#sA&^4qmT zS>U1w2;`R=Rp}88g8jHANHz?c1_s8&YOgSc^IM6z@+U6c_1&T(iu3u4KpqpU(*Z^#mKb zjY6wP)8`kYiVX^><@}gv;3k`}Bl9+P|8*T@{X-gQ+r^)va?w+Ed8uOf754(E5FVyk z0uR>yq4Mm75YOLIGHg78>$0_e%VGFS9}TojshZ9NSsSA7wNxCjvlKZH`liye-8n}4I1%0P%wM9taazWM2RZAXZmMSiVecre%fU3+)II@ zS#X`R@2*@HiWw)fL5bfCF!+kn-Lxlm#?+<<^m@TO+^v&SICOi3fs&preG-w_jvsyx zVCkC^Bd|L+=bw!k#&u|#5-n`YZ~+ajwwOjUq=;`5OzFpq#P|%Sf4TcF(>1g)TTWED z;Jp_8@a(dFpY8Dx3JN&_T41_c&!zZH+m=N$20KBIKa+x6nHl$~X6$-`$Q8s<~sD zF-G(k{EW@j-k+ zZzEP7(_S=uz4$igHlM$`?n|nlp8?voabS#L<0P9pE+n?w2rl0DCob#wjQ%lUg>uSY zopklEv_M7NmT>d_CwQu&LtN5l6?;MPn=L|K=lbs$d+eD4ih!$J562`=w_v_5`?23NQlBy#$mLrdv8JB@pZXTNqNnmZ-mm;c5q?sN~0?fK~C9hrkgHU5QxSOPWP_ zf*0^qnrPBDqo*psiT)C2Rb&Air{*F=Pz>) zePFW5)S7zxQ?<$F6RL5ciDzOZKm2RI3u$txmUosvr&veda?|QXKZ46j9sK0&E-=KS z{;KIy73`TP-Ni%8aLvd&%!(SBIuy{j1wKI333n!SOobQT0l0=LT_ zS3-_weRMsraQ5^1uMwTSx}E?0kuz3D>R;dNkx5)9PEu%&+~7fb!St&QJ5?5YJJUB7 zDwB5`r-ys_bv17eJrQTs=G4C8 zHH?upjw2aQ+kOpv4Hz=#a`ceIQ{_^OAK;K=ydsWIY{^_MX&3#Qf4jv zQ4bLhLGR=i!uYIZGMqp|E@QJ8MI z5!g}#i^%Gu&e3K)f?{hRNxBXurb2(*4`L*z(3u&&E}a>%w78)3-FI>Yu9=LKQ7$51 z$k>ro8HO~g-f3WEbG~*-B^F^d9RU1W$+TmzaIWY3J;R}TOEk2a4yg~o)CnS$I7j^Is@$vg3VET_* zdAsQxxm4c=0e&BLQVedU!@}QCw0*Q#Rc(ywutR31n2gF% z+-lE+*2b&{9?*GajC5YKhlX8F?*}VV4sq+b{JUN_y}u_e9bWd^aW6 zn5iw6wBuQfY$}xBsTuu+17A{wDbYLs<8l>|Ds`kDGtL@-{!|Ds_LmTL(PSQ^ht$F^ zw8_AuSHUagpMb;^`gU)yt$(Dy)?+~4UIv>m9ShcxRa&k;Di^u!w1Q>@H7gs9JW`Az z&f|?P{p+NMv=@BbvRc-5cd6VS-$74Lu1wws0I*)hhdnya8xX=bU@D9+Uoq#HdLSz> z3(pyigw(KKKjnUjN7RU0lITf(g!QcL{czHcT{~H8(>{OfkxCi^yU6n{dxF zpr>cAJ8azQ3>_Ik|M}ZVQij(!5*l-~%x~zBr|fMlV(I>3`cF@I-)YzYBdAC06(V}H z8lb~_5BHc2+3)`onem0r0LC;k)!XbwU-m}X2uLIC{PaP4S+xu6b>i7=mWj-T zRwGis;w2`1&$FI#Y9X skKk?#YbC4uGDx6MJ}CB=Gyb=lAZeDx5n%$yUJ$>taukV-dFZh0W&viZ5b@n-HueHwF&vQTbeV@Zx2D0}70H;KQojgR9MP+yF z6qOS7aXIGU1g}nd9CPu5*B-t>F1|tVgFqL5Zx3guAea9P@;m0@?GWe^aKavX9dH5%AHN_EKVQ)^Qlid&zQ;WNbK&9ZCaQc&)XT&7 z7##lp4hnJ#baR1go)UEpbnmsK}ytJR9SAflxVQ1@^1MvXN0x@aM<9GEP?>A z4*m*p7@9En{%_2&EV~B2|IdpT#s(PDFm}K&gZG?70SJZ>0wV_lpb-b43C3pv&PM_u z2t%C$;3y410E|KgfOjz7!(drB34oLQFaCE1|I2~@<-q@^99X*vfM%><8z+1M-pG8Z z5h!`_4BK}p8Yw_~fyMP+qTEtgIot~*8@>L?SAYV%=*-C(TTgFM%0+w3VQcK&;V?})5sPio-K?cO3 z6-9}x%qLN1NxUSrQ#6F#q<5Ox_%DiFCE<+nk6i#Cs*d8$(La&rykTIbuNqg{X+}s{ zgkoFXJg17AHHrPHiu8~4RN|s(Jh;2&50>n34E0#i2Ed{wJ{%f{Xfms?t#2l&imN2r zfb63k?n@I#e#ByDsBy+II+r1}cT7VIyf7sut<>;vs z(I9y15LI#bJI*RTMWfo^QQcSah-jVtRL$@oT3gS9RByP)ZhCft_CH=gifBx;wzLh> zj64K3eulX1?tGB?F#*@_NI{hR^HF_sOStb=V$D zs_!N3A2-lr6V_y|N-yhaEzVeA6~JIWh7Z?ipa!YlAXVfR7M~oB>^gdwYB+v^NPL@z z2HdwQwNrnFL);qfl_@Wi%f_6Jwu%QQi(d?Cy50KDXiLiKmM=s2y%;W4|sJbA2-g*K`x8b zfMB;*RKy<{;+~5F%&SktC(S`rRXv+3P)VYBA3Koh;dAU(iz0f-GBs*>&N^0bAp$tu zPsDu(FCYcY`w?$54P19`5<)8$p+1g_luybWu2PYV@lU-WZeCEuT#RJc^zY~B=vX0A z_UIb+Tzn0k=5v)a=(vdg7Z_*G)7b}NB%m-|)yY*;d2C0nJ`-dAi?mY<#}z!zAvb zRVHqAw;j|?Kc;ZV5l56_(SjF(v{=M?V*7yzRQ$j^EBvI94ro`V-bn6aqAHeCCYz?2 zXS|t+POC0*WlJn>?N^9|+*yH?9W=*V0oBIK z#d7wA(y3QokZno+%+~%)l1JMb~+WC9cyPJbk;DTv?7~P zW5dLc`H|+6Gg#mVKRQ^I2M(9?VYO0R=uzF1;CY-iMKrdfw>KD2wlZ4?xud^P9Ke{T=n6?Y-nG~ z){l$u%M7Q41LTRZ#&WbyZawQ|ZNm%@JILHqjqprw(@2aiY3#&Bn2$!F&M&W0ty*u0 zz>yxb=<)<>`C|jq^79OtuN}(1S8Ah~{tN6IuOg%&tOg89hTz6Ww9r4LkATG{C!AYg zDZwi8VNr%pA@%{-!|Z5}mk$Y}-g0v1(J@Asn}bR7(I-VBRsgp~gZ_x?>46^^=Wv<_j*L()@>9wK19zK%EPtRV`2vcX`qyhiJq@wXT`D@%^w})jSE?9 z^FDEAZ;&`;e!Uahq|uIK&pCq+ySAb{IV%yBofcGDUJtQZZ$GBD@*1^HVgTJS4O5;yjx6m`Lbfljh4qq-1Z}kh)%N!Y zjp&9P69^p3Z*3Pi5KxKVE+aE-nT>pT_q z*5~NuVtZ;Shc$8XW(pQ|T8lQ#dO#n2C_#}Sx9Oc1PLbMs+u2FWP=>R1Ewa{=6R*8} z4WVnrke&~bcz13D(*JD(TR!Z?1a9&odyYl2#g{Fa2bUd5i3D?;S(rzx{Puv2ilw;f z9y7>?)ahTgC^6MVqZW4?sq^Pr@UDIe9o7HA+`PPtSuj;2t6Www=2AOJ43@{XRcXli z_CC;mqXo0}D@CGo;=qjMB9`H^k612nlxnEn413fsbX_eMJ-XP9|8d?zYN-EX9CZ`v zS7zI(iy6v{nielL#q$h1r{jkNI09r$^C6zFmJ5B^p9)0v2HDlHua_WK*b}~HjNkED z^2&DyTqfHSjfu|&{+W+3-PR9?9=8zk^eite^m87?6DO!%;V=01P9v=7@?*-%{SB?L zc8*-abC}(<o!oe)$du}!c@p-zq9%)Um&ud(m=hGGw#__i4;V20->LlxSP>7cus3E zoaYGD-LDAyts(8Oo5c^i?ljBq@sA{5F9_m@hTz|Ts^jgJuvrR&+1o?q#pg~#OF)C=}OwF#4uY(Qqs z+2g#na%k$r2FR1j@Mh#5&~EU-X5nbP1+dA_*9DAoyR;C8*U8eq7gF?s7b)r%q8sFf@HoR|@XY^GqoLpM zE5$~b+;&dZdiOt?Ii$v_h1)UD=T)g^Ci7VAGIiA9u`Y1cmBd5tn4tC5>VTuIjnWB^ zz`ohugnafJ-WKP81>LcyI!t_-x3Ua?4-a&Mj&$@EEfpIA9#O2Os@+f z!ESr-m4)ueWa4^^!!n%cyb^_qCm7QW$9V|t@OHHShAZy;v>u-66~NPX9h(zdfo{}D zhP|o@&nFqQx9l~wBTb(;b3z_FFj~ScwX|Z!CR8Bj{>%h^IY_RrJYU3@FfCU3D})}K(Lj@(yc?anmNjXKTbOnL$K32>tlX)6(ap){ml@+~-?Z%7q9 zB#5D1F{t5@R?0L>kPu}BFvUH~@Y2gJkb^FR-2E)ldu$uBP`Zc-%JLCmcb=i+U5V7c z(oMt~PgCsIlqoeFc%NKiJi-b`%+iU;haiueXH84InDX-;pn5n6H!{BtvHS_pgIYtu zLmzo5>_eFy>BoNxabW&#Rxk(85IOP&7#new?H-S2uC6a&^?PL)!^PcH`|5bScYHa@ zWjPL#tV*yoQ|5?j@F!T`IDKly9%4d2LU*(r@kfxzZ16=t9(UHgj%bDtfR0=ZI^@eG)U>34`aR?ebxR^P(sqSKIR4Nr0V31} z_9HVVah~Q&%OP)Y>Y+s!J_8rsi}+|`0MvTB5ci7*@yRY;;^fu=v?*>E{cEU%@G{e- z#Wc1N_crT*s7IC9lX^!q{In5Rk)w~FaC1UBu1I6^mB*>c*c9yWZZ3LJ!jC9yZb0{E zZHCy{PTTGFB~>0ILM?B_c<*&3fl?rXG};269$UP|g+eBcqk)KG8Z}>d9P*hZSW$N| z)!HAoq5dvN;dkQSZS=ApFk)rc`kis8nD$pL78hB_2EOt4Yj_>0nm(55Zp1NlfT_Vdm~XSq1+w zTJFIzDrI7haZBl=&81TCbphsRO#6PsovRwZ{1HK~k+%R9z(v2h@`SMVYeBEp#6s;c z0(HZ7sCPZ3E=7zNr0U7pO0OBPEv=5QB~+{I}Rm#P(zw zwzl>rW$APe@|?$Tt|}_z<~zd7X*v6_{t4tbl2qW$U(EZuVp^n7mVEHrgnm$Th4AV* zfl0jAA?!}RMHTl5vinlg8OLLlqjnSB*K0sgIoUyLcaA$EtxZM5w&fOBkJkjO?=WI-Yd?wV z>$8b@D=De$ZrHEx)7a29vcm+!l`7nj6@?n0NSnek$^}sYiDY0}GKbHN?Z;M3Y0)ce z6`>ati`}pOz^<`j2Hk2gNqyGz zVmiq+)XAA>IL?u55Ow8h0y6^1cs&1M=y~m<7fb)({I(6~ zIz>(DO>P~LlJ^#MditF)8RcQNM5vQ-ra1I%7}8`^1Ly9e5xq-+ph>w8;#ohEIw6Ue zkKUu2!}bu$v;C+*02gs*;4`YT-nJbiL7SJbg<+D8emluuA+CqW#YruF7tkRLT8*bI3!CS|o6ITDFN zziJo^meJUN01S%;uC(KhiUZ%}dT=|Nf|Li5c_wmAhl*at- z5Td&5wzL029?>zwAD~zFjctzmj#RsdB4yo=@d|raAW(P@^XjojOr12L*4#+1E8PHf zfeO@YCUkZ=g&)jvAs_z}VCOvDNcSEwHe=3_F~2QFE)%#2ecKCktimel&W2nztvsAw z5|85XrpHh*JsCt~VG1+JItH->MMf4AsCnlPP#0vt9FwH2ww1tobfn&ujYDj9-MKWkg+(gteKVe)=+EEGUj#e`tZ{sraCJ`#+)M#)Z&hg8Xd)!DKzu z=m($o5>jvevOA9U(3ycLq;{(S%Qt2Zwe3GL_+~eTM2;h)nkhgef3f(KH`T%G&)I$tl1vv9I%>&e}$yvFhxTrbT*X^Bm~ORl_r+_0bujX&|!T0DT2P z#A@D*QmY6e+Iio>Uc*oGq!aL&C1Y!3cd~LGwe-!>FU)M=K}OWnnaq9Y2(^zdn)`4m z61VI$o*fhoeQ;Nxcx#NkHIPg`QeMMyrpq$#!zRZKg+NH#I%Q)2XMlC|&p)jG;%_ z0QcPneU9Udz&dH@-+o~u&N-2)6@hs6Tq<(=ssh;Y<}p5LY>9pd|(2nOn+=qd-a_Zb^CE|=iE!zL<4D8b^8HwdU45VnarfTwgP9Yoc1^VQk zozUOg4Sgl9`?&ZEoH2rAKH#Tl8eJc&O1_!Y!t+}4zgt_H>*X=kZ9~B z*63X-qt=v3%4YAvS5FlpKXN+3uc3DAw89kB`c{C6t!KtE?=Z$u39^M(1q7$vd{&qJn|+#;`qo{S3MHK4V4>ANjJ*;>}x^KLMU+ zsu|bw2>3a3rpkDS1o(N60IxcnS|iHD!J8p&aM*HJr+ofK&=S+l#BCH1+WIi>^ZnPJ z^oAkm~-uX+8l2(|OdA>7MvcqS6*i2NsFTYfZEr*!U&Rg~2Cdm4Tdi&{mZLf4O`-dMF zofl4@t&y18cHmTl*M~g(ZdPvfL|DjXWe2s0I~LcJidjbCj&Nn_^TpFrgG;X#UoO%T z`twqta{8l)*tn3@oUx9+pKay4ZF#eMU%w>J@_~!O0cN(U;q`JG z%W^{;n_kuNyb8=NmtU%2$kXmJe74+6RjzSR@#f_W9-G|)yc<^?P3J1~scN|+=7R$T zxvNzxKdk7l*8*U-95olHe)y^V6bI1LB~`em6JJ&n9`qK&N`}i${d!AXN@Fcu!n8M} zFS#Y};IL@(g5mx?!1$&O(%gzdR_CpMHy!L4V44hT8S@J$82Ml+x-V}dkay>Rxw3Rk zmXbKA#+QSSA3_bIgGB*XYB8LP!@Mlag;xLG#qjovTTDj(5O2KdPS52OF?S|Er_IYY z===FRUOINU` zSC=@%GMw}KS8lQ%ZNHazzV0q}6UPBJ&&5(rQJINHwCd-#_qoI_3H{rh_x%|fEygl#a>3%~U%b}dW2RlD`0|TOSLyhcwQlbOTcXldG%UQ=O%_)z z_^tYR&)kQIX59$E)eSQP!#(ml$)~+{u-_LV-kg0_@~crdtZW@+NN0R4SiVo= z>n$5hTD}gub>Z8zqC%#3kHpY1~<>O|R~b=f-Z_v=SZ2 zzW%u@Q{ZrZ_w8LCMJG5jRi3OgqV$&;Eas2W&tpqqE#E3=z;3wcbQUrS*KWP$tAAlx8NMx*&@N#Pi8V0OY5Z0g(Ls*PD}skDk)$-U6vt!I%I(@aHkU!?lCDV zUVZDk>TVElyps{u+FyDn-|V#$=}Kd!4;gVSNs;`j)%tcBmN``idYONGkC|rk@Ou|P zbc`_&0~#ln0u-!y{Z~Z_gc!)bF1MINg2~K$xHpr zR`gWr9nT(Y-}Eygz~o!$5Awpg60UFaTFQ|L{myu6)Ysw2=*cVILW!LN4-|%kG`)n>$5zS4Hl4h% zcW1&H?RTz|{F4qJ7u=Rg@rNUog5N+B!yDwv$^R%Cg;8$>Tliv2>90aFaye%e1*jgQ_gme_1~rS9I!c z-^e8^bMeI17fY|Ds+s>8k8m6N;8*qdN>5AYmRn~f+FlC#Km7jTlDVQ{n}<*4sy%3X LNgU4&(V+hW7Cne_ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..91e94a1c762632cc30eb522866a79307bb2e5f03 GIT binary patch literal 2222 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 bxgBY@5%KXhDVueAQ1qo2i}6y{*ZjOI^HwNn literal 0 HcmV?d00001 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..94d3b4684 --- /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"}],"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..fd134d3f3 --- /dev/null +++ b/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json @@ -0,0 +1 @@ +{"class":"legacy","components":{"v":{"media_playlist":"../hls/legacy/wai-v.m3u8","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/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/wai-pack/src/conformance.rs b/wai/wai-pack/src/conformance.rs index 677778c2c..bb3870572 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}; @@ -431,7 +434,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 +509,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/golden.rs b/wai/wai-pack/src/golden.rs new file mode 100644 index 000000000..aa2ad82fb --- /dev/null +++ b/wai/wai-pack/src/golden.rs @@ -0,0 +1,532 @@ +//! 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. +//! - `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. + +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)>, derived: bool, payload: &[u8]) -> Vec { + let mut m = Manifest::new("video", "replicate", ModelRequirement::new(cap), Conditioning::new("x")); + if let Some((base, layer)) = refines { + let b = hex(base); + m = m.with_refines(&Refines { base: b.clone(), prev: Prev::One(b), 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") +} + +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, + } +} + +/// A two-piece index with a base, two refinements and a floor, then the base again and a +/// floor; and its objects. +fn layered() -> (StageIndex, Vec>) { + let base = env("wai.video.int_motion", None, false, b"base"); + let bh = objects::object_hash(&base); + let r1 = env("wai.video.int_refine", Some((&bh, 1)), false, b"r1"); + let r1h = objects::object_hash(&r1); + let r2 = env("wai.video.int_refine", Some((&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 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), + ] +} + +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. +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 e in &ix.entries { + let Some(b) = objects.get(&format!("{}.wai", hex(&e.content_hash))) else { return "object-missing".into() }; + let below = (e.refines != wsix::NO_REFINES).then(|| &ix.entries[e.refines as usize]); + if let Err(r) = wsix::check_object(&ix, e, below, b) { + return r.code.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()); + let mut wrong_payload = objs[0].clone(); + *wrong_payload.last_mut().expect("bytes") ^= 1; + files.insert(format!("objects/{}.wai", hex(&objects::object_hash(&wrong_payload))), wrong_payload.clone()); + 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])), + ("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[3]); + let r2 = env("wai.video.int_refine", Some((&bh, 2)), false, b"r2"); + 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")); + 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()); + cases.push(json!({"name": format!("receipt-{name}"), "kind": "receipt", "receipt": path, "indexes": [index], "reason": reason})); + } + (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"])), + }) +} + +// ─────────────────────────────── 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)?; + 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")); + } + } + Ok(out) +} diff --git a/wai/wai-pack/src/lib.rs b/wai/wai-pack/src/lib.rs index 7864bb5c3..f868e5bcc 100644 --- a/wai/wai-pack/src/lib.rs +++ b/wai/wai-pack/src/lib.rs @@ -22,6 +22,7 @@ pub mod conformance; pub mod encode; pub mod fixture; pub mod floor; +pub mod golden; pub mod objects; pub mod pack; pub mod plan; -- 2.54.0 (Apple Git-157) From a0bc29ae8328f8b6f502ad48bee1d881577aaf09 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 05:58:50 -0400 Subject: [PATCH 05/28] wai-pack: test 5-8, 14, 17, 18 over the golden; catalog tests; a refinement's refines names its piece's base and the layers below MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/packs.rs: the golden packs byte for byte and twice alike (5), every mutation's reason (6), the repeated bumper one object (7), royalty-free and candidate refusals with nothing written (8), the floor's CMAF structure (14), the measurement claim present, missing and forged (17), the receipts' JSON (18). - catalog: a layered class in every catalog, #WAI-LAYER lines read back by http-delivery's parser, adjacency of the WAI and floor playlists. - stage_index::check_object takes the entry's index and checks staged-delivery §3: base is the piece's base, producers are layers below, among them the entry below. Co-Authored-By: Claude Opus 5.5 --- .../mutations/index/base-with-refines.wsix | Bin 1088 -> 1088 bytes .../mutations/index/class-flag-unknown.wsix | Bin 1088 -> 1088 bytes .../mutations/index/deadline-on-base.wsix | Bin 1088 -> 1088 bytes .../mutations/index/digest-without-tier.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-flag-lie.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-flag-missing.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-length.wsix | Bin 1088 -> 1088 bytes .../mutations/index/entry-flag-unknown.wsix | Bin 1088 -> 1088 bytes .../index/entry-in-another-piece.wsix | Bin 1088 -> 1088 bytes .../mutations/index/floor-at-layer-1.wsix | Bin 1088 -> 1088 bytes .../index/floor-before-refinement.wsix | Bin 1088 -> 1088 bytes .../index/floor-without-derived-from.wsix | Bin 1088 -> 1088 bytes .../mutations/index/layer-gap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/magic.wsix | Bin 1088 -> 1088 bytes .../mutations/index/media-class.wsix | Bin 1088 -> 1088 bytes wai/pack-conformance/mutations/index/ok.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-empty.wsix | Bin 1088 -> 1088 bytes .../index/piece-entries-overlap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-gap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-kind-unknown.wsix | Bin 1088 -> 1088 bytes .../index/plan-floor-only-class.wsix | Bin 1088 -> 1088 bytes .../mutations/index/plan-past-class.wsix | Bin 1088 -> 1088 bytes .../mutations/index/plan-past-piece.wsix | Bin 1088 -> 1088 bytes .../mutations/index/refines-flipped.wsix | Bin 1088 -> 1088 bytes .../index/refines-outside-window.wsix | Bin 1088 -> 1088 bytes .../mutations/index/reserved.wsix | Bin 1088 -> 1088 bytes .../mutations/index/role.wsix | Bin 1088 -> 1088 bytes .../mutations/index/string-index.wsix | Bin 1088 -> 1088 bytes .../index/tier-decode-over-entropy.wsix | Bin 1080 -> 1080 bytes .../index/tier-entropy-over-entropy.wsix | Bin 1080 -> 1080 bytes .../mutations/index/tier-on-floor.wsix | Bin 1088 -> 1088 bytes .../mutations/index/tier.wsix | Bin 1088 -> 1088 bytes .../mutations/index/timebase-zero.wsix | Bin 1088 -> 1088 bytes .../mutations/index/trailing.wsix | Bin 1096 -> 1096 bytes .../mutations/index/truncated-table.wsix | Bin 613 -> 613 bytes .../mutations/index/version.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/capability.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/cmaf-listed.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/derived-from.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/length.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/lengths.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/missing.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/ok.wsix | Bin 1088 -> 1088 bytes .../objects-index/refines-names-another.wsix | Bin 1088 -> 1088 bytes ...5137aa92482479325a3bf227511939aa3b6c7.wai} | Bin 334 -> 334 bytes ...f130b4cec57f2c87578f3ee3dddedb3931c28.wai} | Bin 334 -> 334 bytes .../mutations/receipt/counts.json | 2 +- .../mutations/receipt/energy.json | 2 +- .../mutations/receipt/index-digest.json | 2 +- .../mutations/receipt/index-v.wsix | Bin 1088 -> 1088 bytes .../mutations/receipt/live/index-v.wsix | Bin 1088 -> 1088 bytes .../mutations/receipt/media-class.json | 2 +- .../mutations/receipt/ok-labelled.json | 2 +- .../mutations/receipt/ok-live-window.json | 2 +- .../mutations/receipt/ok.json | 2 +- .../mutations/receipt/parent-kind-live.json | 2 +- .../mutations/receipt/parent-kind.json | 2 +- .../mutations/receipt/policy-digest.json | 2 +- .../mutations/receipt/receipt-hash.json | 2 +- .../mutations/receipt/signature.json | 2 +- .../mutations/receipt/stage-root.json | 2 +- .../mutations/receipt/unknown-member.json | 2 +- wai/wai-pack/src/catalog.rs | 70 +++++ wai/wai-pack/src/golden.rs | 23 +- wai/wai-pack/src/verify.rs | 3 +- wai/wai-pack/tests/packs.rs | 284 ++++++++++++++++++ wai/wai-rs/src/stage_index.rs | 38 ++- wai/wai-rs/src/stage_index/tests.rs | 57 ++-- 68 files changed, 437 insertions(+), 66 deletions(-) rename wai/pack-conformance/mutations/objects/{b94c6b554acac94b32163312dbae792f5aee3b32a03539f6b6d56feaff43f06f.wai => 3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai} (61%) rename wai/pack-conformance/mutations/objects/{97627b882afe8e0512011d2c45bebc4a9f78b0afceb9277b1227bbd4294f51e2.wai => befc8683e2cc0f4c581568b50e6f130b4cec57f2c87578f3ee3dddedb3931c28.wai} (61%) create mode 100644 wai/wai-pack/tests/packs.rs diff --git a/wai/pack-conformance/mutations/index/base-with-refines.wsix b/wai/pack-conformance/mutations/index/base-with-refines.wsix index 5b6e5a5812d007aa05c28e84513f14018074f3da..fe5b164553ae96be172c514a63b5e7a452a98763 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/class-flag-unknown.wsix b/wai/pack-conformance/mutations/index/class-flag-unknown.wsix index a6595f23364395820d4a9c9a1669c8e9486d502d..6234c275ba15b3ab15d58a62279f2758f87998d7 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/deadline-on-base.wsix b/wai/pack-conformance/mutations/index/deadline-on-base.wsix index bb56f6478be188d7e46159f57c15cfb361043d1e..78eb3435f3628737776b28bf486914c4daff9553 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/digest-without-tier.wsix b/wai/pack-conformance/mutations/index/digest-without-tier.wsix index f3c536aa3d67dd53eb944e4e97741a1662648378..ca9a6f8be431ecdd1809513b1b1f818aef7596ee 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/dup-flag-lie.wsix b/wai/pack-conformance/mutations/index/dup-flag-lie.wsix index 0271935deab099e9a97002421bb9c262874d1811..e5212e039dbf266979f501de8a15b38686a1f161 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/dup-flag-missing.wsix b/wai/pack-conformance/mutations/index/dup-flag-missing.wsix index d14069fe4cb28aeaa46556e4e2afd6eb4951491b..80a3c223594641aa2a8333cfe6d543592b89a8b7 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/dup-length.wsix b/wai/pack-conformance/mutations/index/dup-length.wsix index ebf2f1e423cf2f12c62ef3a396ed9177e2b14f96..64610ae667d4d17df7ec1db3f1ba426bc44f3ad9 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/entry-flag-unknown.wsix b/wai/pack-conformance/mutations/index/entry-flag-unknown.wsix index 0c8d14ae2424bcbbc2b90ce5cb9d9dc929f83326..3f0c314c277cb380b16f4e7d5c7abdf7441c2c2e 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/entry-in-another-piece.wsix b/wai/pack-conformance/mutations/index/entry-in-another-piece.wsix index 99e6a9c06c5f004aa2449ddd6c989cb9f0297e9a..0ef83f16c8d6abedb0601799e3fefd88fb8942ae 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/floor-at-layer-1.wsix b/wai/pack-conformance/mutations/index/floor-at-layer-1.wsix index b92cdb7fd1f930841ff8faf950edcead58e25438..bd19c204aa60944e132aca76a7ff7cd9c912ae4e 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/floor-before-refinement.wsix b/wai/pack-conformance/mutations/index/floor-before-refinement.wsix index dee335a824c70707f9166688403a7832cc382193..443057908924a11d754554b04a7a3291782085f1 100644 GIT binary patch delta 45 zcmV+|0Mh@!2*3!iU;-e%{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0sfC9h) D-;EVg delta 45 zcmV+|0Mh@!2*3!iU;-eQVta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_BtfC9h) Dqn#4x diff --git a/wai/pack-conformance/mutations/index/floor-without-derived-from.wsix b/wai/pack-conformance/mutations/index/floor-without-derived-from.wsix index 93b7f283d860cecc52ed310bd2e15081aeaaa0fc..b0b3154791a1da5099642c47175467fa9510d5fd 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/layer-gap.wsix b/wai/pack-conformance/mutations/index/layer-gap.wsix index 7a4bbdfc5c9e3add065dfeb8cc5b52f56baaac31..964362aa3e5768f82064dc21d886f75ec6933136 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/magic.wsix b/wai/pack-conformance/mutations/index/magic.wsix index 4975e932c774ef64fc1a94e884df1f00e1d5b58a..cefbf95774150ce87aeaabae1d640515bb9e88a6 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/media-class.wsix b/wai/pack-conformance/mutations/index/media-class.wsix index 9c6d2a9e168490fdeb4aca7d1621c65780a2c093..78260ba1a90cf7c014dcc7c2ff7289bfe11e6756 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/ok.wsix b/wai/pack-conformance/mutations/index/ok.wsix index 6a58529b043411f3aecd6940494f0637cf0b10c6..c476d02fe4dabeb1da9ff6e6ae0e6a46d74186be 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/piece-empty.wsix b/wai/pack-conformance/mutations/index/piece-empty.wsix index e5cd23ed823c0eeae953083005f747bd009ec500..4821cda796b81ced732db1c989bdd0f2e171c0b8 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/piece-entries-overlap.wsix b/wai/pack-conformance/mutations/index/piece-entries-overlap.wsix index 84a0d23b7d712a1b2a66d8fbea9dfa44154dfd5a..4bffdd2165cf047f8ab73e1c7bf1b75a1ab4e1d7 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/piece-gap.wsix b/wai/pack-conformance/mutations/index/piece-gap.wsix index bee1164ba4ce5a534caae6fc7c82a52d0caffee1..a89edb5fefa7ea1811a9ed6b362c3a0ffc8c2ae1 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/piece-kind-unknown.wsix b/wai/pack-conformance/mutations/index/piece-kind-unknown.wsix index bcbb25b6690cfba4e5ecd4bad1e32c8e7768687e..6e53e0ce71f913863da044df5a0b16430568c3d3 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/plan-floor-only-class.wsix b/wai/pack-conformance/mutations/index/plan-floor-only-class.wsix index d9e768261270719a4277b27beb23cbff96c2e551..5bc2ec6baf5a6faf44b2252632b09424edf129ff 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/plan-past-class.wsix b/wai/pack-conformance/mutations/index/plan-past-class.wsix index e50843bd3fcfa8c6c6f47de979ea4a55a31068ea..62f1a0d49f8eaf08c8c943f6676bda9d137bd116 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/plan-past-piece.wsix b/wai/pack-conformance/mutations/index/plan-past-piece.wsix index 825be28cc61dd630420b34e2bf85c1e8900c6dee..751ae560151a37fbc090a262c039a030802a1fc8 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/refines-flipped.wsix b/wai/pack-conformance/mutations/index/refines-flipped.wsix index f71a91f576886921612e450765ddc7bab720bfbc..cc9e1ca8bfff8a4aaa1daabfedaf4aaea0a39995 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/refines-outside-window.wsix b/wai/pack-conformance/mutations/index/refines-outside-window.wsix index 7f9e747ba612d7e9d4fb94e6d77845636f887747..61147f7a58c3762f930063bd612bcf728a6a10a2 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/reserved.wsix b/wai/pack-conformance/mutations/index/reserved.wsix index 211e8f1f405c380a3a9cfe7c5b4cbf1aec35f76d..61ffd37f1a3fae91c16972cf0eac5c0b710dcbe1 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/role.wsix b/wai/pack-conformance/mutations/index/role.wsix index c9d84577e038279c9844fd6e7905188858aff484..722695ee13f3ee47f4e420777cce71f5d356f091 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/string-index.wsix b/wai/pack-conformance/mutations/index/string-index.wsix index a2450f01e4ee323f70f097a75dc8ab60b0b38518..ccd8d90d4c3861677e682e80653266b3703a8036 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix b/wai/pack-conformance/mutations/index/tier-decode-over-entropy.wsix index 4da7d9d6cbcae2613adbff0408af5858995f288e..e8920613a9d253b4235f7118ced393a42edc433e 100644 GIT binary patch delta 44 zcmV+{0Mq}t2)GEa-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0yqNM C6%@Gu delta 44 zcmV+{0Mq}t2)GEa-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0yqMn CkP=P+ diff --git a/wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix b/wai/pack-conformance/mutations/index/tier-entropy-over-entropy.wsix index ace9d1696a22e4d1b6121e7f2008a7009a75ef27..70434af9239399294d15643d925539260dbecb05 100644 GIT binary patch delta 44 zcmV+{0Mq}t2)GEa-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0yqNM C6%@Gu delta 44 zcmV+{0Mq}t2)GEa-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0yqMn CkP=P+ diff --git a/wai/pack-conformance/mutations/index/tier-on-floor.wsix b/wai/pack-conformance/mutations/index/tier-on-floor.wsix index 2c2a33d26cd3359910d4a74a9dc78002fc444698..19387c36cf3548b910ad32f5ea8c29d7279a9ee9 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/tier.wsix b/wai/pack-conformance/mutations/index/tier.wsix index 644940d9beb252d0fd33b2cbc302dbd9b96c27a7..8391bb2edb446591de2dbd9f5fd84924c9b66b90 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/timebase-zero.wsix b/wai/pack-conformance/mutations/index/timebase-zero.wsix index 58bc30e8828aebf63491ed3068539ea183664e83..a6c47ff86c6d8704492e4485430f51e32254fb3e 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/index/trailing.wsix b/wai/pack-conformance/mutations/index/trailing.wsix index 9cd540c29ad4178e39200acc709f3687277e7854..9544ee8fb13d10713bc9088eec496ff37f47d82f 100644 GIT binary patch delta 44 zcmV+{0Mq}-2*?Pq-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0!RYh Cl@#d! delta 44 zcmV+{0Mq}-2*?Pq-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0!RX- C5E6a> diff --git a/wai/pack-conformance/mutations/index/truncated-table.wsix b/wai/pack-conformance/mutations/index/truncated-table.wsix index b9268d08ecf9085849d1d9c843e418d86ede9c6c..bb3c8d563bd7fc9e4f107173982fc935de4bf62f 100644 GIT binary patch delta 43 zcmV+`0M!5G1my&<-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0L0%g&q B6&3&h delta 43 zcmV+`0M!5G1my&<-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_BM0%e+u B5~~0J diff --git a/wai/pack-conformance/mutations/index/version.wsix b/wai/pack-conformance/mutations/index/version.wsix index 0c4ffd4fde70d15eb31017669bb5872c2c023d8d..da3baaade95fa58559f01a937534daf4c4204d1c 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/capability.wsix b/wai/pack-conformance/mutations/objects-index/capability.wsix index 844f5d7aa593489f563dcf523bb81fb7130065f9..22012995e69754bb745f92bc88ab1dfbcb226358 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix b/wai/pack-conformance/mutations/objects-index/cmaf-listed.wsix index 6a898cbe190a812f69f3aea20778973e816f5852..055f9276d67a5ad7b308518eb3b534fe95d716fb 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/derived-from.wsix b/wai/pack-conformance/mutations/objects-index/derived-from.wsix index d33ce0e9ec8aed81fb09473dadc1e6f2e254060a..8e4879828abd30524c4eb905863cb4d72d7fa315 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/length.wsix b/wai/pack-conformance/mutations/objects-index/length.wsix index 9c4177a8096eeddd247b56530aea8bfa24d1ba92..896212a311bb44cbfbfcaffd3ca939a849ae5e95 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/lengths.wsix b/wai/pack-conformance/mutations/objects-index/lengths.wsix index 979f7479dfcef0321b88245fdb844dc71e1ad6ec..3f390dda881e95ff076784b234baaf581552bbe6 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/missing.wsix b/wai/pack-conformance/mutations/objects-index/missing.wsix index cf28149e745da8a678c2179361ab389859a16ac4..1bb91de7d0841004340249d47bb338f6071447af 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/ok.wsix b/wai/pack-conformance/mutations/objects-index/ok.wsix index 6a58529b043411f3aecd6940494f0637cf0b10c6..c476d02fe4dabeb1da9ff6e6ae0e6a46d74186be 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/objects-index/refines-names-another.wsix b/wai/pack-conformance/mutations/objects-index/refines-names-another.wsix index 88c4adcd8be093e8ca3cfc94d6fe08dfe00ad9d8..2177f1e8fb63a27b98416a85747407326269e717 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k{tDYGAk!CXYQI({zZr4xFoB!WkiC8NJ0brF-AqqfJg0zd+x CrV^?E delta 44 zcmV+{0Mq}#2*3!i-~k}HOlwt2%E?PI7Bdptu6Zw7?mIG|H97XS)o<$mL-2310zd+` CL=+(a diff --git a/wai/pack-conformance/mutations/objects/b94c6b554acac94b32163312dbae792f5aee3b32a03539f6b6d56feaff43f06f.wai b/wai/pack-conformance/mutations/objects/3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai similarity index 61% rename from wai/pack-conformance/mutations/objects/b94c6b554acac94b32163312dbae792f5aee3b32a03539f6b6d56feaff43f06f.wai rename to wai/pack-conformance/mutations/objects/3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai index 6e707d6a9592fb1f4cf92d6d0b9635cdcf2ebf0e..e59f8bcace17c4d21abefcf67351ee44bb27d7f3 100644 GIT binary patch delta 85 zcmWl|!3lsc3;;mvAU^u`ku+&c7tlRw(zp0<4#foo_i-li;NI|t{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/receipt/live/index-v.wsix b/wai/pack-conformance/mutations/receipt/live/index-v.wsix index c1601279d28b53c30c5a4b24b60f8ced4c24ade1..4c34cadd6c69d5f6810f491e97a4d97ef57901d6 100644 GIT binary patch delta 44 zcmV+{0Mq}#2*3!i-~k}M{Dy<#%nwXh6=<~%Zxahl>{s&0b$IjcJ>BiIlN>0r0zd-W C)fCYH delta 44 zcmV+{0Mq}#2*3!i-~k|)Vta@x{*DC_0UazwzPw7Gc(AX|xhH!PC%e=sPf_Bs0zd+y CP!eVU diff --git a/wai/pack-conformance/mutations/receipt/media-class.json b/wai/pack-conformance/mutations/receipt/media-class.json index bd5bf5dc9..ad8b6ecac 100644 --- a/wai/pack-conformance/mutations/receipt/media-class.json +++ b/wai/pack-conformance/mutations/receipt/media-class.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"audio","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"ae997c4ae55987a3efa32c444b64e4b2408d681059274eeb7fd2fde28a078407","sig":"7b47ceee65b27113bed49f458ec1b34e0bfee7bcf33b8a30e74cab23924e321d561ba776db2780743f40b04baf69d3870bc7b1e6a5b1c2aff0485ccd2744cf0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"audio","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"f8d1ddff4be825e9858744f7a8c14f8f36c7cae59c0ece00e4fd8b5983061cbb","sig":"a2bb736cd1b94e396fc1481976bb7c11beca1259dfa03a885acadd8bc7529079e3c6466ffab77db134273d604b4053df25a5a697cec0932a660477875974f708","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index dcd3c83ed..d6892ad62 100644 --- a/wai/pack-conformance/mutations/receipt/ok-labelled.json +++ b/wai/pack-conformance/mutations/receipt/ok-labelled.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"e73df62a94e68d065b3ad4c4ef1883ca991e65ad23202ab8f526e78b2b4e8dd4","sig":"08f1b20d98321145e08f9308e4ae2564a477bbe6f4dcce3b8ddf359cf65979a8f4dd46c7168377aacb671b6705c49eb16609752702cb6fffe7af41875b1b4b0f","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"e76e681abc953b0da9d31fb46e383b122419711654df721c5451a5b0dd343ce5","sig":"b8de34be0fc9aa6abae9ae86908b3b0cbd5dcc273d992cb2d72fa4851c49900aafc1233e428b4a675a2ce94862c41d72581e128958e66925915308b010941e00","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index 96c4b52ff..7f80626f8 100644 --- a/wai/pack-conformance/mutations/receipt/ok-live-window.json +++ b/wai/pack-conformance/mutations/receipt/ok-live-window.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"799b0a65fdadf2fe3c97abf681a09c18fad19169d5fe1b24633c28bd680ac420","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"56b088ecfd780ae7a857450b7cc418b591863ab185cd720a036d0a958efd385d","sig":"f9fa0e3a98dea13c886a294c20bd3064faa0136bf22281e5a5432de69fc834463e7fe9a0979926816489231061ed21241235775184e113cb6adab6a33820d806","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"d36147c49878e359cb65187f17b4e2d1cf7757a672caab9952daf2a45084adae","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"0dd1a1d1c9eaf64ad45e665e190065e131fe2b777dbf86e1c5e0b562a1847433","sig":"ae0dc11dc23d6a6fa50aa87de02f95567abceb64b8220f7a797ee535d79ad2dfc5b68d800e02fb47a2945b381428e8bf7492256e1563847fde53c365d009860e","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index 712682705..888ccc138 100644 --- a/wai/pack-conformance/mutations/receipt/ok.json +++ b/wai/pack-conformance/mutations/receipt/ok.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"4751c6251c3912dba67f955521bfb2ecb607a80bb2ab4cf850a57371d336c6eb","sig":"87f206f39c14acbf78368ec7fee3b10420168131c38becf2a42a0e1c23236a330ff23a2c9672ec04df54c1084591661e6ad6f2a9d5691002aee16a7207060d0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"1e45bcf25bee6227e1181856886255443db421bec2de60ab831b3b354aca6c47","sig":"ed313a28956338de500172beea34e47a49306cde0cc0d8dcf3973ab5204e141e2ae8731335d8a58a7c9cdd5fb42111164a35d93f4e3d733cf877597053b3ea01","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index 712222b84..f5607b738 100644 --- a/wai/pack-conformance/mutations/receipt/parent-kind-live.json +++ b/wai/pack-conformance/mutations/receipt/parent-kind-live.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"799b0a65fdadf2fe3c97abf681a09c18fad19169d5fe1b24633c28bd680ac420","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"92624c30683265026e97901b305faeb941939ca8b25ceab7dba12aa6c81c185c","sig":"e97552e0f9e6208fedb59c4c60647767930b008229f92f1235a2b865ead1d086c16b3caa447da3eacf2492bab898fcda2f6f254e23cfb88ebaa9b3afdfb7e90d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"d36147c49878e359cb65187f17b4e2d1cf7757a672caab9952daf2a45084adae","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"77ae7ced9dcf3e809bab5249c93e5e989f29fb55e9c1b448b609af194e4e1bc0","sig":"4d2c65df78a0bc8d17cbac7119c712762666f408c7a1c4056224278f7ad765aab12d3edb47e9104f6c1b0fafea5e2432d33391e6f9525458564a3f5f12bf750d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index 5ebd89b58..020ca5ff8 100644 --- a/wai/pack-conformance/mutations/receipt/parent-kind.json +++ b/wai/pack-conformance/mutations/receipt/parent-kind.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"667dc14c39232fd95ba5493b82ae3883fcf682f73bcf6e07246b01a8b076bad5","sig":"deb1498e05f394ca4dd4b7b73fd4c1f923f2fca7aba667102a938fa7b68e4b270aa3c4aa840f9199ba98d676319600a60fe5fe6dbd82b8982a751f214248a40a","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"0c5c81dd858b87f744a2224259cfb26a8269ba365759e2f44c2dd9f1115554ed","sig":"b16bdfbd3b5505d2a860f1883d1463479a5295b3014aab9f8e70bfe7857a5c18ccf48d6952a9ef8e617e32985513c9261bd1a8c251c615a9b42ce5f40886e201","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index d20fe4518..48d896b17 100644 --- a/wai/pack-conformance/mutations/receipt/policy-digest.json +++ b/wai/pack-conformance/mutations/receipt/policy-digest.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"5d3486172c191089a765e4a145fd734596544405febf2f87a067548cfb4b1832","sig":"98fd6fe98e1691cba99ab82138e9c8b48b05d77b2abc7670e8587087e387c9bd6c1832b7688d3923edc612fabf81c5f4a9309017b8540ba182d0262b2deec605","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"d3b71a99d477e139a0da9777c02bfba1096c284e65c74dc544c0a1970adf5244","sig":"b1692e21a14719c4a61d260543c9f335fc2b66f058bd25304b9e07bd090f034caa46acc3f7b1d6c3d4ffd0e4256ce1f81eb79aa48ffb661b0a8a85a5952d1004","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index 76b696e57..9a03fcded 100644 --- a/wai/pack-conformance/mutations/receipt/receipt-hash.json +++ b/wai/pack-conformance/mutations/receipt/receipt-hash.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"4751c6251c3912dba67f955521bfb2ecb607a80bb2ab4cf850a57371d336c6eb","sig":"87f206f39c14acbf78368ec7fee3b10420168131c38becf2a42a0e1c23236a330ff23a2c9672ec04df54c1084591661e6ad6f2a9d5691002aee16a7207060d0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1783,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"1e45bcf25bee6227e1181856886255443db421bec2de60ab831b3b354aca6c47","sig":"ed313a28956338de500172beea34e47a49306cde0cc0d8dcf3973ab5204e141e2ae8731335d8a58a7c9cdd5fb42111164a35d93f4e3d733cf877597053b3ea01","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1783,"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 index 3436ad2aa..d6ed69933 100644 --- a/wai/pack-conformance/mutations/receipt/signature.json +++ b/wai/pack-conformance/mutations/receipt/signature.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"93786f581520b20366eb1f48658028639c666445880b06f252f1637eb42089f2","sig":"86f206f39c14acbf78368ec7fee3b10420168131c38becf2a42a0e1c23236a330ff23a2c9672ec04df54c1084591661e6ad6f2a9d5691002aee16a7207060d0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"2dce5e5e6c7d91c723ab5d991aca1185e4159620437d1145d2bc561e37ee51d4","sig":"ec313a28956338de500172beea34e47a49306cde0cc0d8dcf3973ab5204e141e2ae8731335d8a58a7c9cdd5fb42111164a35d93f4e3d733cf877597053b3ea01","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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 index e3b3bd473..619da2751 100644 --- a/wai/pack-conformance/mutations/receipt/stage-root.json +++ b/wai/pack-conformance/mutations/receipt/stage-root.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"0000000000000000000000000000000000000000000000000000000000000000"}],"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":"20f66227d4b2905c30d7fa6b9dc99c01a2afb5835141840ca5a66897bdc49663","sig":"5b693c183f4a658800cc4f6bbf2b0b0d2b0fe014fa87bd6bec6a06c21531aee6ee3511f5aea345e15b522c217162de6a87b79cb06e8b13d762c257f3cfd7fc09","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"0000000000000000000000000000000000000000000000000000000000000000"}],"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":"183a7339fdd9bac0e2b12661cf639dab467be2d6a21d467d6933d556dc249509","sig":"71c9c68d08024cc06206ebce27eb487c786304e2a7af13bc1d2cb85d7aa8ee2a64e78c8b6abb12e3ef7f415da1556fcc047bee3c06602722ba39303a66420506","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"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/unknown-member.json b/wai/pack-conformance/mutations/receipt/unknown-member.json index 62ad41dd9..c0d6b21da 100644 --- a/wai/pack-conformance/mutations/receipt/unknown-member.json +++ b/wai/pack-conformance/mutations/receipt/unknown-member.json @@ -1 +1 @@ -{"extra":1,"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"a60d79a9fbd894d4205ae16459a14347c014582c7dd1c9849aa9981a766159a3","media_class":"video","stage_root":"33da550d0871b455ca8896fcbf05abeef35fc7d87dcb456e24b2f6d8ceca30ef"}],"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":"4751c6251c3912dba67f955521bfb2ecb607a80bb2ab4cf850a57371d336c6eb","sig":"87f206f39c14acbf78368ec7fee3b10420168131c38becf2a42a0e1c23236a330ff23a2c9672ec04df54c1084591661e6ad6f2a9d5691002aee16a7207060d0d","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"extra":1,"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"1e45bcf25bee6227e1181856886255443db421bec2de60ab831b3b354aca6c47","sig":"ed313a28956338de500172beea34e47a49306cde0cc0d8dcf3973ab5204e141e2ae8731335d8a58a7c9cdd5fb42111164a35d93f4e3d733cf877597053b3ea01","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file diff --git a/wai/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs index 44d307200..51eac73d4 100644 --- a/wai/wai-pack/src/catalog.rs +++ b/wai/wai-pack/src/catalog.rs @@ -443,3 +443,73 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol } 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(); + 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); + } + + #[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/golden.rs b/wai/wai-pack/src/golden.rs index aa2ad82fb..0fe214212 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -64,11 +64,10 @@ pub fn pack_golden(dir: &Path, name: &str) -> std::io::Result, derived: bool, payload: &[u8]) -> Vec { +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, layer)) = refines { - let b = hex(base); - m = m.with_refines(&Refines { base: b.clone(), prev: Prev::One(b), layer, span: None }); + 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) }); @@ -98,12 +97,12 @@ fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32 /// A two-piece index with a base, two refinements and a floor, then the base again and a /// floor; and its objects. -fn layered() -> (StageIndex, Vec>) { +pub fn layered() -> (StageIndex, Vec>) { let base = env("wai.video.int_motion", None, false, b"base"); let bh = objects::object_hash(&base); - let r1 = env("wai.video.int_refine", Some((&bh, 1)), false, b"r1"); + let r1 = env("wai.video.int_refine", Some((&bh, &bh, 1)), false, b"r1"); let r1h = objects::object_hash(&r1); - let r2 = env("wai.video.int_refine", Some((&r1h, 2)), false, b"r2"); + 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 entries = vec![ @@ -221,10 +220,9 @@ pub fn objects_verdict(index: &[u8], objects: &Files) -> String { return v; } let ix = StageIndex::parse(index).expect("checked"); - for e in &ix.entries { + for (k, e) in ix.entries.iter().enumerate() { let Some(b) = objects.get(&format!("{}.wai", hex(&e.content_hash))) else { return "object-missing".into() }; - let below = (e.refines != wsix::NO_REFINES).then(|| &ix.entries[e.refines as usize]); - if let Err(r) = wsix::check_object(&ix, e, below, b) { + if let Err(r) = wsix::check_object(&ix, k, b) { return r.code.into(); } } @@ -310,8 +308,9 @@ pub fn mutations() -> (Files, Value) { // 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[3]); - let r2 = env("wai.video.int_refine", Some((&bh, 2)), false, b"r2"); + 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, b"r2"); 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); diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 3bd5cee57..942746fa5 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -135,8 +135,7 @@ pub fn verify_files(files: &BTreeMap>) -> Result 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"]); + } +} + +/// 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 = golden_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); +} + +fn pack_with(policy: &[u8], royalty_free_only: Option) -> 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 = 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() +} + +/// 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, None)), "license-refused:wai.video.avc:patent-pool"); + // Without royalty_free_only it is not the licence that stops it. + assert_ne!(code(pack_with(&avc, Some(false))), "license-refused:wai.video.avc:patent-pool"); + assert_eq!(code(pack_with(&with_rung("wai.video.package", "floor"), Some(false))), "capability-not-payload:wai.video.package"); + assert_eq!(code(pack_with(&with_rung("wai.splat.sog", "base"), Some(false))), "capability-candidate:wai.splat.sog"); + assert_eq!(code(pack_with(&with_rung("wai.video.nope", "base"), Some(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 = golden_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-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs index 0aea1ff0f..3ff015396 100644 --- a/wai/wai-rs/src/stage_index.rs +++ b/wai/wai-rs/src/stage_index.rs @@ -777,16 +777,18 @@ pub fn digest_of(file: &[u8]) -> [u8; 32] { *h.finalize().as_bytes() } -/// The rules of §5 and §6 that need the object an entry names, `bytes`, and, for a -/// refinement, the entry it refines (`below`): 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` names the object of `below` at the -/// entry's layer (`object-refines`); and `derived_from` is present, well formed, exactly -/// when the entry says so (`object-derived-from`). -pub fn check_object(ix: &StageIndex, e: &EntryRec, below: Option<&EntryRec>, bytes: &[u8]) -> Result<(), IndexReason> { +/// 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)); } @@ -820,11 +822,23 @@ pub fn check_object(ix: &StageIndex, e: &EntryRec, below: Option<&EntryRec>, byt return r("object-capability", format!("the object's capability is {cap}, the entry's {}", ix.capability(e))); } if e.role == Role::Refinement { - let below = below.ok_or_else(|| IndexReason::new("object-refines", "a refinement with no entry below"))?; + // 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 && hex(&below.content_hash) == rf.base); + 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 the entry below at the entry's layer".into()); + 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(); diff --git a/wai/wai-rs/src/stage_index/tests.rs b/wai/wai-rs/src/stage_index/tests.rs index 53a81e5ca..1a702164a 100644 --- a/wai/wai-rs/src/stage_index/tests.rs +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -1,11 +1,10 @@ use super::*; use crate::container::{Conditioning, Manifest, ModelRequirement, Prev}; -fn env(cap: &str, refines: Option<(&[u8; 32], u8)>, derived: bool, payload: &[u8]) -> Vec { +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, layer)) = refines { - let b = hex(base); - m = m.with_refines(&Refines { base: b.clone(), prev: Prev::One(b), layer, span: None }); + 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) }); @@ -39,9 +38,9 @@ fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32 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, 1)), false, b"r1"); + 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((&r1h, 2)), false, b"r2"); + 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()]; @@ -97,9 +96,8 @@ fn an_index_round_trips_and_checks() { 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, e) in ix.entries.iter().enumerate() { - let below = (e.refines != NO_REFINES).then(|| &ix.entries[e.refines as usize]); - check_object(&ix, e, below, &objects[k]).unwrap_or_else(|r| panic!("entry {k}: {r}")); + for k in 0..ix.entries.len() { + check_object(&ix, k, &objects[k]).unwrap_or_else(|r| panic!("entry {k}: {r}")); } } @@ -217,16 +215,15 @@ fn a_malformed_file_is_refused_with_its_reason() { 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 mut e = ix.entries[0].clone(); - e.content_hash = crate::jwp_hash::content_hash(&cmaf); - e.byte_len = cmaf.len() as u64; - assert_eq!(check_object(&ix, &e, None, &cmaf).unwrap_err().code, "non-envelope-entry"); - let r = |k: usize, bytes: &[u8], f: &dyn Fn(&mut EntryRec)| { - let mut e = ix.entries[k].clone(); - f(&mut e); - let below = (e.refines != NO_REFINES).then(|| &ix.entries[e.refines as usize]); - check_object(&ix, &e, below, bytes).map(|_| "ok").unwrap_or_else(|r| r.code) + 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(); @@ -234,15 +231,23 @@ fn every_object_mutation_names_its_reason() { assert_eq!(r(0, &flipped, &|_| {}), "object-hash"); let mut trailing = objects[0].clone(); trailing.push(0); - assert_eq!(r(0, &trailing, &|e| { - e.byte_len += 1; - e.content_hash = crate::jwp_hash::content_hash(&trailing); + 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], &|e| e.payload_len += 1), "object-lengths"); - assert_eq!(r(0, &objects[0], &|e| e.capability = 4), "object-capability"); - assert_eq!(r(2, &objects[2], &|e| e.refines = 0), "object-refines"); - assert_eq!(r(3, &objects[3], &|e| e.flags = 0), "object-derived-from"); - assert_eq!(r(0, &objects[0], &|e| e.flags = ENTRY_DERIVED_FROM), "object-derived-from"); + 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] -- 2.54.0 (Apple Git-157) From 3b6cef4cb9b0779fdf4081c1ed481bf43be6be15 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:03:58 -0400 Subject: [PATCH 06/28] =?UTF-8?q?wai:=20packaging=20=C2=A75.1=E2=80=935.4?= =?UTF-8?q?=20and=20=C2=A76=E2=80=93=C2=A711=20as=20built,=20the=20reasons?= =?UTF-8?q?=20of=20=C2=A712;=20SPEC=20=C2=A77.1=20Derived=20renditions=20a?= =?UTF-8?q?nd=20its=20=C2=A78=20member;=20wai.video.package=20version=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - packaging.md: objects (one envelope, position independence, the name, refines on a refinement); the stage index layout, its rules and reasons, tiers, prefix digest and SSE; the floor, derived_from on floor objects, a supplied floor and its checks, the keyframe plan, the CMAF files; what a class lists, the HLS and DASH files in http-delivery's Mode N, the MoQ catalog, priority, deadline, the stage view; the version-2 receipt, its JSON, verification and reasons, v1 JSON, the measurement claim; royalty-free packaging; the package directory, re-pack equivalence and verification; what this revision packs. - SPEC: §7.1 Derived renditions; §8 lists derived_from among the OPTIONAL members that never break a parse; the wai.video.package row names version 2. - verify: an index whose timebase is not its component's is index-policy-mismatch. Co-Authored-By: Claude Opus 5.5 --- wai/SPEC.md | 33 ++- wai/extensions/packaging.md | 543 +++++++++++++++++++++++++++++++++++- wai/wai-pack/src/verify.rs | 2 +- 3 files changed, 563 insertions(+), 15 deletions(-) diff --git a/wai/SPEC.md b/wai/SPEC.md index 6b6d5d5c2..e41ced335 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, @@ -1875,6 +1875,33 @@ 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. 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 @@ -1971,7 +1998,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..038ba5967 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. @@ -278,15 +281,34 @@ 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`, the rung's capability, no +fallback, `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). + +**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 +354,500 @@ 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 | + +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. The reference +packager writes the component's id first, then the class ids in policy order, then each +capability at its first use. + + 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: +another magic (`index-magic`) or version (`index-version`); a table that runs past the +end, its size taken from its count before anything is allocated (`index-truncated`); +reserved or padding bytes that are not zero (`index-reserved`); a media class, role or +tier with no meaning (`index-media-class`, `index-role`, `index-tier`); a string that +is empty or not UTF-8, or a string index past the table (`index-string`); bytes after +the string table (`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`). 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, roles out + of the order base, refinement, floor, any other, or refinements out of layer order + (`entry-order`); a base or a floor 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. Not one plan row per class (`class-plan`). Then class by class: a flag with no + meaning (`class-flags`); a row of another length than the pieces, or a 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 `refines` does not name 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, or well formed, otherwise +than bit 2 says (`object-derived-from`). + +**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 that states decode-equivalence over a prefix that + does not hold it is refused `tier-weakest`; any other 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. + +## 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. 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, a unit that starts at a key frame without a sequence + header, or a sequence header other than the first (`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, the + unity matrix, the width and height in 16.16; `mdia`: `mdhd` version 0, timescale + `tb_num`, language `und`; `hdlr` `vide` named `video`; `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), `…/wai/class` (the class) and `…/wai/stages` (`../stages/.json`), then + per layer *k* up to the highest the class lists, `` (with + `dependencyId="-l"` for *k* > 0) and `bandwidth` its peak rate, 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). + +**8.4** *MoQ.* `moq/catalog.json` is RFC 8785 canonical JSON and a line feed: +`{"tracks": [...], "version": 1}`. 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. A refinement track `depends` on +`["/l"]` and 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 provisional names, under the +`wai_` prefix of [moq-streaming-format](moq-streaming-format.md) §4. + +**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": ""}`, 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`; and, when labelled, the class's members (`energy_provenance` +and its evidence, energy-measurement §2.7). A reader refuses (`receipt-json`) a member +it does not define or one missing, a value of another form, a parent kind and hash that +disagree, an `index` other than `index-.wsix`, and a `receipt_hash` that is not the +receipt's. A `"version"` other than 1 or 2 is `receipt-version`. + +A verifier of a receipt against its indexes, in its components' order, refuses, the +first that applies: another number of indexes (`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`); other counts (`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. +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: + + 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` 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, refuses, in this +order: a missing `LISTING` (`listing-missing`) or one that is not the listing of the +other files (`listing-mismatch`); a receipt that does not read as version 2 +(`receipt-json`, `receipt-version`); a `policy.json` that does not hash to its policy +digest (`policy-digest-mismatch`) or is not `policy.toml`'s effective canonical form +with its `royalty_free_only` (`policy-json-mismatch`); a missing index +(`index-missing`) and the receipt's own refusals (§9.1). Then per component: an index +whose timebase is not the policy's component's (`index-policy-mismatch`); a plan file +that is not the index's plan (`plan-mismatch`); an entry's missing object +(`object-missing`) or one that breaks §6.3; a decode-equivalent prefix that does not +decode to its `prefix_digest` (`prefix-digest`); a floor object that does not present +its piece's base (`floor-motif-mismatch`), and CMAF files that are not the ones its +temporal units give (§7.6, `floor-cmaf-mismatch`); and catalogs that are not the ones +the index, the objects and the floor give (§8, `catalog-mismatch`, and +`catalog-unexpected` for one more). Then a metered package's claim (§9.4), an object +file no entry names (`object-unlisted`), and 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:`); and a component with no source +(`pack-source-missing`) or whose source's frame rate is not its timebase +(`pack-source-rate`). ## 12. Reasons @@ -347,3 +858,11 @@ 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`, `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 | +| `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 | +| `listing-missing`, `listing-mismatch`, `policy-digest-mismatch`, `policy-json-mismatch`, `index-missing`, `index-policy-mismatch`, `plan-mismatch`, `object-missing`, `prefix-digest`, `floor-motif-mismatch`, `floor-cmaf-mismatch`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted`, `unexpected-file` | §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` | §11.1, §11.4 | diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 942746fa5..0636608ed 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -115,7 +115,7 @@ pub fn verify_files(files: &BTreeMap>) -> Result Date: Tue, 6 Oct 2026 06:07:05 -0400 Subject: [PATCH 07/28] wai: the registry lists packaging's wire identifiers (WSIX, the class and stage-view SESSION-DATA ids and DASH schemes, wai_layer and wai_delivery_timeout_ms) Co-Authored-By: Claude Opus 5.5 --- wai/REGISTRY.md | 12 ++++++-- wai/extensions/packaging.md | 9 ++++-- wai/registry/capabilities.json | 49 +++++++++++++++++++++++++++++++ wai/wai-rs/src/codecs/registry.rs | 14 +++++++-- 4 files changed, 76 insertions(+), 8 deletions(-) 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/extensions/packaging.md b/wai/extensions/packaging.md index 038ba5967..25f2eee35 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -663,7 +663,8 @@ one `AdaptationSet`, `` and ``, indented by two spaces a level: `` holding ``, ``s of schemes `tag:transaction.science,2026:wai/floor` (value the - floor), `…/wai/class` (the class) and `…/wai/stages` (`../stages/.json`), then + 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, `` (with `dependencyId="-l"` for *k* > 0) and `bandwidth` its peak rate, whose `SegmentList` has a timeline entry and a `SegmentURL media="../objects/.wai"` @@ -684,8 +685,10 @@ track has `altGroup` 1 when the component has a floor. A refinement track `depen `/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 provisional names, under the -`wai_` prefix of [moq-streaming-format](moq-streaming-format.md) §4. +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 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-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 -- 2.54.0 (Apple Git-157) From 238c8bacea0b1b582630ca0314567911ddf2c65d Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:08:10 -0400 Subject: [PATCH 08/28] wai: CHANGELOG and the corpus README for packages Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ wai/pack-conformance/README.md | 19 ++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 1176f4e33..80333a7f1 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -106,6 +106,39 @@ 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 `#WAI-LAYER` lines and `#EXT-X-GAP` for a + piece the class takes as floor only; 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/pack-conformance/README.md b/wai/pack-conformance/README.md index 663b82419..2bc0df15c 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -36,6 +36,19 @@ 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. + 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 +57,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,7 +72,9 @@ 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 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 -- 2.54.0 (Apple Git-157) From 915b3340bd75d45c2316a3cacb0eb82152fee732 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:09:14 -0400 Subject: [PATCH 09/28] ci: wai-pack's media job reads and decodes the golden's floor; the independent job installs the Ed25519 library; the V2 figure-view test needs receipt_claims Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-pack.yml | 49 +++++++++++++++++++++++++++++---- wai/wai-rs/src/package/tests.rs | 1 + 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 6fd8ec5b4..7fc4d39f8 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. # @@ -109,8 +113,12 @@ 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, as the claim verifiers check it. + - name: Install the Ed25519 library + shell: bash + run: python -m pip install cryptography - name: pack_verify.py working-directory: wai shell: bash @@ -150,6 +158,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/wai-rs/src/package/tests.rs b/wai/wai-rs/src/package/tests.rs index 0caa39ff4..3a08cb259 100644 --- a/wai/wai-rs/src/package/tests.rs +++ b/wai/wai-rs/src/package/tests.rs @@ -249,6 +249,7 @@ fn the_v1_json_round_trips_and_keeps_the_legacy_pin() { /// 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}; -- 2.54.0 (Apple Git-157) From e8d0469227f2a08829df7a43981f05811fa18a66 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:09:50 -0400 Subject: [PATCH 10/28] wai-pack: clippy clean under -D warnings Co-Authored-By: Claude Opus 5.5 --- wai/wai-pack/src/floor.rs | 15 +++++++++------ wai/wai-pack/src/golden.rs | 1 + wai/wai-pack/src/pack.rs | 6 +++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/wai/wai-pack/src/floor.rs b/wai/wai-pack/src/floor.rs index cf63e9120..b47c1b180 100644 --- a/wai/wai-pack/src/floor.rs +++ b/wai/wai-pack/src/floor.rs @@ -78,10 +78,10 @@ pub fn floor_supplied(files: &BTreeMap>, listing: Option<&str>, } let mut key = Vec::with_capacity(tus.len()); for (i, tu) in tus.iter().enumerate() { - if let Some(s) = tu.sequence_header() { - if s.bytes != seq_obu.as_slice() { - return refuse("floor-av1-malformed", format!("temporal unit {i} carries another sequence header")); - } + 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() { @@ -122,9 +122,12 @@ pub fn av1_payload(units: &[Vec], h: u32, w: u32, fps_num: u32, fps_den: u32 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's `(h, w, fps_num, fps_den)`; `None` when it is not that framing. -pub fn av1_units(payload: &[u8]) -> Option<(Vec>, (u32, u32, u32, u32))> { +/// 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)?); diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index 0fe214212..14ca1b0bc 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -75,6 +75,7 @@ fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, pa 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 { diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index 25e3c3b0e..3f8bca972 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -167,9 +167,13 @@ fn entry(env: &[u8], piece: u64, capability: u16, role: Role, prefix_tier: u8, p } } +/// 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<(String, Vec, Vec<(String, Vec)>), String> { +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() { -- 2.54.0 (Apple Git-157) From bca471be2cfd0825ebac215160ad76fc83fbe465 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:11:47 -0400 Subject: [PATCH 11/28] wai-pack: tests 7 and 14 read a fresh pack Co-Authored-By: Claude Opus 5.5 --- wai/wai-pack/tests/packs.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wai/wai-pack/tests/packs.rs b/wai/wai-pack/tests/packs.rs index e3a90be8d..88910d599 100644 --- a/wai/wai-pack/tests/packs.rs +++ b/wai/wai-pack/tests/packs.rs @@ -53,11 +53,16 @@ fn every_mutation_names_its_reason() { } } +/// 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 = golden_package(); + 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; @@ -147,7 +152,7 @@ fn child<'a>(b: &'a [u8], path: &[&[u8; 4]]) -> &'a [u8] { /// after (the supplied floor has no extra key frame). #[test] fn the_floor_s_cmaf_files_hold_every_frame() { - let pkg = golden_package(); + let pkg = fresh_package(); let ix = StageIndex::parse(&pkg["index-v.wsix"]).unwrap(); let init = &pkg["floor/v/init.mp4"]; let top = boxes(init); -- 2.54.0 (Apple Git-157) From d54753279c3a831a7cdfa6e8b02b590cbc8f4cf8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:23:22 -0400 Subject: [PATCH 12/28] wai-rs: the derived_from test's malformed motif is 64 uppercase digits, so only the lowercase rule refuses it Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/src/container.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wai/wai-rs/src/container.rs b/wai/wai-rs/src/container.rs index 0d2ce7004..eb3181326 100644 --- a/wai/wai-rs/src/container.rs +++ b/wai/wai-rs/src/container.rs @@ -4246,7 +4246,7 @@ mod tests { 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"); + 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 [ -- 2.54.0 (Apple Git-157) From 753051923ede25d9aee58622617057122f700dc7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:24:29 -0400 Subject: [PATCH 13/28] ci: wai-pack's test-count floors for the packer's tests Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-pack.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 7fc4d39f8..5c88b98fa 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -94,8 +94,9 @@ 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 26 --test=conformance 1 + --test=packs 7 --test=scenecut 3 --test=source_alloc 1 EOF -- 2.54.0 (Apple Git-157) From 25b6fda432bd3a6d5029ea6392657cec9785ec28 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:29:12 -0400 Subject: [PATCH 14/28] wai-pack: an object's lengths are its counted bytes (staged measurement's count_object) where WAI checks its payload's format; frame digests through output_tree Co-Authored-By: Claude Opus 5.5 --- wai/extensions/packaging.md | 8 +++++++- wai/wai-pack/Cargo.toml | 8 ++++---- wai/wai-pack/src/pack.rs | 30 ++++++++++++++++++++++-------- wai/wai-pack/src/verify.rs | 6 ++++++ 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 25f2eee35..1434f8e06 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -416,6 +416,11 @@ bytes are zero. | 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), and a verifier also requires the +payload to parse as that format (`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 @@ -666,7 +671,8 @@ one `AdaptationSet`, `` and ``, indented by two spaces a level: 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, `` (with - `dependencyId="-l"` for *k* > 0) and `bandwidth` its peak rate, whose + `dependencyId="-l"` for *k* > 0) 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). diff --git a/wai/wai-pack/Cargo.toml b/wai/wai-pack/Cargo.toml index 21cad1c87..e9d59370f 100644 --- a/wai/wai-pack/Cargo.toml +++ b/wai/wai-pack/Cargo.toml @@ -21,10 +21,10 @@ path = "src/bin/wai_pack.rs" [dependencies] # 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); 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", "receipt_claims", "energy_meter"] } +# (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"] } diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index 3f8bca972..fff6fbdb0 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -136,20 +136,34 @@ pub fn clip_digest>(frames: &[F]) -> [u8; 32] { use sha2::Digest as _; let mut h = sha2::Sha256::new(); for f in frames { - h.update(sha2::Sha256::digest(f.as_ref())); + 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) -> EntryRec { - let man = u32::from_le_bytes(env[4..8].try_into().expect("an envelope")); - EntryRec { +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, @@ -163,8 +177,8 @@ fn entry(env: &[u8], piece: u64, capability: u16, role: Role, prefix_tier: u8, p prefix_digest, sse, manifest_len: man, - payload_len: env.len() as u64 - 12 - man as u64, - } + payload_len, + }) } /// A component's CMAF files: the initialization segment's path and bytes, and each @@ -296,13 +310,13 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut 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 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); + 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, diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 0636608ed..47f92f24b 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -139,6 +139,12 @@ pub fn verify_files(files: &BTreeMap>) -> Result Date: Tue, 6 Oct 2026 06:40:07 -0400 Subject: [PATCH 15/28] pack-conformance: pack_verify.py verifies the golden package, the mutations and the version-1 receipts from the extension text; the mutation objects carry real WIV1 and WIR1 payloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pack_verify.py, written from packaging §5–§11, SPEC and the bindings it cites and sharing no code with wai-pack: the stage index's layout and rules, every object rule, the version-1 and version-2 receipts (payload, hash, Ed25519 through the cryptography package, imported only where a signature is checked), the supplied AV1 floor and its key frames, the floor objects and the CMAF files rebuilt, every catalog re-rendered, and the package's verification in §11.3's order. From the regenerated fixture it also re-derives what only the source decides: the pieces, the rules, moments and class plan, each base's SSE and step, and the encoder's choices. The mutations' base and layers now carry real WIV1 and WIR1 payloads, so they meet §6.2's counted-bytes rule, which a verifier applies after the other object rules; new cases object-hash (a misnamed file) and object-lengths (a base whose payload is not its format). wai-pack's objects verdict applies the rule too. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/packaging.md | 5 +- wai/pack-conformance/mutations/expected.json | 14 + .../mutations/index/base-with-refines.wsix | Bin 1088 -> 1088 bytes .../mutations/index/class-flag-unknown.wsix | Bin 1088 -> 1088 bytes .../mutations/index/deadline-on-base.wsix | Bin 1088 -> 1088 bytes .../mutations/index/digest-without-tier.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-flag-lie.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-flag-missing.wsix | Bin 1088 -> 1088 bytes .../mutations/index/dup-length.wsix | Bin 1088 -> 1088 bytes .../mutations/index/entry-flag-unknown.wsix | Bin 1088 -> 1088 bytes .../index/entry-in-another-piece.wsix | Bin 1088 -> 1088 bytes .../mutations/index/floor-at-layer-1.wsix | Bin 1088 -> 1088 bytes .../index/floor-before-refinement.wsix | Bin 1088 -> 1088 bytes .../index/floor-without-derived-from.wsix | Bin 1088 -> 1088 bytes .../mutations/index/layer-gap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/magic.wsix | Bin 1088 -> 1088 bytes .../mutations/index/media-class.wsix | Bin 1088 -> 1088 bytes wai/pack-conformance/mutations/index/ok.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-empty.wsix | Bin 1088 -> 1088 bytes .../index/piece-entries-overlap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-gap.wsix | Bin 1088 -> 1088 bytes .../mutations/index/piece-kind-unknown.wsix | Bin 1088 -> 1088 bytes .../index/plan-floor-only-class.wsix | Bin 1088 -> 1088 bytes .../mutations/index/plan-past-class.wsix | Bin 1088 -> 1088 bytes .../mutations/index/plan-past-piece.wsix | Bin 1088 -> 1088 bytes .../mutations/index/refines-flipped.wsix | Bin 1088 -> 1088 bytes .../index/refines-outside-window.wsix | Bin 1088 -> 1088 bytes .../mutations/index/reserved.wsix | Bin 1088 -> 1088 bytes .../mutations/index/role.wsix | Bin 1088 -> 1088 bytes .../mutations/index/string-index.wsix | Bin 1088 -> 1088 bytes .../index/tier-decode-over-entropy.wsix | Bin 1080 -> 1080 bytes .../index/tier-entropy-over-entropy.wsix | Bin 1080 -> 1080 bytes .../mutations/index/tier-on-floor.wsix | Bin 1088 -> 1088 bytes .../mutations/index/tier.wsix | Bin 1088 -> 1088 bytes .../mutations/index/timebase-zero.wsix | Bin 1088 -> 1088 bytes .../mutations/index/trailing.wsix | Bin 1096 -> 1096 bytes .../mutations/index/truncated-table.wsix | Bin 613 -> 613 bytes .../mutations/index/version.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/capability.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/cmaf-listed.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/derived-from.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/hash.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/length.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/lengths.wsix | Bin 1088 -> 1088 bytes .../mutations/objects-index/missing.wsix | Bin 1088 -> 1088 bytes .../objects-index/not-its-format.wsix | Bin 0 -> 1088 bytes .../mutations/objects-index/ok.wsix | Bin 1088 -> 1088 bytes .../objects-index/refines-names-another.wsix | Bin 1088 -> 1088 bytes ...9b7de5c6daf311bf5eea5121c0c6870829408d.wai | Bin 0 -> 564 bytes ...4f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai | Bin 334 -> 0 bytes ...d593c8442644263d45ffed1f416040b9de7198.wai | Bin 166 -> 0 bytes ...a5137aa92482479325a3bf227511939aa3b6c7.wai | Bin 334 -> 0 bytes ...9822496608b637c1aca7229d12690781a69f2e.wai | Bin 0 -> 434 bytes ...04dd11fb156adee4c925fbf712b61819148fc9.wai | Bin 0 -> 564 bytes ...9118d30146f8152d15f91f8f307d9024620608.wai | Bin 0 -> 434 bytes ...9118d30146f8152d15f91f8f307d9024620609.wai | Bin 0 -> 434 bytes ...9380e53c7386d65f858f1c002f166c9d8b3fa9.wai | Bin 0 -> 532 bytes ...6f130b4cec57f2c87578f3ee3dddedb3931c28.wai | Bin 334 -> 0 bytes ...a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai | Bin 166 -> 0 bytes .../mutations/receipt/counts.json | 2 +- .../mutations/receipt/energy.json | 2 +- .../mutations/receipt/index-digest.json | 2 +- .../mutations/receipt/index-v.wsix | Bin 1088 -> 1088 bytes .../mutations/receipt/live/index-v.wsix | Bin 1088 -> 1088 bytes .../mutations/receipt/media-class.json | 2 +- .../mutations/receipt/ok-labelled.json | 2 +- .../mutations/receipt/ok-live-window.json | 2 +- .../mutations/receipt/ok.json | 2 +- .../mutations/receipt/parent-kind-live.json | 2 +- .../mutations/receipt/parent-kind.json | 2 +- .../mutations/receipt/policy-digest.json | 2 +- .../mutations/receipt/receipt-hash.json | 2 +- .../mutations/receipt/signature.json | 2 +- .../mutations/receipt/stage-root.json | 2 +- .../mutations/receipt/unknown-member.json | 2 +- wai/pack-conformance/pack_verify.py | 1562 ++++++++++++++++- wai/wai-pack/src/golden.rs | 63 +- 77 files changed, 1642 insertions(+), 30 deletions(-) create mode 100644 wai/pack-conformance/mutations/objects-index/hash.wsix create mode 100644 wai/pack-conformance/mutations/objects-index/not-its-format.wsix create mode 100644 wai/pack-conformance/mutations/objects/03707889bfb7d683de788771279b7de5c6daf311bf5eea5121c0c6870829408d.wai delete mode 100644 wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai delete mode 100644 wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai delete mode 100644 wai/pack-conformance/mutations/objects/3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai create mode 100644 wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai create mode 100644 wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai create mode 100644 wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai create mode 100644 wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai create mode 100644 wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai delete mode 100644 wai/pack-conformance/mutations/objects/befc8683e2cc0f4c581568b50e6f130b4cec57f2c87578f3ee3dddedb3931c28.wai delete mode 100644 wai/pack-conformance/mutations/objects/fae6cf7e7d39c8a77e06f86929a1b1f9e0e00a503ec25915f5b0920d4518a07b.wai diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 1434f8e06..0104cc5e8 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -418,8 +418,9 @@ bytes are zero. `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), and a verifier also requires the -payload to parse as that format (`object-lengths`). +([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 diff --git a/wai/pack-conformance/mutations/expected.json b/wai/pack-conformance/mutations/expected.json index e0bd0fcee..686506f00 100644 --- a/wai/pack-conformance/mutations/expected.json +++ b/wai/pack-conformance/mutations/expected.json @@ -237,6 +237,20 @@ "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", diff --git a/wai/pack-conformance/mutations/index/base-with-refines.wsix b/wai/pack-conformance/mutations/index/base-with-refines.wsix index fe5b164553ae96be172c514a63b5e7a452a98763..f85a564ad9f57829434c582be0b857a9864ea019 100644 GIT binary patch delta 213 zcmX@Wae!mO1I79hi@v*&Zc7DreEKIeQQ|VA+YeD)(Vz1D2DKAZlGr#mO=e^go-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8STx<+Q8 P>zHkr1Q;g=F~0)<6D~@t delta 210 zcmX@Wae!mO10}_~c@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM758N*~IMn5AK z22>!rH}8M(-v!l2%#S`v^Y`gnKkv-GsspPW!??~&J$=`$!;f)t6QiFY6RIwTeSg}T zAD!X%i4e`$%9k(9?eix5(}~iG&+lySzTG@oM#FD1Bct$S5hfc(rpbX!{Y1Hr*^h~Z JVR8`jI{-1{PzeA4 diff --git a/wai/pack-conformance/mutations/index/class-flag-unknown.wsix b/wai/pack-conformance/mutations/index/class-flag-unknown.wsix index 6234c275ba15b3ab15d58a62279f2758f87998d7..e5a72f2b07f55bb62975b6949275d82481b9a781 100644 GIT binary patch delta 213 zcmX@Wae!mO1I79hi@v*&Zc7DreEKIeQQ|VA+YeD)(Vz1D2DKAZlGr#mO>Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oDP!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%crz Mm;@Ln2Qj|`0BIacDF6Tf delta 217 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8R5-R8-S R%s|&M+b{_*P7Y#z2LNe4ODq5Y delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW9DFbRFYl OCT2e-0mjKe%f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4y zV$K-`X$3pf8p)x7&WD2-D;yMnA?AlP@y% zD^=g0a4b0`p+VBh_tSZnyMn((v+g}Psrvi7&^8H4k^Yk=K*dachA&WTU|=BZx<+Q8 P>zHkr1Q;g=F~0)f{qHYA$NFuK(Z4J0>{nyjM75*2Ua zmG=ESfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYKLY?;=1UU* delta 210 zcmdnNv4dm614YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75*2UaYZ1A)2w3FJGA3=S}#h6QvcO-`U=MyLqyV22dLllOH3~Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYKLY?;=1UU* delta 210 zcmdnNv4dm614YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75*2UaYZ1A)2w3FJGA3=S}#h6QvcO-`U=MyLqyV22dLllOH3~Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oDP!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYe**w^c1uG5 delta 210 zcmX@Xae`yQ14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ysm49~Z-R{(9xsw~7+7!24i*N0hVbB-LncHo@QiN%86Hwa;6io~a)%PbH uOHN5>khJpsbe`p|;BV2adrwZP{{AksO+r$n|D*}iWF{s*#ut+VnM45S95>Ja delta 161 zcmaFL@|0!510}_~c@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM758N*~IMn5AK z22>!rH}8M(-v!l2%#S`v^Y`gnKkv-GsspPW!??~&J$=`$!;f)t6HprysxF3of7+TK so#FS15Y5=imoLoi^CtY$iPDPC?`-eB-8@-F1E`IO$&Zm~av+lk0L-mB(*OVf diff --git a/wai/pack-conformance/mutations/index/version.wsix b/wai/pack-conformance/mutations/index/version.wsix index da3baaade95fa58559f01a937534daf4c4204d1c..1bea7fc74fd1befbe0a456a212ea6b10e9ec5fcc 100644 GIT binary patch delta 213 zcmX@Wae!mO1I79hi@v*&Zc7DreEKIeQQ|VA+YeD)(Vz1D2DKAZlGr#mO>Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ysm49~Z-R{(9xsw~7+7!24i*N0hVbB-LncHo@QiN%86Hwa;6io~a)%PbH zOHN5>khJpsbe`p|;BV2adrwZP{{AksO+r$n|D*}iWF{s*#ut+Vnfet8I&E?zv+!gQ NW*a5}#>qjf{qHYA$NFuK(Z4J0>{nyjM75+2lqh;mIP5 zHbyKAs6cdY-v8pi3#yNpAAOSM@6)$_-kE(>2UaYZ1A)2w3FJGA3=S}#h6QvcO-`U=MyLqyV22dLllOH3~Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ywY%gF66;wm=RQh1x*1=9OQnG91{c3!ANTAODy{CM!GE(*zxI~&_s#LjBYxI^h^&SofrJm`zE-txX)4o-$`&ZbT(21YBe%ZC7g%`*_ETGUwk7zV=VO*I1OQ7bk z!z7?IvGEUc2FN^+-@yR@q=^lGT=DO?!}I<#BdsT{lXqvmwauPZc{Kg|X9deDA4k=U z<~U<5nE#ROr>6fI;VBssAh76WWGG`|C@B+RC{N7PE6Yqt&DYD!D~Zp|FUic$Ll-Sd RP0P$n zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJei>`N^G delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75`Q(jE!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@87BuZzXJei*h?({ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nMP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zBOt)YfC45bGRiZ*6JeU%#OP;q0!4~}q5A%WW63EA4U$&ApU$(~75pulb??bZ)!*NR mwn<2e^q(|gn#{!H$M|A$AX7gvZeRwwf!T&hfN^pV^E&_(SUpJq delta 184 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZ3I{tP{HIxMtSCUevFfw82yZxP~{l*{b_4{bcWw2LNsG5U%oK6&zta1CrT?mzq7sj kcJpKz4WKqACO<}|$$?D$N(9}&Fqw(jkBNn0auD-70O0CCZU6uP 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 0000000000000000000000000000000000000000..1ecfbca9b77bfc71df88c157431f70f3e0483f0a GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANSdYhR~Yd3?Onb8(}m(hMcfG>&cN z2iGiDnk$sa-neYO-X>(lKz;v#02Brw3JYQA1(UP*jzeo1D29=d2z RYFcJqDld}S#4u*%(NO>Z literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects-index/ok.wsix b/wai/pack-conformance/mutations/objects-index/ok.wsix index c476d02fe4dabeb1da9ff6e6ae0e6a46d74186be..f21dc31108d11742f6233625c9d003605af9317b 100644 GIT binary patch delta 213 zcmX@Wae!mO1I79hi@v*&Zc7DreEKIeQQ|VA+YeD)(Vz1D2DKAZlGr#mO>Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zmf{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4yBJVZ}uWY8A#Jj6sM*IZ!@}MGBR;2+l>KQdN;CuJr5JXJzK8K*cqn z!7{sIkp%-Fr3wXZbYZ^~auusmsKNo9tK&fxa==H@-al$6l|42jKz5{K8k7poE!VV} zMKq5}T?(SRgivm1#Iz+Pmd=SKT_UM8G>5yIZrZwLYc7mhj>IrHdDIcM>oP%g#wn8~ zH8r0QCTv}p7Bd`^Ylcr9&(dw`NoLu?w=A$U0A@z|>44QlgmXGf8nb9Xz*53$5H#iy zLWSNFBm=u_f%m%ASOfY0ffo9cBuVS4aH<`lCwYWU4kDD?fOw10;~$Wd6v&`MaHmn; z!WT~O$)H)(_WgXVIbN7*eUzQ+?77G)A6{2R);Hfxf8O2kpcb8(e)i%{Zh3yOar5$# zjm3O*X?w@}p1!`d->%MykHz~3y07dwhS09#orRxIb7P~6doL}~+lMz#?Nd8HEnFYl t)HXBTfA7`o#M$P=m#ve_2RDqqss5U2zxDNQ_wS?Ip7WM#jmd{E{{nKKxLW`K literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai b/wai/pack-conformance/mutations/objects/15bd6eff73fda07bc437c5e4664f4c8eaf9eccbe7ac0aa41560acc95cbdd4688.wai deleted file mode 100644 index 917b58f633f07195137570c140319bfe88fb4b56..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 334 zcmb`Dv2MdK3`Es_3g^I(Y}u7PwPU`ZV7 z{Y9x2Zk}NPQ)$@2k&wNpDmU1EH^Dy6dd E1-+YNga7~l diff --git a/wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai b/wai/pack-conformance/mutations/objects/21dd6e90eb3828e3685044e4b1d593c8442644263d45ffed1f416040b9de7198.wai deleted file mode 100644 index 1be50f23e074b9ea61a2eef7c2269be8fb3074db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 166 zcmW-bJr08~423iFB%V=JR<@2kK}Tc~3t3GZnwtJ7;u<|~4dZ+7(`Vl<*Z$|6Yw$*f z0eXLi4l+eXOD~R8EVC$yT2X0998hV8N~APadaPV2AA5s<3r?I^zebzV9|~`RF&E_` iuyqiMMBW3-3`2uEOEmnmZ;Ev$J0)vM+sQtg@JWAw>o*<% diff --git a/wai/pack-conformance/mutations/objects/3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai b/wai/pack-conformance/mutations/objects/3529b31f87c15c44b73a7e2ee6a5137aa92482479325a3bf227511939aa3b6c7.wai deleted file mode 100644 index e59f8bcace17c4d21abefcf67351ee44bb27d7f3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 334 zcmXAlJ#ND=3`X4^MRQ>2-%_S_>^(UZ`G*oj779;O<5lXYQ#kSQic&o+cV$!3~--PG1erS88~C&Q~$Q2@*}T+?i%& z|5WrWV6sp}@z{xRu9+5^E7gP8ui~3(X8d=X{Zp4MPH;hvwaVFJl$cD=0gVq%T8lQR z?4lWiwc2lGvUR0kR>dkWtxPfFpkm-vPYrKuN?|}BBVf>w0~8yq9F+p}8m$8}*eqE% pB&U5y1)Rsxxu}X^7|eF`1b1v=uX)o2+5!0~^*T+D*RJcD{sRvRWY+)y diff --git a/wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai b/wai/pack-conformance/mutations/objects/6b7deb653a48f1dc8cd1b149229822496608b637c1aca7229d12690781a69f2e.wai new file mode 100644 index 0000000000000000000000000000000000000000..ca2a07ed6b9c79e043b56dd1fbfac6d4b6ff4527 GIT binary patch literal 434 zcmeH@F%H5o3`N5YID_XZqH8xsPQX~1#HuV3r$i|QMZE^+#p%W!@b>(_zQ24uU;2+R zroxI01N8O;9YpesN>BEblvxT%(M6^?u){*llstto(_`UGan~z2obkqi%Uj(VY4-@L p0b@*slfc%2OX9f!%LGG(JBwHQt7!%EM0!e=5Vqq%tNrKa17DM5HIM)R literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai b/wai/pack-conformance/mutations/objects/7bdf90c663646080193a4df2cf04dd11fb156adee4c925fbf712b61819148fc9.wai new file mode 100644 index 0000000000000000000000000000000000000000..3b31d181d41dd2aae396bb5057ed8f140594e5bd GIT binary patch literal 564 zcmXv~L1+^}6rEHMdr`5rf+*-5ieO1+b~BUNAR0leMMacqyr`YsnJHs8n`FDGjUl%l zY!DSe#a6JQpd!+Pq6bCoSv+`BiXasVRk0@zJ=CtVwGO=b|G)Qme12hnFV3d`sKX0f z!yLqE1iN5S3eC+ltc5I@SqG5}B143->}!ulGBK5e^v{K|Qq`d>uIP2>@iKR{ucMlA zo5=J^MA`%xr_zChjV|bw0-+;a3Un|4b9FeV17YkViT2Mml+Z38l7`NzY{q6x3PGF< z6ZuC8*?#25$j){KM2ZF{HpD^Cu{8 z5eujHq`)j_`+mOG94}0@K1$DZ_FUwZ53egD>znVUKkx2%Pz%pYKYMW}w>-btxOw@= z#$vv@w7uhfPha2KZ&&B!$Kw41-B)%T17O$j&ce^9xv|m3y_c5Q?Zca=_UWCU7OszN tYMUAFzxQf(;%sx`%ht)|gBwQQRDaF1-}-vD`}fgp&qd3%#^l47e*sdcxHkX* literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620608.wai new file mode 100644 index 0000000000000000000000000000000000000000..ded799e859d58813ba3f0ae9a30d074614e7187f GIT binary patch literal 434 zcmWG{^fa8tz`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LpGM4FX}zK(#;|0mP9&90kN1_!t;iWf&Nu)fpJrOc)qq?1AbQGcf$S z%)qen0T90d;vWnQ|Nnzb09t|uCII=TdHww(mjlhUsfhlT(E literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai b/wai/pack-conformance/mutations/objects/7f74388edd5946a510b8f2fe129118d30146f8152d15f91f8f307d9024620609.wai new file mode 100644 index 0000000000000000000000000000000000000000..ded799e859d58813ba3f0ae9a30d074614e7187f GIT binary patch literal 434 zcmWG{^fa8tz`#(gRGyfrWTj-NXP~5`l$)B8nF!>SWu~O&1NoVGC8>EOK!Kvvf}G6c z#FA8yqWqN9ocN;D!qUv5)LgK9wNi3oL1I#7PG(6ZP&LpGM4FX}zK(#;|0mP9&90kN1_!t;iWf&Nu)fpJrOc)qq?1AbQGcf$S z%)qen0T90d;vWnQ|Nnzb09t|uCII=TdHww(mjlhUsfhlT(E literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai b/wai/pack-conformance/mutations/objects/a538ac08b51fe6c6eebb65e66d9380e53c7386d65f858f1c002f166c9d8b3fa9.wai new file mode 100644 index 0000000000000000000000000000000000000000..e2d941c304ae50da6e1534f81aedb9a3f4dccfd5 GIT binary patch literal 532 zcmWG{^fYu}WMHUPDo@N*vQjeCGf>h|%1uqlOa$`EGE-9Xf&9$8lGMBspg>V-K~83J zVo54UQGQBlPJB^nVQFSjYA#s5S}8fPATcR3C$ppys2XUb9@q#ypaDRYX_+bo%_uF^(8$uz&?3dyz|h1j&BD;sD81=FfcHLdj=UY0>hFC$mW1zW*`j%Y(U1p z|NsB9!vxeA7(z1{82-;^U|4wvNdE^(Ff!NxNf540DCeHz#m{hNNs1|}M!3=GhF>n# z`bW&H@2Dy|+&o!p_4oM%XFCt2IU04#9JVsds6Q`#((p>iuhz6{HovAxKlXko!SLxp zQI4(IY34-dDPBi6u{C(`=N4Y%y<9dodfn77n>Jq+xN5rc?Zb>!3`N}@MRQa3-FE-fWvc^TSnAFYIUY5K-glmR=dA`{ z2!`y~M-R}sZF&Nm5|c?_oCY06;=znwPCZq=iOJX?rj&+05$YjC=!a+-oPz|>Gp3Pa q3_M`eh+wSK7~^PTAOhz%(}Z`fYTwDF3)Tb6Q_6Lk9Ngz# diff --git a/wai/pack-conformance/mutations/receipt/counts.json b/wai/pack-conformance/mutations/receipt/counts.json index b2c4cb8c4..bf33b5db8 100644 --- a/wai/pack-conformance/mutations/receipt/counts.json +++ b/wai/pack-conformance/mutations/receipt/counts.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"49accf316a7e70d5b9b7ed4aeb54e7aba71953059742dc8c1875ac51633c659e","sig":"ce8ec7c016be688368f9973c3d4133961991cc7522190898ba716e46ddb785e8db78e372518b8956d2c03e92720929b5170123a3979d74da8e00da6150083c05","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1606,"total_bytes":1782,"total_entries":6,"unique_objects":4,"version":2,"work_units":6} \ No newline at end of file +{"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":"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/energy.json b/wai/pack-conformance/mutations/receipt/energy.json index 2d2a4c2a7..e7f583e00 100644 --- a/wai/pack-conformance/mutations/receipt/energy.json +++ b/wai/pack-conformance/mutations/receipt/energy.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"f1d4110eebe104e96939a838d70ccabce70417966d1b429ddd684416fa026f0f","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"0ede60a595995d7b3e15a496dfe496afcbc6ffe9ad5593773e9ccdd5609a0192","sig":"e0fba537ae6fd86fd7193770a24a575ac200dc6f61bb897fb5c8a380aacf1954d045a4227ed49e7c1f90738f20f4d87c2233846452fdcd08c9495594f60fde02","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"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":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/index-digest.json b/wai/pack-conformance/mutations/receipt/index-digest.json index ce6ad1c09..6996c2eea 100644 --- a/wai/pack-conformance/mutations/receipt/index-digest.json +++ b/wai/pack-conformance/mutations/receipt/index-digest.json @@ -1 +1 @@ -{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"0000000000000000000000000000000000000000000000000000000000000000","media_class":"video","stage_root":"69517ce937163dbdc3bc3f5bae7e414bb001aa530a6bbd262bd32653e7374dbc"}],"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":"d67a67cfda398afc5004312c8b1430f08653edbaa2f5b484527125969b540964","sig":"906b4dc2f10fa2adb7afa5e662afb60c9daa48b667a7324839333e40b5a47cc7f8759e3fc19a592b60759e97309945934980756c92812f542d0a3d80b41ecd0c","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"2152f8d19b791d24453242e15f2eab6cb7cffa7b6a5ed30097960e069881db12","stored_bytes":1616,"total_bytes":1782,"total_entries":6,"unique_objects":5,"version":2,"work_units":6} \ No newline at end of file +{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"0000000000000000000000000000000000000000000000000000000000000000","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":"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 index c476d02fe4dabeb1da9ff6e6ae0e6a46d74186be..f21dc31108d11742f6233625c9d003605af9317b 100644 GIT binary patch delta 213 zcmX@Wae!mO1I79hi@v*&Zc7DreEKIeQQ|VA+YeD)(Vz1D2DKAZlGr#mO>Sfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4ySfoo-D#> zV$K-`X$3pf8p)x7&WD2-D;yMn9tyD4G}; zs_#!YmYkB%AZg|M={(C_!QY};_nw?o{rz2Nn}noD|49?3$xKXsj4vh!GW8SVI%c5j Om~EH@7$*lYzXJeiE=wx_ delta 210 zcmX@Wae!mO14YHVc@th+XgtmcaCx%v>f{qHYA$NFuK(Z4J0>{nyjM75+2oB(!jnZ9 zZH!nLP=V;)y#K|27gQfHKl&ui-=}Z=yfgc%4y @@ -774,7 +795,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 +834,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 +1126,1105 @@ 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") + 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): + """JSON in UTF-8 whose objects name no member twice (SPEC §2), or None.""" + 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) + try: + return json.loads(b.decode("utf-8"), object_pairs_hook=members, parse_constant=no_constant) + except (UnicodeDecodeError, ValueError, RecursionError): + return None + + +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]) + if not isinstance(man, dict): + 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") + if e["role"] == REFINEMENT and not refines_ok(ix, i, m.get("refines")): + raise Refused("object-refines") + if (m.get("derived_from") is not None) != bool(e["flags"] & ENTRY_DERIVED) or \ + (e["flags"] & ENTRY_DERIVED and not derived_from_ok(m["derived_from"])): + 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", "energy_provenance", "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): + """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.""" + cls = j.get("energy_provenance") + present = EVIDENCE_MEMBERS & set(j) + if cls is None: + need(not present) + return None + need(isinstance(cls, str) and cls in CLASS_TAGS and present == EVIDENCE.get(cls, set())) + 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["energy_counter_filtering"] + 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.""" + j = read_json(b) + need(isinstance(j, dict)) + r = receipt_payload(j) + need(blake3(RECEIPT_ID + r["payload"] + bytes.fromhex(j["sig"])).hex() == j["receipt_hash"]) + 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") + if v == 2: + need(V2_MEMBERS <= set(j) and set(j) <= V2_MEMBERS | {"energy_provenance"} | EVIDENCE_MEMBERS) + need("energy_provenance" not in j or j["energy_provenance"] is not None) + else: + need(V1_MEMBERS <= set(j) and set(j) <= V1_MEMBERS | {"version"} | EVIDENCE_MEMBERS) + label = energy_label(j) + 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 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) + 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) + view = {"class": name, "components": {c: {"media_playlist": f"../hls/{name}/wai-{c}.m3u8", "pieces": views}}, + "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) + tracks, groups = [], {} + for e in E: + if e["role"] in (BASE, REFINEMENT): + dl = e["deadline"] if e["role"] == REFINEMENT and e["deadline"] != P["defaults"]["deadline_ms"] else None + groups.setdefault((e["layer"], dl), []).append(e) + lag = P["live"]["layer_lag_ms"] + for (layer, dl), es in sorted(groups.items(), key=lambda g: (g[0][0], list(groups).index(g[0]))): + caps = [] + for e in es: + if e["cap"] not in caps: + caps.append(e["cap"]) + t = {"name": f"{c}/l{layer}" + (f"@{dl}" if dl is not None else ""), "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 any(e["role"] == FLOOR for e in E): + t["altGroup"] = 1 + if layer: + t["depends"] = [f"{c}/l{layer - 1}"] + t["wai_delivery_timeout_ms"] = es[0]["deadline"] - lag[layer] + tracks.append(t) + tracks.append({"altGroup": 1, "codec": F, "height": FH, "name": f"{c}/floor", "packaging": "cmaf", + "renderGroup": 1, "width": FW}) + out["moq/catalog.json"] = (jcs({"tracks": tracks, "version": 1}) + "\n").encode() + return out + + # ---------------------------------------------------------------- the corpus @@ -1108,6 +2245,404 @@ def load(path): return f.read() +def tree(root): + """Every file below root, as listing paths, in byte order.""" + out = [] + for at, _, files in os.walk(root): + out += [os.path.relpath(os.path.join(at, f), root).replace(os.sep, "/") for f in files] + 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): + """packs//: package/ verified in §11.3's order, without the source; then 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 = os.path.join(root, "package") + tag = f"packs/{name}" + files = tree(pkg) + if "LISTING" not in files: + rep.check(False, f"{tag}: no LISTING (listing-missing)") + return + others = [p for p in files if p != "LISTING"] + rep.check(all(all(segment_ok(s) for s in p.split("/")) for p in files) + and len({p.lower() for p in files}) == len(files), f"{tag}: every path a listing path (SPEC §3.1)") + rep.check(load(os.path.join(pkg, "LISTING")) == listing(pkg, others), + f"{tag}: LISTING is the listing of the other files (listing-mismatch)") + 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"] + rep.check(raw == jcs(j).encode(), f"{tag}: package.receipt.json is RFC 8785 canonical JSON (§9.1)") + + # The policy: as given, its effective canonical JSON, its digest (§3.2, §11.1). + toml_b, pj = get("policy.toml"), get("policy.json") + 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)") + try: + P = admit(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 isinstance(given.get("royalty_free_only"), bool): + P["royalty_free_only"] = given["royalty_free_only"] + rep.check(jcs(P).encode() == pj, f"{tag}: policy.json is policy.toml's effective canonical JSON (policy-json-mismatch)") + ids = [c["id"] for c in P["component"]] + [c["id"] for c in P["class"]] + cls = [c["id"].lower() for c in P["class"]] + rep.check(all(segment_ok(i) for i in ids) and len(set(cls)) == len(cls) and "classic" not in cls, + f"{tag}: component and class ids are path segments, class ids distinct in any case, none `classic` (pack-id)") + + # 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): + named |= verify_component(rep, tag, root, pkg, files, P, j, c, ix) + + # §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)") + + +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 verify_component(rep, tag, root, pkg, files, P, j, c, ix): + """§11.3 per component, then 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 + rep.check(ix["tb"] == tb and MEDIA_CLASSES[ix["media_class"]] == comp["media"], + f"{t}: the index's timebase and media class are the policy's component's (index-policy-mismatch)") + pieces, E = ix["pieces"], ix["entries"] + caps = [] + for e in E: + if e["cap"] not in caps: + caps.append(e["cap"]) + rep.check(ix["strings"] == [c["id"]] + [x["id"] for x in P["class"]] + caps, + f"{t}: the strings are the component's id, the class ids, each capability at its first use (§6.2)") + rep.check([(x["name"], x["max_layer"], x["flags"], x["max_kbps"]) for x in ix["classes"]] + == [(x["id"], x["max_layer"], int(x["floor_only"]), x.get("max_kbps", 0)) for x in P["class"]], + f"{t}: the class records are the policy's classes, in its order (§6.2)") + 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)") + rep.check(ix["first_piece"] == 0 and ix["first_entry"] == 0 and not any(ix["prev"]), + f"{t}: 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}: {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)") + 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)") + rep.check(mans[i].get("target") == mans[base].get("target"), + f"{t}: piece {k}: the floor's target is its piece's (§5.2)") + fp = floor_payload(objs[i][12 + E[i]["man_len"]:]) + try: + if fp is None: + raise Malformed("not §7.2's framing") + 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 for o in units): + raise Malformed("a unit that is not one temporal unit") + if seq is None: + seq_obu = next((whole for o in units for typ, whole, _ in o if typ == OBU_SEQ), None) + if seq_obu is None: + raise Malformed("no sequence header") + seq = sequence_header(obus(seq_obu)[0][2]) + except Malformed as x: + rep.check(False, f"{t}: piece {k}: the floor payload ({x})") + 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)") + 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). + cats = render_catalogs(P, ix, c["id"], (av1_codecs(seq), seq["max_w"], seq["max_h"], frags), size, + j["policy_digest"]) + 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)") + 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 key_frame(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): + raise Malformed("a 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"))) @@ -1231,6 +2766,23 @@ 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") return rep diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index 14ca1b0bc..b46955276 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -96,14 +96,39 @@ fn entry(bytes: &[u8], piece: u64, cap: u16, role: Role, layer: u8, refines: u32 } } +/// 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. +/// 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 base = env("wai.video.int_motion", None, false, b"base"); + 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, b"r1"); + 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, b"r2"); + 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![ @@ -214,7 +239,8 @@ pub fn index_verdict(b: &[u8]) -> 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. +/// 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" { @@ -226,6 +252,9 @@ pub fn objects_verdict(index: &[u8], objects: &Files) -> String { 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() } @@ -272,9 +301,17 @@ pub fn mutations() -> (Files, Value) { } 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()); - let mut wrong_payload = objs[0].clone(); - *wrong_payload.last_mut().expect("bytes") ^= 1; - files.insert(format!("objects/{}.wai", hex(&objects::object_hash(&wrong_payload))), wrong_payload.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; @@ -285,6 +322,14 @@ pub fn mutations() -> (Files, Value) { 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; @@ -311,7 +356,7 @@ pub fn mutations() -> (Files, Value) { { 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, b"r2"); + 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); -- 2.54.0 (Apple Git-157) From 54ae7206de4f3fafc043156c50ebc6ce9bf1ebfb Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:45:58 -0400 Subject: [PATCH 16/28] wai: settle what pack_verify.py found ambiguous; the WSIX reader refuses in the stated order MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The independent verifier, written from the text, found places the text did not decide; each is now stated and both implementations follow it: - §6.2: the reader's refusal order (magic, version, sizes from counts, reserved bytes, media class, then each entry's role and tier, strings, trailing); wai-rs's reader is rewritten to it. - §6.3: entry order and layer-role for every role; the class-plan values; a refinement's object is a WAI1; null is absent; a WAI2's derived_from is its entries'; tier-weakest is a layered entry's. - §6.6 a floor's sse; §7.3 a shown key frame needs a sequence header, and which header differs; §7.6 hdlr's name and tkhd's fields; §8.3 the Representation's attributes and one component; §8.4 track order; §8.8 media_playlist only when the class lists a WAI object (the golden's stages/legacy.json changes); §9.1 the reader's order and label rules, counts across indexes; §9.3 the version-1 signing payload. - §5.2: the base manifest's fallback member and derived_from's place. pack_verify.py follows the same choices (MoQ track order, null labels, WAI2 entries, shown key frames); 540 of 540 checks hold, and the Rust corpus check passes. Co-Authored-By: Claude Opus 5.5 --- wai/extensions/packaging.md | 120 ++++++---- wai/pack-conformance/pack_verify.py | 43 ++-- .../packs/video-base-01/package/LISTING | 2 +- .../video-base-01/package/stages/legacy.json | 2 +- wai/wai-pack/src/catalog.rs | 5 +- wai/wai-rs/src/package/tests.rs | 4 +- wai/wai-rs/src/stage_index.rs | 209 +++++++++--------- wai/wai-rs/src/stage_index/tests.rs | 20 +- 8 files changed, 240 insertions(+), 165 deletions(-) diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index 0104cc5e8..a8dc7b7a8 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -288,10 +288,11 @@ nothing after it ([http-delivery](http-delivery.md) §5.1). 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`, the rung's capability, no -fallback, `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). +`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); 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`: @@ -435,13 +436,22 @@ capability at its first use. 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: -another magic (`index-magic`) or version (`index-version`); a table that runs past the -end, its size taken from its count before anything is allocated (`index-truncated`); -reserved or padding bytes that are not zero (`index-reserved`); a media class, role or -tier with no meaning (`index-media-class`, `index-role`, `index-tier`); a string that -is empty or not UTF-8, or a string index past the table (`index-string`); bytes after -the string table (`index-trailing`). +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 @@ -454,10 +464,11 @@ of these, checked in this order: (`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, roles out - of the order base, refinement, floor, any other, or refinements out of layer order - (`entry-order`); a base or a floor 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 + (`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 @@ -466,9 +477,8 @@ of these, checked in this order: 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. Not one plan row per class (`class-plan`). Then class by class: a flag with no - meaning (`class-flags`); a row of another length than the pieces, or a value other - than `0xFF` for a floor-only class, or other than `0xFF` or at most both the class's +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 @@ -476,10 +486,13 @@ 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 `refines` does not name 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, or well formed, otherwise -than bit 2 says (`object-derived-from`). +`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 @@ -493,9 +506,9 @@ entropy-consistency for `NeuralEntropyExact`, and none otherwise (SPEC §5, §7) 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 that states decode-equivalence over a prefix that - does not hold it is refused `tier-weakest`; any other tier than the one this section - gives, `tier-mismatch`. + (`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 @@ -507,7 +520,7 @@ 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. +and one without the source cannot. A floor's `sse` is `2^64 − 1`. ## 7. Floor and `derived_from` @@ -539,8 +552,9 @@ unit per source frame. Its listing (SPEC §3.1) of every file is pinned by the p (`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, a unit that starts at a key frame without a sequence - header, or a sequence header other than the first (`floor-av1-malformed`); + 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 @@ -566,9 +580,10 @@ 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, the - unity matrix, the width and height in 16.16; `mdia`: `mdhd` version 0, timescale - `tb_num`, language `und`; `hdlr` `vide` named `video`; `minf`: `vmhd` with flags 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`, @@ -671,17 +686,20 @@ one `AdaptationSet`, `` and ``, indented by two spaces a level: ``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, `` (with - `dependencyId="-l"` for *k* > 0) and `bandwidth` the peak rate of its own + 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). +([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}`. Each (component, layer, deadline) is one track of +`{"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*; @@ -715,7 +733,8 @@ to wait for a refinement. What the receipts of a dropped object say is **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": ""}`, each piece +"../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 @@ -760,17 +779,22 @@ no ceiling (a joule ceiling of `2^64 − 1`); `joules_micro`; `kind` `"package"` (`null` for none); `policy_digest`; `receipt_hash`; `sig`; `signer_id`; `signer_pubkey`; `stored_bytes`; `total_bytes`; `total_entries`; `unique_objects`; `version` 2; `work_units`; and, when labelled, the class's members (`energy_provenance` -and its evidence, energy-measurement §2.7). A reader refuses (`receipt-json`) a member -it does not define or one missing, a value of another form, a parent kind and hash that -disagree, an `index` other than `index-.wsix`, and a `receipt_hash` that is not the -receipt's. A `"version"` other than 1 or 2 is `receipt-version`. +and its evidence, energy-measurement §2.7). A reader reads `"version"` first: 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, a value of +another form, 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 member whose value is `null` is absent (an on-chip counter's filtering, +absent, reads as unknown). A reader does not require the canonical form; a writer +writes it. A verifier of a receipt against its indexes, in its components' order, refuses, the first that applies: another number of indexes (`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`); other counts (`receipt-counts`); a parent kind that does not +(`receipt-policy-digest`); 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 @@ -786,6 +810,20 @@ still verifies. Its JSON form is RFC 8785 canonical with `version` 1: `asset_id` `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. +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 diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index 3120da563..a677458f0 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -1499,10 +1499,19 @@ def object_rules(ix, i, data): raise Refused("object-lengths") if cap != e["cap"]: raise Refused("object-capability") - if e["role"] == REFINEMENT and not refines_ok(ix, i, m.get("refines")): + 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") - if (m.get("derived_from") is not None) != bool(e["flags"] & ENTRY_DERIVED) or \ - (e["flags"] & ENTRY_DERIVED and not derived_from_ok(m["derived_from"])): + # 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 @@ -1558,11 +1567,15 @@ def energy_label(j): for no class. Evidence without its class, a class without its evidence, and evidence a class does not carry are refused.""" cls = j.get("energy_provenance") - present = EVIDENCE_MEMBERS & set(j) + # A member whose value is null is absent; an on-chip counter's filtering, absent, + # reads as Unknown (energy-measurement §2.7). + present = {k for k in EVIDENCE_MEMBERS if j.get(k) is not None} if cls is None: need(not present) return None - need(isinstance(cls, str) and cls in CLASS_TAGS and present == EVIDENCE.get(cls, set())) + allowed = EVIDENCE.get(cls, set()) + need(isinstance(cls, str) and cls in CLASS_TAGS and present <= allowed + and (allowed - present) <= {"energy_counter_filtering"}) out = bytes([CLASS_TAGS[cls]]) if cls in EVIDENCE: u = j["energy_uncertainty"] @@ -1570,7 +1583,7 @@ def energy_label(j): 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["energy_counter_filtering"] + f = j.get("energy_counter_filtering") or "Unknown" need(isinstance(f, str) and f in FILTERING) out += bytes([FILTERING[f]]) else: @@ -1617,7 +1630,6 @@ def receipt_payload(j): need(isinstance(j.get("asset_id"), str) and j.get("kind") == "package") if v == 2: need(V2_MEMBERS <= set(j) and set(j) <= V2_MEMBERS | {"energy_provenance"} | EVIDENCE_MEMBERS) - need("energy_provenance" not in j or j["energy_provenance"] is not None) else: need(V1_MEMBERS <= set(j) and set(j) <= V1_MEMBERS | {"version"} | EVIDENCE_MEMBERS) label = energy_label(j) @@ -2150,8 +2162,10 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): s["deadline_ms"] = e["deadline"] v["stages"].append(s) views.append(v) - view = {"class": name, "components": {c: {"media_playlist": f"../hls/{name}/wai-{c}.m3u8", "pieces": views}}, - "policy_digest": policy_hex} + 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 @@ -2198,13 +2212,14 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): lines += [f' ' for m in mine for e in m] lines += [" ", " "] out[f"dash/{name}.mpd"] = text(lines + mpd_tail) + # §8.4: one track per (layer, deadline), by layer, then by deadline ascending. tracks, groups = [], {} for e in E: if e["role"] in (BASE, REFINEMENT): - dl = e["deadline"] if e["role"] == REFINEMENT and e["deadline"] != P["defaults"]["deadline_ms"] else None - groups.setdefault((e["layer"], dl), []).append(e) + groups.setdefault((e["layer"], e["deadline"]), []).append(e) lag = P["live"]["layer_lag_ms"] - for (layer, dl), es in sorted(groups.items(), key=lambda g: (g[0][0], list(groups).index(g[0]))): + for (layer, deadline), es in sorted(groups.items()): + dl = deadline if layer and deadline != P["defaults"]["deadline_ms"] else None caps = [] for e in es: if e["cap"] not in caps: @@ -2577,8 +2592,8 @@ def verify_sources(rep, t, root, P, comp, ix, decoded, units_of): 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 key_frame(o, seq) and not any(typ == OBU_SEQ for typ, _, _ in o): - raise Malformed("a key frame without a sequence header") + 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)") diff --git a/wai/pack-conformance/packs/video-base-01/package/LISTING b/wai/pack-conformance/packs/video-base-01/package/LISTING index df79adc58..1a7563f66 100644 --- a/wai/pack-conformance/packs/video-base-01/package/LISTING +++ b/wai/pack-conformance/packs/video-base-01/package/LISTING @@ -30,6 +30,6 @@ d3f0cbc108b844f87d73a0b6f71cfb2141dbb4c9c1aaf38fce11054657481968 package.receip 517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml -681cdfa04e7fe7d7b06ed6d85a7ccd7fba40301919b145e5394bddcc2bd2ef58 stages/legacy.json +59b343802800c815390b840da3ee6981367b495a30d8a1952f6484d04271caf9 stages/legacy.json b7514cb869338126e4785943a4624e9dbfa44464fa450c55341b7931bd9c19f9 stages/mobile.json 33898452413a8fe209fb6b158bfd1e9b06a74fc3ab7639810522a586a4aa6ac6 stages/tv.json 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 index fd134d3f3..5ffa239e8 100644 --- a/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json +++ b/wai/pack-conformance/packs/video-base-01/package/stages/legacy.json @@ -1 +1 @@ -{"class":"legacy","components":{"v":{"media_playlist":"../hls/legacy/wai-v.m3u8","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"} +{"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/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs index 51eac73d4..b746e6552 100644 --- a/wai/wai-pack/src/catalog.rs +++ b/wai/wai-pack/src/catalog.rs @@ -412,7 +412,10 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> St pieces.push(o); } let class = v.class_id(c); - let comp = json!({"media_playlist": format!("../hls/{class}/wai-{}.m3u8", v.id()), "pieces": pieces}); + 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)})).expect("a stage view is in the canonical subset") + "\n" diff --git a/wai/wai-rs/src/package/tests.rs b/wai/wai-rs/src/package/tests.rs index 3a08cb259..5ff09a031 100644 --- a/wai/wai-rs/src/package/tests.rs +++ b/wai/wai-rs/src/package/tests.rs @@ -195,12 +195,10 @@ fn the_v2_json_is_canonical_and_round_trips() { back.verify_detailed(&refs(&ix)).unwrap(); assert_eq!(AnyPackageReceipt::from_json(&j).unwrap(), AnyPackageReceipt::V2(r)); } - // Its key order is code-unit order: two keys swapped is not this receipt's JSON. + // 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}"); - let swapped = j.replacen("\"asset_id\":\"asset-01\",", "", 1).replacen("{\"components\"", "{\"components\"", 1); - assert_ne!(swapped, j); } #[test] diff --git a/wai/wai-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs index 3ff015396..9ce305dd1 100644 --- a/wai/wai-rs/src/stage_index.rs +++ b/wai/wai-rs/src/stage_index.rs @@ -328,20 +328,6 @@ fn pad8(n: usize) -> usize { n.div_ceil(8) * 8 } -struct Reader<'a> { - b: &'a [u8], - at: usize, -} - -impl<'a> Reader<'a> { - fn take(&mut self, n: usize, what: &str) -> Result<&'a [u8], IndexReason> { - let end = self.at.checked_add(n).filter(|&e| e <= self.b.len()).ok_or_else(|| IndexReason::new("index-truncated", format!("the {what} runs past the file's end")))?; - let s = &self.b[self.at..end]; - self.at = end; - Ok(s) - } -} - fn u16_at(b: &[u8], i: usize) -> u16 { u16::from_le_bytes([b[i], b[i + 1]]) } @@ -355,14 +341,6 @@ fn h32(b: &[u8], i: usize) -> [u8; 32] { b[i..i + 32].try_into().expect("32 bytes") } -fn zero(b: &[u8], what: &str) -> Result<(), IndexReason> { - if b.iter().all(|&x| x == 0) { - Ok(()) - } else { - Err(IndexReason::new("index-reserved", format!("{what} is not zero"))) - } -} - impl EntryRec { /// The entry's 128 bytes. pub fn to_bytes(&self) -> [u8; ENTRY_LEN] { @@ -389,9 +367,6 @@ impl EntryRec { if b[56] > TIER_DECODE { return Err(IndexReason::new("index-tier", format!("prefix_tier {}", b[56]))); } - zero(&b[58..60], "an entry's reserved bytes")?; - zero(&b[108..112], "an entry's reserved bytes")?; - zero(&b[120..128], "an entry's reserved bytes")?; Ok(EntryRec { content_hash: h32(b, 0), byte_len: u64_at(b, 32), @@ -486,95 +461,120 @@ impl StageIndex { o } - /// Read a file (§6). Refused: another magic (`index-magic`) or version - /// (`index-version`); a table that runs past the end (`index-truncated`); bytes after - /// the string table (`index-trailing`); reserved or padding bytes that are not zero - /// (`index-reserved`); a media class, role or tier with no meaning (`index-media-class`, - /// `index-role`, `index-tier`); a string that is empty, longer than its file allows or - /// not UTF-8, or a string index past the table (`index-string`). + /// 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 mut r = Reader { b, at: 0 }; - let hd = r.take(HEADER_LEN, "header")?; - if &hd[0..4] != MAGIC_WSIX { - return Err(IndexReason::new("index-magic", "not a WSIX file")); + 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"); } - let version = u16_at(hd, 4); + 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}"))); } - let media_class = MediaClass::from_code(u16_at(hd, 6)).ok_or_else(|| IndexReason::new("index-media-class", format!("media class {}", u16_at(hd, 6))))?; - let (n_pieces, n_entries, n_strings, n_classes) = (u32_at(hd, 16) as usize, u32_at(hd, 20) as usize, u32_at(hd, 24) as usize, u32_at(hd, 28) as usize); - zero(&hd[146..160], "the header's reserved bytes")?; - let header = Header { - media_class, - tb_num: u32_at(hd, 8), - tb_den: u32_at(hd, 12), - policy_digest: h32(hd, 32), - prev_index_digest: h32(hd, 64), - first_piece: u64_at(hd, 96), - first_entry: u64_at(hd, 104), - plan_digest: h32(hd, 112), - component: u16_at(hd, 144), + 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)) }; - // Every table is sized from a count before anything is allocated: a count the - // file cannot hold is refused as truncated. - let need = |n: usize, len: usize| n.checked_mul(len); - let rest = b.len().saturating_sub(HEADER_LEN); - for (n, len, what) in [(n_pieces, PIECE_LEN, "piece table"), (n_entries, ENTRY_LEN, "entry table"), (n_classes, CLASS_LEN, "class table")] { - if need(n, len).is_none_or(|x| x > rest) { - return Err(IndexReason::new("index-truncated", format!("the {what} runs past the file's end"))); - } - } - let mut pieces = Vec::with_capacity(n_pieces); - for _ in 0..n_pieces { - let p = r.take(PIECE_LEN, "piece table")?; - zero(&p[28..32], "a piece's reserved bytes")?; - pieces.push(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), - }); - } - let mut entries = Vec::with_capacity(n_entries); - for _ in 0..n_entries { - entries.push(EntryRec::parse(r.take(ENTRY_LEN, "entry table")?)?); - } - let mut classes = Vec::with_capacity(n_classes); - for _ in 0..n_classes { - let c = r.take(CLASS_LEN, "class table")?; - zero(&c[8..16], "a class's reserved bytes")?; - classes.push(ClassRec { name: u16_at(c, 0), max_layer: c[2], flags: c[3], max_kbps: u32_at(c, 4) }); - } - let plan_len = n_classes.checked_mul(n_pieces).filter(|&x| x <= b.len()).ok_or_else(|| IndexReason::new("index-truncated", "the class plan runs past the file's end"))?; - let plan_bytes = r.take(plan_len, "class plan")?; - let plan: Vec> = if n_pieces == 0 { vec![Vec::new(); n_classes] } else { plan_bytes.chunks(n_pieces).map(<[u8]>::to_vec).collect() }; - zero(r.take(pad8(plan_len) - plan_len, "class plan's padding")?, "the class plan's padding")?; - if n_strings > b.len() / 2 { - return Err(IndexReason::new("index-truncated", "the string table runs past the file's end")); - } - let strings_start = r.at; - let mut strings = Vec::with_capacity(n_strings); + 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 len = u16_at(r.take(2, "string table")?, 0) as usize; - let s = r.take(len, "string table")?; + 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 used = r.at - strings_start; - zero(r.take(pad8(used) - used, "string table's padding")?, "the string table's padding")?; - if r.at != b.len() { - return Err(IndexReason::new("index-trailing", format!("{} bytes follow the string table", b.len() - r.at))); - } + 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) } @@ -811,7 +811,15 @@ pub fn check_object(ix: &StageIndex, k: usize, bytes: &[u8]) -> Result<(), Index return r("non-envelope-entry", "bytes follow the envelope".into()); } let first = w.manifest.renditions.first().map(|m| m.capability.clone()).unwrap_or_default(); - (first, None, None, payload) + // 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()), }; @@ -843,6 +851,7 @@ pub fn check_object(ix: &StageIndex, k: usize, bytes: &[u8]) -> Result<(), Index } 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(()) diff --git a/wai/wai-rs/src/stage_index/tests.rs b/wai/wai-rs/src/stage_index/tests.rs index 1a702164a..c7d33a074 100644 --- a/wai/wai-rs/src/stage_index/tests.rs +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -13,6 +13,7 @@ fn env(cap: &str, refines: Option<(&[u8; 32], &[u8; 32], u8)>, derived: bool, pa } /// 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 { @@ -96,8 +97,8 @@ fn an_index_round_trips_and_checks() { 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 in 0..ix.entries.len() { - check_object(&ix, k, &objects[k]).unwrap_or_else(|r| panic!("entry {k}: {r}")); + for (k, o) in objects.iter().enumerate() { + check_object(&ix, k, o).unwrap_or_else(|r| panic!("entry {k}: {r}")); } } @@ -179,11 +180,22 @@ fn every_index_mutation_names_its_reason() { fn a_malformed_file_is_refused_with_its_reason() { let (ix, _) = fixture(); let b = ix.to_bytes(); - // Every strict prefix is truncated (the header, a table, a padding). + // 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, "index-truncated", "prefix {n}: {r}"); + 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"); -- 2.54.0 (Apple Git-157) From 4130b1c532fe467ee62a3bfd60a2f5f27cfcf2e3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:47:48 -0400 Subject: [PATCH 17/28] pack-conformance: the README describes pack_verify.py's package checks Co-Authored-By: Claude Opus 5.5 --- wai/pack-conformance/README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 2bc0df15c..1a65f3aec 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -86,7 +86,16 @@ 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, and checks the version-1 receipts. + 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 -- 2.54.0 (Apple Git-157) From 30e4fd0c0f207717b48d8814a02c6da9cb984022 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 06:47:54 -0400 Subject: [PATCH 18/28] pack-conformance: README names the verifier's one package dependency Co-Authored-By: Claude Opus 5.5 --- wai/pack-conformance/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 1a65f3aec..5b88d400c 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -77,7 +77,7 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. (`wai-pack verify`), and gives every 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 -- 2.54.0 (Apple Git-157) From 6012a7c3cfb6145820e04c831d4235335fb58f9c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 07:42:47 -0400 Subject: [PATCH 19/28] wai: a package is bound to the policy its receipt signs; malformed packages are refused, never panicked on; floor key frames and members checked against the package; receipt JSON read strictly; MoQ dependencies never dangle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2, items 1-10: - 1: §11.3 rewritten in order. The verifier (Rust and Python) requires the receipt's components to be the policy's by id, order and media (policy-component-mismatch, so a zero-component receipt is refused); runs pack-id and §3.6's capability rules under the effective policy; holds each index's timebase, media, string table, class records, every entry's role/capability/layer, and every piece's rules, moments and class plan to the policy (index-policy-mismatch). Seal and verify refuse a receipt with no component (receipt-index-count). - 2: verify reads objects by get(), skips the floor and catalogs once anything has failed; the catalog renderers return Result (a start past 2^53 - 1 is catalog-mismatch); the fragment time uses checked_mul; Python maps encoding, overflow and value errors to refusals. New index rule window-start: an index with no previous window starts at piece 0, entry 0 and time 0 (§4.5), in both. - 3: each floor piece opens on a shown key frame, and holds no other unless extra_keyframes; every sequence header is the first's bytes (both, §11.3). - 4: both readers refuse a member named twice before reading the version; the package's receipt file must be its canonical bytes; Python refuses -0. - 5: a refinement track depends on the tracks carrying what its entries refine (MSF's depends is all-of); never a track the catalog lacks. catalogs/ corpus. - 6: Rust binds a supplied floor's renderer, settings, framing header and target (floor-object-mismatch); SPEC §7.1 says a profile may fix those members. - 7: verify reads the directory strictly (dot-names, *.sha256 included). - 8: Python's manifest parse is the typed one; the object differential is 0. - 9: Python refuses two components of one id (receipt-component). - 10: v2 writes every member, energy_provenance null when unmetered, evidence exactly as its class carries; v1 keeps reading an absent class. Regression tests: tests/review.rs (the review's probes, re-sealed with the key, and a seeded run of random alterations that never panics); mutation corpus receipt cases. Co-Authored-By: Claude Opus 5.5 --- wai/SPEC.md | 4 +- wai/extensions/packaging.md | 146 ++++-- .../catalogs/both-layers-on-800.wsix | Bin 0 -> 1088 bytes wai/pack-conformance/catalogs/expected.json | 49 ++ .../catalogs/layer-1-on-its-own-deadline.wsix | Bin 0 -> 1088 bytes .../catalogs/one-deadline.wsix | Bin 0 -> 1088 bytes wai/pack-conformance/mutations/expected.json | 71 +++ .../mutations/receipt/counts.json | 2 +- .../mutations/receipt/duplicate-member.json | 1 + .../receipt/energy-member-missing.json | 1 + .../mutations/receipt/energy.json | 2 +- .../receipt/evidence-without-class.json | 1 + .../mutations/receipt/index-digest.json | 2 +- .../mutations/receipt/lone-surrogate.json | 1 + .../mutations/receipt/media-class.json | 2 +- .../mutations/receipt/minus-zero.json | 1 + .../mutations/receipt/no-component.json | 1 + .../mutations/receipt/null-value.json | 1 + .../mutations/receipt/ok-live-window.json | 2 +- .../mutations/receipt/ok.json | 2 +- .../mutations/receipt/parent-kind-live.json | 2 +- .../mutations/receipt/parent-kind.json | 2 +- .../mutations/receipt/policy-digest.json | 2 +- .../mutations/receipt/receipt-hash.json | 2 +- .../mutations/receipt/signature.json | 2 +- .../mutations/receipt/stage-root.json | 2 +- .../receipt/two-components-one-id.json | 1 + .../mutations/receipt/unknown-member.json | 2 +- wai/pack-conformance/pack_verify.py | 324 ++++++++++--- .../packs/video-base-01/package/LISTING | 2 +- .../package/package.receipt.json | 2 +- wai/wai-pack/src/catalog.rs | 175 ++++--- wai/wai-pack/src/classify.rs | 5 + wai/wai-pack/src/golden.rs | 163 ++++++- wai/wai-pack/src/pack.rs | 3 +- wai/wai-pack/src/verify.rs | 445 +++++++++++++----- wai/wai-pack/tests/review.rs | 391 +++++++++++++++ wai/wai-rs/src/package.rs | 61 ++- wai/wai-rs/src/package/tests.rs | 45 +- wai/wai-rs/src/stage_index.rs | 56 ++- 40 files changed, 1625 insertions(+), 351 deletions(-) create mode 100644 wai/pack-conformance/catalogs/both-layers-on-800.wsix create mode 100644 wai/pack-conformance/catalogs/expected.json create mode 100644 wai/pack-conformance/catalogs/layer-1-on-its-own-deadline.wsix create mode 100644 wai/pack-conformance/catalogs/one-deadline.wsix create mode 100644 wai/pack-conformance/mutations/receipt/duplicate-member.json create mode 100644 wai/pack-conformance/mutations/receipt/energy-member-missing.json create mode 100644 wai/pack-conformance/mutations/receipt/evidence-without-class.json create mode 100644 wai/pack-conformance/mutations/receipt/lone-surrogate.json create mode 100644 wai/pack-conformance/mutations/receipt/minus-zero.json create mode 100644 wai/pack-conformance/mutations/receipt/no-component.json create mode 100644 wai/pack-conformance/mutations/receipt/null-value.json create mode 100644 wai/pack-conformance/mutations/receipt/two-components-one-id.json create mode 100644 wai/wai-pack/tests/review.rs diff --git a/wai/SPEC.md b/wai/SPEC.md index e41ced335..f783c039b 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -1897,7 +1897,9 @@ renderer or encoder: 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. A value of another form makes the member +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. diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index a8dc7b7a8..dc2f364dc 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -291,8 +291,8 @@ object. The reference packager writes a base's manifest as SPEC §3 orders it, w `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); a floor's `derived_from` (§7.2) comes after -`conditioning`. +(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`: @@ -428,9 +428,9 @@ floor only), `u32 max_kbps` (0 for none) and 8 reserved bytes, in the policy's c 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. The reference -packager writes the component's id first, then the class ids in policy order, then each -capability at its first use. +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) @@ -458,8 +458,12 @@ Files that are not WAI envelopes (CMAF initialization segments and fragments, FL 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`). Then piece by piece: a piece that lasts no - time or does not start where the one before it ended (`piece-order`); whose +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`). @@ -536,7 +540,9 @@ the piece's base: `motif` the base's content hash, `motif_capability` its capabi `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. An AV1 floor object's payload is the +""}`, 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 @@ -705,8 +711,13 @@ 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. A refinement track `depends` on -`["/l"]` and carries `wai_delivery_timeout_ms` (§8.6). The floor is a track +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 @@ -778,22 +789,29 @@ 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`; and, when labelled, the class's members (`energy_provenance` -and its evidence, energy-measurement §2.7). A reader reads `"version"` first: 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, a value of -another form, 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 member whose value is `null` is absent (an on-chip counter's filtering, -absent, reads as unknown). A reader does not require the canonical form; a writer -writes it. +`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: another number of indexes (`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`); other counts, or a content hash at two lengths in any of 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 @@ -809,7 +827,11 @@ still verifies. Its JSON form is RFC 8785 canonical with `version` 1: `asset_id` `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. +`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" @@ -870,22 +892,59 @@ measurement claim of a metered package and floor objects §7.5 covers. A verifie **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, refuses, in this -order: a missing `LISTING` (`listing-missing`) or one that is not the listing of the -other files (`listing-mismatch`); a receipt that does not read as version 2 -(`receipt-json`, `receipt-version`); a `policy.json` that does not hash to its policy -digest (`policy-digest-mismatch`) or is not `policy.toml`'s effective canonical form -with its `royalty_free_only` (`policy-json-mismatch`); a missing index -(`index-missing`) and the receipt's own refusals (§9.1). Then per component: an index -whose timebase is not the policy's component's (`index-policy-mismatch`); a plan file -that is not the index's plan (`plan-mismatch`); an entry's missing object -(`object-missing`) or one that breaks §6.3; a decode-equivalent prefix that does not -decode to its `prefix_digest` (`prefix-digest`); a floor object that does not present -its piece's base (`floor-motif-mismatch`), and CMAF files that are not the ones its -temporal units give (§7.6, `floor-cmaf-mismatch`); and catalogs that are not the ones -the index, the objects and the floor give (§8, `catalog-mismatch`, and -`catalog-unexpected` for one more). Then a metered package's claim (§9.4), an object -file no entry names (`object-unlisted`), and any other file (`unexpected-file`). +**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 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); `policy.json` is its effective + canonical JSON with `policy.json`'s `royalty_free_only` (`policy-json-mismatch`); 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 @@ -907,10 +966,11 @@ ladder subset (`pack-rule-ladder-unsupported`), a floor to encode | `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`, `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 | +| `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 | -| `listing-missing`, `listing-mismatch`, `policy-digest-mismatch`, `policy-json-mismatch`, `index-missing`, `index-policy-mismatch`, `plan-mismatch`, `object-missing`, `prefix-digest`, `floor-motif-mismatch`, `floor-cmaf-mismatch`, `catalog-mismatch`, `catalog-unexpected`, `object-unlisted`, `unexpected-file` | §11.3 | +| `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` | §11.1, §11.4 | 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 0000000000000000000000000000000000000000..f07c7d47077dac7cbb9a3d957f62659c3bc49cb3 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANTAODy{CM!GE(*zxI~&_s#LjBYnG55B)Pq0?)Esu01e7K= z{$b7lnFaDYH~@e&f#_!ksRsdE@$b09^ZqjydrvDk?DH#$Vu;^xFC}UzMDHCBRPt4RS%S=hl*UQW+iOwY%gF66;wm=RQh1x*1=9OQnG91{c3!ANTAODy{CM!GE(*zxI~&_s#LjBY`3n=u_BO1v} zWG2l15~w-s$dW)dvGEUc2FN^+-@yR@q>1%EuK0J{;d%d=k=7H}$-A@O+GbCyJevOf zvw~%nkE3cvbDXgj%>T&tQ`7&9@RSS*7+7>OGL$hfl$41ulqY8Dm1U-+=IdqVmBi=f Zmt^MWp^Fx!re)@(@*=5CEHi|d2LKZ{&7%MS literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/catalogs/one-deadline.wsix b/wai/pack-conformance/catalogs/one-deadline.wsix new file mode 100644 index 0000000000000000000000000000000000000000..f21dc31108d11742f6233625c9d003605af9317b GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF66;wm=RQh1x*1=9OQnG91{c3!ANTAODy{CM!GE(*zxI~&_s#LjBYxI^h^&SofrJm`zE-txX)4o-$`&ZbT(21YBe%ZC7g%`*_ETGUwk7zV=VO*I1OQ7bk z!z7?IvGEUc2FN^+-@yR@q=^lGT=DO?!}I<#BdsT{lXqvmwauPZc{Kg|X9deDA4k=U z<~U<5nE#ROr>6fI;VBssAh76WWGG`|C@B+RC{N7PE6Yqt&DYD!D~Zp|FUic$Ll-Sd RP0P$n len(b): return None man = read_json(b[8:8 + ml]) - if not isinstance(man, dict): + # §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): @@ -1534,7 +1598,7 @@ EVIDENCE_MEMBERS = {"energy_uncertainty", "energy_counter_filtering", "energy_ca 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", "energy_provenance", "grant", "joules_micro", "kind", "parent_receipt_hash", +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" @@ -1562,20 +1626,24 @@ def need(ok): raise Refused("receipt-json") -def energy_label(j): +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.""" + 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") - # A member whose value is null is absent; an on-chip counter's filtering, absent, - # reads as Unknown (energy-measurement §2.7). - present = {k for k in EVIDENCE_MEMBERS if j.get(k) is not None} + 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()) - need(isinstance(cls, str) and cls in CLASS_TAGS and present <= allowed - and (allowed - present) <= {"energy_counter_filtering"}) + 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"] @@ -1614,10 +1682,16 @@ 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.""" - j = read_json(b) + # 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)) - r = receipt_payload(j) - need(blake3(RECEIPT_ID + r["payload"] + bytes.fromhex(j["sig"])).hex() == j["receipt_hash"]) + 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 @@ -1628,11 +1702,13 @@ def receipt_payload(j): 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 <= set(j) and set(j) <= V2_MEMBERS | {"energy_provenance"} | EVIDENCE_MEMBERS) + 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"} | EVIDENCE_MEMBERS) - label = energy_label(j) + 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",))) @@ -1716,7 +1792,7 @@ def verify_receipt_v2(r, index_files): j = r["json"] if r["version"] != 2: raise Refused("receipt-version") - if len(index_files) != len(j["components"]): + 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): @@ -1731,6 +1807,8 @@ def verify_receipt_v2(r, index_files): 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") @@ -2212,32 +2290,50 @@ def render_catalogs(P, ix, c, floor, size, policy_hex): lines += [f' ' for m in mine for e in m] lines += [" ", " "] out[f"dash/{name}.mpd"] = text(lines + mpd_tail) - # §8.4: one track per (layer, deadline), by layer, then by deadline ascending. - tracks, groups = [], {} - for e in E: - if e["role"] in (BASE, REFINEMENT): - groups.setdefault((e["layer"], e["deadline"]), []).append(e) - lag = P["live"]["layer_lag_ms"] - for (layer, deadline), es in sorted(groups.items()): - dl = deadline if layer and deadline != P["defaults"]["deadline_ms"] else None + 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": f"{c}/l{layer}" + (f"@{dl}" if dl is not None else ""), "packaging": "wai", "renderGroup": 1, + 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 any(e["role"] == FLOOR for e in E): + if layer == 0 and floor is not None: t["altGroup"] = 1 if layer: - t["depends"] = [f"{c}/l{layer - 1}"] - t["wai_delivery_timeout_ms"] = es[0]["deadline"] - lag[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) - tracks.append({"altGroup": 1, "codec": F, "height": FH, "name": f"{c}/floor", "packaging": "cmaf", - "renderGroup": 1, "width": FW}) - out["moq/catalog.json"] = (jcs({"tracks": tracks, "version": 1}) + "\n").encode() - return out + 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 @@ -2261,9 +2357,15 @@ def load(path): def tree(root): - """Every file below root, as listing paths, in byte order.""" + """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.""" out = [] - for at, _, files in os.walk(root): + for at, dirs, files in os.walk(root): + 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] return sorted(out, key=lambda p: p.encode()) @@ -2300,14 +2402,21 @@ def verify_pack(rep, d, name): pkg = 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"] - rep.check(all(all(segment_ok(s) for s in p.split("/")) for p in files) - and len({p.lower() for p in files}) == len(files), f"{tag}: every path a listing path (SPEC §3.1)") - rep.check(load(os.path.join(pkg, "LISTING")) == listing(pkg, others), - f"{tag}: LISTING is the listing of the other files (listing-mismatch)") + 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") @@ -2319,7 +2428,10 @@ def verify_pack(rep, d, name): rep.check(False, f"{tag}: package.receipt.json does not read as version 2 ({e.code})") return j = r["json"] - rep.check(raw == jcs(j).encode(), f"{tag}: package.receipt.json is RFC 8785 canonical JSON (§9.1)") + 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") @@ -2334,11 +2446,26 @@ def verify_pack(rep, d, name): given = read_json(pj) if isinstance(given, dict) and isinstance(given.get("royalty_free_only"), bool): P["royalty_free_only"] = given["royalty_free_only"] - rep.check(jcs(P).encode() == pj, f"{tag}: policy.json is policy.toml's effective canonical JSON (policy-json-mismatch)") + 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"]] - rep.check(all(segment_ok(i) for i in ids) and len(set(cls)) == len(cls) and "classic" not in cls, - f"{tag}: component and class ids are path segments, class ids distinct in any case, none `classic` (pack-id)") + 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 = [] @@ -2361,7 +2488,12 @@ def verify_pack(rep, d, name): named = {"LISTING", "policy.toml", "policy.json", "package.receipt.json"} | {c["index"] for c in j["components"]} for c, ix in zip(j["components"], ixs): - named |= verify_component(rep, tag, root, pkg, files, P, j, c, ix) + try: + named |= verify_component(rep, tag, root, pkg, files, P, j, c, ix) + 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" @@ -2387,6 +2519,39 @@ def unreached(files, c): 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): """§11.3 per component, then the source's side. Returns the files it names.""" t = f"{tag} {c['id']}" @@ -2396,18 +2561,11 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): return set() tb = (comp["timebase"]["num"], comp["timebase"]["den"]) num, den = tb - rep.check(ix["tb"] == tb and MEDIA_CLASSES[ix["media_class"]] == comp["media"], - f"{t}: the index's timebase and media class are the policy's component's (index-policy-mismatch)") pieces, E = ix["pieces"], ix["entries"] - caps = [] - for e in E: - if e["cap"] not in caps: - caps.append(e["cap"]) - rep.check(ix["strings"] == [c["id"]] + [x["id"] for x in P["class"]] + caps, - f"{t}: the strings are the component's id, the class ids, each capability at its first use (§6.2)") - rep.check([(x["name"], x["max_layer"], x["flags"], x["max_kbps"]) for x in ix["classes"]] - == [(x["id"], x["max_layer"], int(x["floor_only"]), x.get("max_kbps", 0)) for x in P["class"]], - f"{t}: the class records are the policy's classes, in its order (§6.2)") + 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(), @@ -2474,7 +2632,7 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): 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)") + 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] @@ -2482,26 +2640,40 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): 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)") - rep.check(mans[i].get("target") == mans[base].get("target"), - f"{t}: piece {k}: the floor's target is its piece's (§5.2)") + 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: - if fp is None: - raise Malformed("not §7.2's framing") 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 for o in units): - raise Malformed("a unit that is not one temporal unit") + 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 o in units for typ, whole, _ in o if typ == OBU_SEQ), 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("no sequence header") + 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})") + 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)") + 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)) @@ -2798,6 +2970,20 @@ def verify(d): 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") return rep diff --git a/wai/pack-conformance/packs/video-base-01/package/LISTING b/wai/pack-conformance/packs/video-base-01/package/LISTING index 1a7563f66..b8ce14807 100644 --- a/wai/pack-conformance/packs/video-base-01/package/LISTING +++ b/wai/pack-conformance/packs/video-base-01/package/LISTING @@ -26,7 +26,7 @@ f3eedf9eb2870c0160801397412e7a4c3aca4a6e16f1a69a276e1356d38c9220 objects/61c7c3 a3135205d3139b39409d4e4e2aafed2c042797d293a9f9fc8664467165336e25 objects/7cc2b824ad6105abfccae27093190311b6a2348842e571684b021a213341b94a.wai 3c66c0b25b759cd1fe6245d282ba928441b873d62e4ac62c95260e45a80fff77 objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai b2a40a9a8cec3dcaf6c92a07d6765203c412c57ec879b7e33a52eabb2e74609d objects/c2ce1907255a041c388afd06ccd007436368e8d20467fdec2a1eebbb84b03bf2.wai -d3f0cbc108b844f87d73a0b6f71cfb2141dbb4c9c1aaf38fce11054657481968 package.receipt.json +506811bffac615caaf76ea20a3096ef07ef698a1e6a7d0ddddc940298dc68f5a package.receipt.json 517630d9bee1f5559baff3bb3e642f6aa7b5276226f6e3bdba4f3f9724794859 plan-v.txt a497fdf29528c4744ce8548e3b430d6311f15c10ca2a4cb548fb9958d3d98f07 policy.json d667cfa0c1e4e52f254f3a89a80cb0d3b6be0d833c40c8b44f60be33765859b0 policy.toml 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 index 94d3b4684..e7594dd7a 100644 --- a/wai/pack-conformance/packs/video-base-01/package/package.receipt.json +++ b/wai/pack-conformance/packs/video-base-01/package/package.receipt.json @@ -1 +1 @@ -{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"5ffc2be3f21af79078a1158e3d669e4268a35b0ca61a6e73ff252fdf38f346d0","media_class":"video","stage_root":"bd0c7799633b108cf9ccb0c7a4cedbdf6173a83470d3d96b94d075c03e0db7b6"}],"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 +{"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/wai-pack/src/catalog.rs b/wai/wai-pack/src/catalog.rs index b746e6552..870c71f08 100644 --- a/wai/wai-pack/src/catalog.rs +++ b/wai/wai-pack/src/catalog.rs @@ -269,19 +269,21 @@ 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`. -fn timeline(ix: &StageIndex, ps: &[usize], tb: Timebase, indent: &str) -> String { +/// 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 _ = writeln!(o, "{indent} ", (piece.start - ix.pieces[0].start) * tb.den as u64, piece.duration as u64 * tb.den as u64); + 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}"); - o + Ok(o) } /// `dash/classic.mpd`. -pub fn render_dash_classic(v: &CompView) -> String { +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)); @@ -290,13 +292,13 @@ pub fn render_dash_classic(v: &CompView) -> String { 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, " ")); + 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); - o + Ok(o) } /// `dash/.mpd`, or `None` when the class lists no WAI object. @@ -318,7 +320,7 @@ pub fn render_dash_class(v: &CompView, c: usize) -> Result, Catal 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, " ")); + 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)); } @@ -339,58 +341,74 @@ fn tier_label(t: u8) -> Option<&'static str> { /// `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`. -pub fn render_moq_catalog(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64) -> String { +/// +/// 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(); - let max_layer = v.index.entries.iter().map(|e| e.layer).max().unwrap_or(0); - for k in 0..=max_layer { - let layer: Vec<&EntryRec> = v.index.entries.iter().filter(|e| e.layer == k && matches!(e.role, Role::Base | Role::Refinement)).collect(); - let mut deadlines: Vec = layer.iter().map(|e| e.deadline_ms).collect(); - deadlines.sort_unstable(); - deadlines.dedup(); - for d in deadlines { - let on: Vec<&&EntryRec> = layer.iter().filter(|e| e.deadline_ms == d).collect(); - let mut caps: Vec = Vec::new(); - for e in &on { - let c = v.index.capability(e).to_string(); - if !caps.contains(&c) { - caps.push(c); - } + 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 name = if k == 0 || d as u64 == default_deadline { format!("{id}/l{k}") } else { format!("{id}/l{k}@{d}") }; - let mut t = json!({ - "name": name, - "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 { - t["depends"] = json!([format!("{id}/l{}", k - 1)]); - 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); } + 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})).expect("a catalog is in the canonical subset") + "\n" + 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. -pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> String { +/// `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 @@ -405,7 +423,8 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> St s }) .collect(); - let mut o = json!({"dur": piece.duration, "p": v.index.header.first_piece + p as u64, "stages": stages, "start": piece.start}); + 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); } @@ -418,7 +437,9 @@ pub fn render_stage_view(v: &CompView, c: usize, policy_digest: &[u8; 32]) -> St } 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)})).expect("a stage view is in the canonical subset") + "\n" + 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. @@ -430,8 +451,8 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol } 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()); + 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()); @@ -442,7 +463,7 @@ pub fn render_all(v: &CompView, layer_lag_ms: &[u64], default_deadline: u64, pol 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()); + out.insert(format!("stages/{class}.json"), render_stage_view(v, c, policy_digest)?.into_bytes()); } Ok(out) } @@ -491,20 +512,60 @@ mod tests { 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(); + 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 }; 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/golden.rs b/wai/wai-pack/src/golden.rs index b46955276..565cf0387 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -9,6 +9,8 @@ //! - `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. @@ -412,11 +414,40 @@ pub fn mutations() -> (Files, Value) { 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()); - cases.push(json!({"name": format!("receipt-{name}"), "kind": "receipt", "receipt": path, "indexes": [index], "reason": reason})); + 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})) } @@ -441,6 +472,125 @@ pub fn case_verdict(c: &Value, files: &Files) -> Result { }) } +// ─────────────────────────────── 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`; 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> { + use crate::catalog::{CompView, FloorView}; + files.remove("LISTING"); + let policy = crate::policy::Policy::parse_bytes(files.get("policy.toml").ok_or("no policy.toml")?).map_err(|e| e.to_string())?; + files.insert("policy.json".into(), policy.canonical_json().into_bytes()); + let policy_digest = policy.digest(); + 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})) +} + // ─────────────────────────────── version-1 receipts ─────────────────────────────── /// The version-1 receipts and their JSON: the legacy receipt `wai-rs` pins @@ -509,6 +659,9 @@ pub fn regen(dir: &Path) -> std::io::Result<()> { } 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)?; Ok(()) } @@ -573,5 +726,13 @@ pub fn check(dir: &Path) -> std::io::Result> { 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()); + } Ok(out) } diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index fff6fbdb0..5c8f75ab9 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -197,7 +197,8 @@ pub fn cmaf_files(comp: &str, floor: &Floor, starts: &[u64], tb_den: u32, timesc 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 f = crate::cmaf::write_fragment(p as u32 + 1, starts[p] * tb_den as u64, &samples); + 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)) diff --git a/wai/wai-pack/src/verify.rs b/wai/wai-pack/src/verify.rs index 47f92f24b..70322abaf 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -1,32 +1,42 @@ -//! A package's verifier ([packaging] §9, §11): everything a package states that its own -//! files decide, recomputed from those files. It reads no source: a statement only a -//! source decides (an entry's `sse`) is the producer's. +//! 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 order, each failure a reason of §12: +//! In §11.3's order, each failure a reason of §12: //! -//! 1. `LISTING` lists every other file with its SHA-256 (`listing-missing`, -//! `listing-mismatch`); no file is outside the layout (`unexpected-file`). -//! 2. The receipt reads as version 2 (`receipt-json`, `receipt-version`); `policy.json` -//! hashes to its policy digest (`policy-digest-mismatch`) and is the canonical form of -//! `policy.toml` with its `royalty_free_only` (`policy-json-mismatch`). -//! 3. The receipt verifies against the indexes (`index-missing`, and -//! `PackageReceiptV2::verify_detailed`'s reasons, the index rules among them). -//! 4. Per component: the plan file holds the pieces' starts and hashes to the index's plan -//! digest (`plan-mismatch`); every entry's object is held (`object-missing`) and is -//! what its entry says (the object rules of `stage_index::check_object`); every -//! decode-equivalent prefix decodes to its digest (`prefix-digest`); every object file -//! is some entry's (`object-unlisted`). -//! 5. The floor: each floor object presents its piece's base (`floor-motif-mismatch`) and -//! its CMAF files are the ones its samples give (`floor-cmaf-mismatch`). -//! 6. The catalogs are the ones the index, objects and floor give (`catalog-mismatch`, -//! `catalog-unexpected`). -//! 7. A metered receipt has its measurement claim (`measurement-claim-missing`), which -//! verifies and checks against the receipt (`measurement-claim-invalid`). +//! 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 (`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, with `policy.json`'s `royalty_free_only`, is `policy.json` +//! (`policy-json-mismatch`); its ids can name files (`pack-id`); 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::policy::{Overrides, Policy, Timebase}; +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}; @@ -61,29 +71,86 @@ pub struct VerifyReport { pub metered: bool, } -/// Verify the package in `dir`. +/// 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, or a name that is not UTF-8 is +/// 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())?)); + } + 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 = wai::pin::read_dir_set(dir).map_err(|e| vec![reason("unexpected-file", e.to_string())])?; + 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 != "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. LISTING. + // 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"))); } - // 2. Receipt and policy. - let text = files.get("package.receipt.json").ok_or_else(|| one(reason("receipt-json", "no package.receipt.json")))?; - let text = std::str::from_utf8(text).map_err(|_| one(reason("receipt-json", "not UTF-8")))?; + // 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"); @@ -98,102 +165,39 @@ pub fn verify_files(files: &BTreeMap>) -> Result = 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)))?; - let mut errs: Vec = Vec::new(); + // 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) in r.components.iter().zip(&index_bytes) { + for ((c, bytes), comp) in r.components.iter().zip(&index_bytes).zip(&policy.components) { expected.insert(c.index_file()); - let ix = StageIndex::parse(bytes).expect("verified above"); - let comp = policy.component(&c.id).ok_or_else(|| one(reason("receipt-component", format!("the policy has no component {}", c.id))))?; - let tb = comp.timebase; - if (ix.header.tb_num, ix.header.tb_den) != (tb.num, tb.den) { - errs.push(reason("index-policy-mismatch", format!("component {}: the index's timebase is not the policy's", c.id))); - } - // 4. Plan. - let plan_path = format!("plan-{}.txt", c.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"))), - } - // Objects. - 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"))); - } - } - // 5. Floor. - let floor = match floor_view(&c.id, &ix, &objects, tb) { - Ok((view, produced)) => { - 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 samples give"))); - } - } - view - } - Err(e) => { - errs.push(e); - None - } - }; - if !errs.is_empty() { - return Err(errs); - } - // 6. Catalogs. - 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 }; - match catalog::render_all(&view, &policy.layer_lag_ms, policy.deadline_ms, &r.policy_digest) { - Ok(m) => catalog_files.extend(m), - Err(e) => errs.push(reason("catalog-mismatch", e.0)), - } + 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. Measurement claim. + // 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()); @@ -230,6 +234,151 @@ pub fn verify_files(files: &BTreeMap>) -> Result 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())?; @@ -241,40 +390,88 @@ fn decode_frames(cap: &str, env: &[u8]) -> Result<(Vec>, u32, u32), Stri type FloorFiles = (Option, Vec<(String, Vec)>); -/// The floor of a component as its objects give it: each floor object presents its -/// piece's base, and its CMAF files are rebuilt from the objects' temporal units. -fn floor_view(comp: &str, ix: &StageIndex, objects: &BTreeMap<[u8; 32], Vec>, tb: Timebase) -> Result { +/// 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 Some(fe) = es.iter().find(|e| e.role == Role::Floor) else { return Ok((None, Vec::new())) }; + 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 w = wai::container::Wai::from_bytes(&objects[&fe.content_hash]).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?; + 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"))); } - let (units, _) = crate::floor::av1_units(&w.payload).ok_or_else(|| reason("floor-cmaf-mismatch", format!("piece {p}: the floor's payload is not the reference AV1 framing")))?; - if units.len() as u64 != ix.pieces[p].duration as u64 { - return Err(reason("floor-cmaf-mismatch", format!("piece {p}: {} units for {} frames", units.len(), ix.pieces[p].duration))); + 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 mut key = Vec::new(); + 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 tus = crate::av1::temporal_units(u).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?; - let [tu] = tus.as_slice() else { return Err(reason("floor-cmaf-mismatch", format!("piece {p} unit {i} is not one temporal unit"))) }; - if seq.is_none() { - let s = tu.sequence_header().ok_or_else(|| reason("floor-cmaf-mismatch", "the floor's first unit has no sequence header"))?; - seq = Some((crate::av1::SequenceHeader::parse(s.payload).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?, s.bytes.to_vec())); + 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"))); } - key.push(crate::av1::starts_with_shown_key_frame(tu, &seq.as_ref().expect("set").0).map_err(|e| reason("floor-cmaf-mismatch", e.to_string()))?); + 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, &fl, &starts, tb.den, tb.num).map_err(|e| reason("floor-cmaf-mismatch", e))?; + 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, diff --git a/wai/wai-pack/tests/review.rs b/wai/wai-pack/tests/review.rs new file mode 100644 index 000000000..b9758be7e --- /dev/null +++ b/wai/wai-pack/tests/review.rs @@ -0,0 +1,391 @@ +//! 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::{Role, 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 +} + +fn policy_edit(f: &mut Files, from: &str, to: &str) { + let t = String::from_utf8(f["policy.toml"].clone()).unwrap(); + assert!(t.contains(from), "{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. +fn floor_of(f: &Files, p: usize) -> (usize, Vec) { + let ix = index(f); + let k = ix.pieces[p].first_entry as usize + ix.piece_entries(p).iter().position(|e| e.role == Role::Floor).unwrap(); + (k, f[&wai_pack::objects::object_path(&ix.entries[k].content_hash)].clone()) +} + +/// Replace entry `k`'s object with `bytes`, its entry following (hash and lengths). +fn replace_object(f: &mut Files, k: usize, bytes: Vec) { + let mut ix = index(f); + let old = ix.entries[k].content_hash; + let h = wai_pack::objects::object_hash(&bytes); + let man = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); + for e in ix.entries.iter_mut().filter(|e| e.content_hash == old).take(1) { + 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(&wai_pack::objects::object_path(&old)); + } + f.insert(wai_pack::objects::object_path(&h), bytes); + // Duplicate flags follow the new content. + let mut seen = std::collections::BTreeSet::new(); + for e in ix.entries.iter_mut() { + if seen.insert(e.content_hash) { + e.flags &= !wai::stage_index::ENTRY_DUP; + } else { + e.flags |= wai::stage_index::ENTRY_DUP; + } + } + set_index(f, &ix); +} + +/// A floor object's manifest JSON and its temporal units, and the envelope back. +fn floor_parts(b: &[u8]) -> (serde_json::Value, wai_pack::floor::Av1Header, Vec>) { + let ml = u32::from_le_bytes(b[4..8].try_into().unwrap()) as usize; + let man: serde_json::Value = serde_json::from_slice(&b[8..8 + ml]).unwrap(); + let (units, hdr) = wai_pack::floor::av1_units(&b[12 + ml..]).unwrap(); + (man, hdr, units) +} + +fn floor_envelope(man: &serde_json::Value, hdr: (u32, u32, u32, u32), units: &[Vec]) -> Vec { + let m = serde_json::to_vec(man).unwrap(); + let p = wai_pack::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() +} + +#[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() { + // A version-2 receipt over no component at all, signed with the package key. + let g = golden_files(); + let mut r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&g["package.receipt.json"]).unwrap()).unwrap(); + r.components.clear(); + r.total_entries = 0; + r.unique_objects = 0; + r.total_bytes = 0; + r.stored_bytes = 0; + r.work_units = 0; + for parent in [wai::package::Parent::None, wai::package::Parent::LiveWindow([9; 32])] { + r.parent = parent; + use ed25519_dalek::Signer; + r.sig = key().sign(&r.signing_payload()).to_bytes(); + let mut empty: Files = ["policy.toml", "policy.json"].iter().map(|p| (p.to_string(), g[*p].clone())).collect(); + empty.insert("package.receipt.json".into(), r.to_json().into_bytes()); + golden::relist(&mut empty); + assert_eq!(first(&empty), "policy-component-mismatch"); + } + // The index's class records are not the policy's (a floor-only class offered WAI). + let mut f = g.clone(); + let mut ix = index(&f); + ix.classes[2].flags = 0; + ix.plan[2] = ix.plan[0].clone(); + set_index(&mut f, &ix); + assert_eq!(first(&resealed(f)), "index-policy-mismatch"); + // An entry's capability is no rung of its component. + let mut f = g.clone(); + let mut ix = index(&f); + let at = ix.strings.iter().position(|s| s == "wai.video.av1").unwrap(); + ix.strings[at] = "wai.video.avc".into(); + set_index(&mut f, &ix); + assert_eq!(first(&resealed(f)), "index-policy-mismatch"); + // The string table in another order. + let mut f = g.clone(); + let mut ix = 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_index(&mut f, &ix); + assert_eq!(first(&resealed(f)), "index-policy-mismatch"); + // A class plan the policy does not give. + let mut f = g.clone(); + let mut ix = index(&f); + let p = ix.plan[1].iter().position(|&v| v == wai::stage_index::FLOOR_ONLY).unwrap(); + ix.plan[1][p] = 0; + set_index(&mut f, &ix); + assert_eq!(first(&resealed(f)), "index-policy-mismatch"); + // A class named classic, which would collide with the classical master. + let mut f = g.clone(); + policy_edit(&mut f, "id = \"legacy\"", "id = \"classic\""); + let mut ix = index(&f); + let at = ix.strings.iter().position(|s| s == "legacy").unwrap(); + ix.strings[at] = "classic".into(); + set_index(&mut f, &ix); + assert_eq!(first(&resealed(f)), "pack-id"); + // Under royalty_free_only, a patent-pool floor. + let mut f = g.clone(); + policy_edit(&mut f, "capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.video.avc\"\nrole = \"floor\""); + assert_eq!(first(&resealed(f)), "license-refused:wai.video.avc:patent-pool"); + // The policy's component of another medium. + let mut f = g.clone(); + policy_edit(&mut f, "media = \"video\"", "media = \"image\""); + assert_eq!(first(&resealed(f)), "policy-component-mismatch"); +} + +/// Item 2: a malformed package is refused, never panicked on. +#[test] +fn malformed_packages_are_refused() { + let g = golden_files(); + // A floor object removed. + let mut f = g.clone(); + let (k, _) = floor_of(&f, 1); + f.remove(&wai_pack::objects::object_path(&index(&f).entries[k].content_hash)); + golden::relist(&mut f); + assert_eq!(first(&f), "object-missing"); + // A floor object corrupted. + let mut f = g.clone(); + let (k, mut b) = floor_of(&f, 2); + *b.last_mut().unwrap() ^= 1; + f.insert(wai_pack::objects::object_path(&index(&f).entries[k].content_hash), b); + golden::relist(&mut f); + assert!(codes(&f).iter().all(|c| c == "object-hash"), "{:?}", codes(&f)); + // A live window whose pieces start past 2^53 − 1: no stage view can be written. + let mut f = g.clone(); + let mut ix = index(&f); + ix.header.prev_index_digest = [3; 32]; + let shift = 1u64 << 53; + for p in ix.pieces.iter_mut() { + p.start += shift; + } + let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); + ix.header.plan_digest = wai::stage_index::plan_digest(&starts); + set_index(&mut f, &ix); + f.insert("plan-v.txt".into(), wai_pack::pack::plan_file(&starts).into_bytes()); + assert_eq!(first(&resealed(f)), "catalog-mismatch"); + // An on-demand index whose first piece does not start at 0. + 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) = floor_of(&g, 0); + let (_, _, units) = 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() { + let g = golden_files(); + let (k, b) = floor_of(&g, 2); + let (man, hdr, units) = floor_parts(&b); + let mut rotated = units.clone(); + rotated.rotate_left(1); + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&man, hdr, &rotated)); + assert_eq!(first(&resealed(f)), "floor-keyframe-mismatch"); + // A key frame inside the piece: refused, unless extra key frames are allowed. + let mut extra = units.clone(); + extra[5] = units[0].clone(); + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&man, hdr, &extra)); + assert_eq!(first(&resealed(f.clone())), "floor-keyframe-mismatch"); + policy_edit(&mut f, "[floor]\n", "[floor]\nextra_keyframes = true\n"); + assert_eq!(first(&resealed(f)), "ok"); + // A sequence header that is not the first's bytes. + let mut other = units.clone(); + let tu = wai_pack::av1::temporal_units(&other[0]).unwrap().remove(0); + let s = tu.sequence_header().unwrap(); + let at = other[0].windows(s.bytes.len()).position(|w| w == s.bytes).unwrap(); + let last = at + s.bytes.len() - 1; + other[0][last] ^= 0x01; + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&man, hdr, &other)); + assert_eq!(first(&resealed(f)), "floor-av1-malformed"); +} + +/// Item 6: a supplied floor's members are §7.2's. +#[test] +fn a_floor_object_is_bound_to_its_profile() { + let g = golden_files(); + let (k, b) = floor_of(&g, 2); + let (man, hdr, units) = floor_parts(&b); + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&man, (1080, 1920, 30000, 1001), &units)); + assert_eq!(first(&resealed(f)), "floor-object-mismatch"); + let mut m = man.clone(); + m["derived_from"]["renderer"] = "anything@1".into(); + m["derived_from"]["settings"] = "00".repeat(32).into(); + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&m, hdr, &units)); + assert_eq!(first(&resealed(f)), "floor-object-mismatch"); + let mut m = man.clone(); + m["target"] = serde_json::json!({"frames": 1, "h": 1, "w": 1}); + let mut f = g.clone(); + replace_object(&mut f, k, floor_envelope(&m, hdr, &units)); + assert_eq!(first(&resealed(f)), "floor-object-mismatch"); +} + +/// Item 7: a directory read strictly, dot-names and `*.sha256` files included. +#[test] +fn hidden_files_are_refused() { + let dir = std::env::temp_dir().join(format!("wai-pack-hidden-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + for (name, extra) in [("dot", ".hidden"), ("dot-dir", ".git-objects/blob"), ("dot-object", "objects/.x.wai"), ("sha256", "payload.sha256")] { + let d = dir.join(name); + for (p, b) in golden_files() { + let path = d.join(&p); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(path, b).unwrap(); + } + let path = d.join(extra); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::write(&path, b"anything").unwrap(); + let got = wai_pack::verify::verify_dir(&d).err().map(|v| v[0].code.clone()); + assert!(matches!(got.as_deref(), Some("unexpected-file") | Some("listing-mismatch")), "{extra}: {got:?}"); + let files = wai_pack::verify::read_strict(&d).unwrap(); + assert!(files.contains_key(extra), "{extra} is read"); + } + 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() { + let g = golden_files(); + let text = String::from_utf8(g["package.receipt.json"].clone()).unwrap(); + let v: serde_json::Value = serde_json::from_str(&text).unwrap(); + for bad in [serde_json::to_string_pretty(&v).unwrap(), format!("{text}\n"), text.replacen("{\"asset_id\"", "{\"asset_id\":\"video-base-01\",\"asset_id\"", 1)] { + let mut f = g.clone(); + f.insert("package.receipt.json".into(), bad.into_bytes()); + golden::relist(&mut f); + assert_eq!(first(&f), "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"); +} diff --git a/wai/wai-rs/src/package.rs b/wai/wai-rs/src/package.rs index e1a85f792..938879ead 100644 --- a/wai/wai-rs/src/package.rs +++ b/wai/wai-rs/src/package.rs @@ -29,7 +29,9 @@ 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}; +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}; @@ -245,7 +247,6 @@ fn grant_fits_json(g: &GrantRef) -> bool { /// 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> { - use crate::energy_class::{JSON_CALIBRATION_REF, JSON_CLASS, JSON_FILTERING, JSON_UNCERTAINTY}; 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()) { @@ -260,6 +261,14 @@ fn exact_members(o: &Map, names: &[&str]) -> Result<(), String> { 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"))) } @@ -336,6 +345,9 @@ impl PackageReceiptV2 { 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 { @@ -393,7 +405,8 @@ impl PackageReceiptV2 { /// Verify the receipt against its components' index files, in its order: /// - /// 1. one index per component (`receipt-index-count`); each parses and checks (its + /// 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`); @@ -403,8 +416,8 @@ impl PackageReceiptV2 { /// 4. the energy figure and its class (`receipt-energy`); /// 5. the signature (`receipt-signature`). pub fn verify_detailed(&self, indexes: &[&[u8]]) -> Result<(), PackageReason> { - if indexes.len() != self.components.len() { - return err("receipt-index-count", format!("{} indexes for {} components", indexes.len(), self.components.len())); + 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) { @@ -460,6 +473,7 @@ impl PackageReceiptV2 { 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, @@ -496,20 +510,24 @@ impl PackageReceiptV2 { jcs(&v).expect("checked at seal") } - /// Read a receipt from its JSON. Refused (`receipt-json`): JSON that does not parse, - /// a member this version does not define or one it requires missing, a value of the - /// wrong form, 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. + /// 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: Value = serde_json::from_str(text).map_err(|e| bad(e.to_string()))?; + let v = read_json(text)?; let o = v.as_object().ok_or_else(|| bad("not an object".into()))?; exact_members( o, &[ - "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", + "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)?; @@ -538,6 +556,14 @@ impl PackageReceiptV2 { 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")?, @@ -624,11 +650,14 @@ impl PackageReceipt { } /// Read a version-1 receipt from its JSON (`"version"` 1, or absent). Refused as - /// [`PackageReceiptV2::from_json`] refuses; the signature is checked by `verify`. + /// [`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: Value = serde_json::from_str(text).map_err(|e| bad(e.to_string()))?; + 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", @@ -689,7 +718,7 @@ 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: Value = serde_json::from_str(text).map_err(|e| PackageReason::new("receipt-json", e.to_string()))?; + 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), diff --git a/wai/wai-rs/src/package/tests.rs b/wai/wai-rs/src/package/tests.rs index 5ff09a031..7d6e8a166 100644 --- a/wai/wai-rs/src/package/tests.rs +++ b/wai/wai-rs/src/package/tests.rs @@ -122,6 +122,18 @@ fn every_receipt_mutation_names_its_reason() { } 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]; @@ -156,6 +168,7 @@ fn seal_refuses_what_it_cannot_sign() { }; 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"); @@ -189,7 +202,7 @@ fn the_v2_json_is_canonical_and_round_trips() { 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.get("energy_provenance").is_some(), r.energy_provenance.is_some()); + 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(); @@ -213,6 +226,36 @@ fn the_v2_json_reader_refuses_what_it_does_not_define() { 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 diff --git a/wai/wai-rs/src/stage_index.rs b/wai/wai-rs/src/stage_index.rs index 9ce305dd1..11cacae57 100644 --- a/wai/wai-rs/src/stage_index.rs +++ b/wai/wai-rs/src/stage_index.rs @@ -620,36 +620,46 @@ impl StageIndex { self.piece_entries(i).iter().map(|e| e.layer).max().unwrap_or(0) } - /// Every rule of §6 the index alone decides, in this order; the first that fails is - /// the reason: + /// Every rule of §6.3 the index alone decides, in §6.3's order; the first that fails + /// is the reason: /// - /// 1. the timebase is not zero (`index-timebase`); pieces follow each other, each - /// starting where the one before ended and lasting at least one unit - /// (`piece-order`); the pieces' entries are the index's entries, in order, at - /// least one each, every entry naming its piece's absolute number - /// (`piece-entries`); an unknown piece kind or flag (`piece-kind`); - /// 2. per entry, in order: an unknown flag (`entry-flags`); within its piece, roles - /// in the order base, refinement, floor, others, and refinements in layer order - /// (`entry-order`); a base or floor at layer 0, a refinement above it, and - /// `refines` exactly on a refinement (`layer-role`); `refines` inside the window - /// (`refines-outside-window`) and naming an earlier entry of the same piece - /// (`refines-mismatch`) one layer below (`layer-gap`); a deadline only on a - /// refinement (`deadline-role`); a floor carrying `derived_from` (`floor-derived-from`); - /// `entropy-consistency` only on layer 0 (`tier-entropy-passthrough`); - /// decode-equivalence on a refinement only over a decode-equivalent prefix by a - /// decode-equivalent step (`tier-weakest`); otherwise the tier §6.3 gives - /// (`tier-mismatch`); a non-zero prefix digest only with decode-equivalence - /// (`digest-without-tier`); the duplicate flag exactly when an earlier entry has - /// the content hash (`dup-flag`), at the same length (`dup-length`); - /// 3. the classes: an unknown flag (`class-flags`); one plan row per class and one - /// value per piece, each [`FLOOR_ONLY`] for a floor-only class, otherwise at most - /// the class's `max_layer` and the piece's highest layer (`class-plan`). + /// 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 { -- 2.54.0 (Apple Git-157) From 267eb52e01b704fde163a1ae4ff3cdfa5f4bfeed Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 07:44:38 -0400 Subject: [PATCH 20/28] ci: pack_verify.py's Ed25519 library installed from a hashed requirements file with --require-hashes cryptography, cffi and pycparser pinned to one release each, every distribution file of each listed by SHA-256; a wrong hash stops the install. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-pack.yml | 8 +- wai/pack-conformance/requirements-verify.txt | 170 +++++++++++++++++++ 2 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 wai/pack-conformance/requirements-verify.txt diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 5c88b98fa..db8e5b449 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -116,10 +116,12 @@ jobs: python-version: '3.11' # 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, as the claim verifiers check it. - - name: Install the Ed25519 library + # `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 - run: python -m pip install cryptography + 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 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 -- 2.54.0 (Apple Git-157) From 80dbd8d3e4d780979582ee6935e26c0b950ceb4c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 07:50:50 -0400 Subject: [PATCH 21/28] wai-pack: every packer refusal tested and listed; pack-capability-unsupported names its capability; a meter with no figure refuses the pack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tests/refusals.rs: each supplied-floor reason (floor-missing, floor-listing-mismatch, floor-stream-missing, floor-av1-malformed, floor-frame-count, floor-keyframe-mismatch, and an extra key frame allowed by the policy); pack-media-unsupported, pack-refinement-unsupported, pack-rule-ladder-unsupported, pack-floor-encode-unsupported and pack-capability-unsupported: for a base, a second base and a floor; pack-meter for a model past its range and one that rounds to nothing; the encoder's caps on a 4K piece of 50 frames. - pack: the policy's own refusals in the verifier's order (policy_refusal); two base rungs name the second's capability; a meter that was asked for and gives no figure refuses with pack-meter instead of packing unmetered. - packaging §11.4 and §12 list pack-encode, pack-piece-length, pack-catalog, pack-meter, pack-measurement-claim and pack-listing. CHANGELOG: no package of this revision carries a #WAI-LAYER line. - wai-pack.yml: test-count floors --lib 28, review 8, refusals 4. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-pack.yml | 4 +- wai/CHANGELOG.md | 7 +- wai/extensions/packaging.md | 16 ++- wai/wai-pack/src/bin/wai_pack.rs | 4 +- wai/wai-pack/src/pack.rs | 26 ++--- wai/wai-pack/tests/refusals.rs | 168 +++++++++++++++++++++++++++++++ 6 files changed, 203 insertions(+), 22 deletions(-) create mode 100644 wai/wai-pack/tests/refusals.rs diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index db8e5b449..9d7b13dc3 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -94,9 +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 26 + --lib 28 --test=conformance 1 --test=packs 7 + --test=review 8 + --test=refusals 4 --test=scenecut 3 --test=source_alloc 1 EOF diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 80333a7f1..53514a60c 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -126,9 +126,10 @@ wire format may change and such changes are recorded here. - **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 `#WAI-LAYER` lines and `#EXT-X-GAP` for a - piece the class takes as floor only; the classical and per-class DASH MPDs; the MoQ - catalog; and a stage view per class. + 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 diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index dc2f364dc..caf7ac465 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -952,9 +952,16 @@ refuses by name, before writing anything, what it does not pack yet: another med (`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:`); and a component with no source -(`pack-source-missing`) or whose source's frame rate is not its timebase -(`pack-source-rate`). +(`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`). ## 12. Reasons @@ -973,4 +980,5 @@ ladder subset (`pack-rule-ladder-unsupported`), a floor to encode | `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` | §11.1, §11.4 | +| `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` | §11.4 | diff --git a/wai/wai-pack/src/bin/wai_pack.rs b/wai/wai-pack/src/bin/wai_pack.rs index d7de8f0a1..805e691e9 100644 --- a/wai/wai-pack/src/bin/wai_pack.rs +++ b/wai/wai-pack/src/bin/wai_pack.rs @@ -15,7 +15,9 @@ //! files, catalogs, the receipt (and, metered, its measurement claim) and LISTING; //! 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 +//! 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 diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index 5c8f75ab9..fe07f774e 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -8,8 +8,10 @@ //! 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 base of another capability -//! (`pack-capability-unsupported`). +//! (`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). //! //! The package: //! @@ -212,9 +214,9 @@ pub fn plan_file(starts: &[u64]) -> String { /// 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); - if let Err(v) = crate::capcheck::check(&policy) { - let r = &v[0]; - return Err(PackError::new(r.code.clone(), format!("ladder.{}", r.rung))); + // 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")); @@ -222,11 +224,6 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut if comp.media != Media::Video { return Err(PackError::new("pack-media-unsupported", format!("component {} is {}", comp.id, comp.media.label()))); } - let classes_ok = policy.classes.iter().all(|c| id_is_path_segment(&c.id) && c.id != "classic"); - let folded: std::collections::BTreeSet = policy.classes.iter().map(|c| c.id.to_ascii_lowercase()).collect(); - if !id_is_path_segment(&comp.id) || !classes_ok || folded.len() != policy.classes.len() { - return Err(PackError::new("pack-id", "a component or class id that cannot name a file, or a class named classic")); - } 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")); } @@ -235,7 +232,8 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut } let bases = rungs_of(&policy, &comp.id, RungRole::Base); let [base] = bases.as_slice() else { - return Err(PackError::new("pack-capability-unsupported", "this revision packs one base rung")); + 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); @@ -378,9 +376,11 @@ pub fn pack(policy_bytes: &[u8], sources: &BTreeMap>, opts: &mut }; 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(); - let energy = match opts.meter.as_mut().and_then(|m| m.stop()) { - Some(r) => r.as_seal_args(), + // 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, diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs new file mode 100644 index 000000000..62e7c105d --- /dev/null +++ b/wai/wai-pack/tests/refusals.rs @@ -0,0 +1,168 @@ +//! 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, and the encoder's caps. + +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"); + // Piece 2 (frames 85 to 134) opening on an inter frame. + let mut moved = u.clone(); + moved[85..135].rotate_left(1); + assert_eq!(case(moved.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 what the +/// encoder holds, refused before anything is allocated; 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 r = wai_pack::encode::choose_q(&frames, h, w, &p); + assert!(r.is_err(), "{:?}", r.map(|q| q.q)); +} -- 2.54.0 (Apple Git-157) From 3d5a146ac410d4e654b755267ab87a13a615d1de Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 07:52:20 -0400 Subject: [PATCH 22/28] wai: window-start tested in wai-rs and in the corpus An on-demand index (no previous window) whose first_piece or first_entry is not 0, or whose first piece does not start at 0, is refused with window-start: three cases in the stage index's mutation test, and the same three as corpus index mutations that pack_verify.py re-derives. Co-Authored-By: Claude Opus 5.5 --- wai/pack-conformance/mutations/expected.json | 18 ++++++++++++++++++ .../index/on-demand-first-entry.wsix | Bin 0 -> 1088 bytes .../index/on-demand-first-piece.wsix | Bin 0 -> 1088 bytes .../mutations/index/on-demand-start.wsix | Bin 0 -> 1088 bytes wai/wai-pack/src/golden.rs | 7 +++++++ wai/wai-rs/src/stage_index/tests.rs | 8 ++++++++ 6 files changed, 33 insertions(+) create mode 100644 wai/pack-conformance/mutations/index/on-demand-first-entry.wsix create mode 100644 wai/pack-conformance/mutations/index/on-demand-first-piece.wsix create mode 100644 wai/pack-conformance/mutations/index/on-demand-start.wsix diff --git a/wai/pack-conformance/mutations/expected.json b/wai/pack-conformance/mutations/expected.json index 7d619b935..aa002b3e1 100644 --- a/wai/pack-conformance/mutations/expected.json +++ b/wai/pack-conformance/mutations/expected.json @@ -162,6 +162,24 @@ "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", 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 0000000000000000000000000000000000000000..64f6c433c5cbf49670e66c07ab2aad248b78eb34 GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9#0g%}sV4{2?Z~Elk#7WmOrnNGJB_zdh zO6l(2@x6&<-Ot9`g**^dn4}R@hy_gwCJu5xNREjC=wKu@^(7X4cO%`F3hemwPiUgV zWk$CjqPn6#<@*h4C#WQ`ac)9Z4Al1@2tZ)~!XUs-0D#nkz%+<*1_3nlmRhXg*ed_* z*t^}S&vGX>JhdrqyB6QtFTkmF{|%SIF_7}&>(5$`{_K(UBTa?S@)itRQ>&3Xq$wjNdHL_H2Xou0PSZY*Zvo1 z_V1|6ob^KM#(EEjgHq4*To;#Iuxa0_*8MB&P3XkWUBB$w(ZUPl9~MyPqenEFxiBuw z|0Pg!*kKY-n%MY+1_EXdUjPR5U2@qIxGcuGhF_e^vFq9`|>Xl`tq~_~o=9R?f=9gsV W=b?)hrKV-(rSc-FO)N8nmOo){L^*>1nt4kt)^Kc< ze|GHM?$l?wlN+Ae6t`WAZ|#?1&=<>@+iky61ZELbKZs%m`T-<-2N($itY?SV{{O!N z)ZP<=uhr>asXL_!SOD@>7Z&mC574{}{;^(ekcI{~41@aFIDD=@I8qHi7 z7v}#Gs5$H~2`Ei${KK39G7sc;Z~y>lV#6O-{5$UOy#LHd>xt{+-C1vKv!_)aP5=H` z!LrK7Q8lAE&R7fPe`Ncq>3>FeN`?doEV>yP%9t2R%0w8-6EpS7GE-9X^)mBH;&by$ YGV}A$MT=6?GV@Y-k<=!Z8A8kh01WHSVgLXD literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2711a16081add26ca7edbd6ff037e45ca3a56a6b GIT binary patch literal 1088 zcmWF!_KaX;U|^60Vi3&)#B4y!3gt5r06=9V0TbmDdDAENCQiDRF|Cy$EFme5Q%ZOD zj_*w@>wY%gF62Qs%m^v~qgl`tq(J!~_k-k^7=R8&Qd3`I(RVk}ZK=SHPyd7_N?c}i z`yr|;`cuB&pmu^v5*z0xWW_*z|A7D$1|SRq>;wQvJqS#LC}$8rGjFNI8jh{<&yKy@ zo%$?ya>G-b;lF?g+6*jqnQii!u($X zHHRH00i}tJf0#2s=7IbU4gercZ204ff5#o3_n#SQJ#n49JL|1&_O!~Q>EAyqSXTKs zs%A9D8Ee7(k8D3R{m%$b$&dhnMK>cu852WEnFvF9Vy0eMW=d+lUS?iNd~SY8W_}*J TXi;ifW?m{UlG?;FLx_0*ZCcL7 literal 0 HcmV?d00001 diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index 565cf0387..fe7fa2341 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -212,6 +212,13 @@ fn index_cases() -> Vec { ("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; + } + }), ] } diff --git a/wai/wai-rs/src/stage_index/tests.rs b/wai/wai-rs/src/stage_index/tests.rs index c7d33a074..9f293a4d7 100644 --- a/wai/wai-rs/src/stage_index/tests.rs +++ b/wai/wai-rs/src/stage_index/tests.rs @@ -165,6 +165,14 @@ fn every_index_mutation_names_its_reason() { }), ("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(); -- 2.54.0 (Apple Git-157) From b1a7a776ddcbb8daf8012361c92b40e85fd871f9 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 08:01:06 -0400 Subject: [PATCH 23/28] pack-conformance: package mutations, refused by both verifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review's package cases become a corpus, package-mutations/: each the golden altered and, where the receipt or LISTING alone would catch it, re-sealed with the golden's key, held as an overlay (the files that differ or are new, and the files it removes) with its reason. 26 cases: a receipt over no component (on demand and live), the index against its policy (class records, a capability, the string order, the class plan), a class named classic, a patent-pool floor under royalty_free_only, a component of another medium, a floor object missing and corrupted, a live window starting past 2^53 - 1, floor objects that do not open on a key frame / hold an extra one / have another sequence header / framing header / renderer and settings / target, an extra key frame the policy allows (ok), dot-names and a *.sha256 file read strictly, and receipt files that are not canonical or name a member twice. - golden.rs builds them (package_mutations), writes the overlays (regen) and requires each, applied to the golden, to get its reason from verify_files (check, so cargo test and CI). - pack_verify.py writes each case out and verifies it in §11.3's order without the golden's inputs, taking the reason of the first check that fails. Two fixes the corpus found in it: an index no catalog can be written for (a start past 2^53 - 1) is catalog-mismatch, not an uncaught ValueError; and "on-demand, one window" is an assertion about the golden, not a rule a package is held to (a live window is a package too, §6.1, §9.2). An object-rules failure now names its reason in brackets. - review.rs asserts the same cases by name, keeping what the corpus cannot hold (an on-demand index starting past 0 cannot be sealed, a fragment time past 2^64 - 1, the random alterations, the verifier's own directory walk). Co-Authored-By: Claude Opus 5.5 --- wai/pack-conformance/README.md | 18 +- wai/pack-conformance/pack_verify.py | 108 +++++- .../class-named-classic/LISTING | 34 ++ .../hls/classic/master.m3u8 | 5 + .../class-named-classic/index-v.wsix | Bin 0 -> 1728 bytes .../class-named-classic/package.receipt.json | 1 + .../class-named-classic/policy.json | 1 + .../class-named-classic/policy.toml | 47 +++ .../class-named-classic/stages/classic.json | 1 + .../class-named-classic/stages/mobile.json | 1 + .../class-named-classic/stages/tv.json | 1 + .../component-medium/LISTING | 35 ++ .../component-medium/index-v.wsix | Bin 0 -> 1728 bytes .../component-medium/package.receipt.json | 1 + .../component-medium/policy.json | 1 + .../component-medium/policy.toml | 47 +++ .../component-medium/stages/legacy.json | 1 + .../component-medium/stages/mobile.json | 1 + .../component-medium/stages/tv.json | 1 + .../dot-directory/.cache/blob | 1 + .../package-mutations/dot-file/.hidden | 1 + .../dot-object/objects/.x.wai | 1 + .../package-mutations/expected.json | 246 ++++++++++++++ .../floor-extra-key-frame-allowed/LISTING | 35 ++ .../dash/classic.mpd | 24 ++ ...8f1bebe3424054b97a2b09bdde13068ea8036b.m4s | Bin 0 -> 2929 bytes .../hls/classic/floor-v.m3u8 | 18 + .../hls/classic/master.m3u8 | 5 + .../hls/legacy/master.m3u8 | 5 + .../hls/mobile/master.m3u8 | 6 + .../hls/tv/master.m3u8 | 6 + .../index-v.wsix | Bin 0 -> 1728 bytes ...731ff239e0e6c861e2560be53aa536dfee1989.wai | Bin 0 -> 3032 bytes .../package.receipt.json | 1 + .../floor-extra-key-frame-allowed/policy.json | 1 + .../floor-extra-key-frame-allowed/policy.toml | 48 +++ .../stages/legacy.json | 1 + .../stages/mobile.json | 1 + .../stages/tv.json | 1 + .../floor-extra-key-frame/LISTING | 35 ++ .../floor-extra-key-frame/dash/classic.mpd | 24 ++ ...8f1bebe3424054b97a2b09bdde13068ea8036b.m4s | Bin 0 -> 2929 bytes .../hls/classic/floor-v.m3u8 | 18 + .../hls/classic/master.m3u8 | 5 + .../hls/legacy/master.m3u8 | 5 + .../hls/mobile/master.m3u8 | 6 + .../floor-extra-key-frame/hls/tv/master.m3u8 | 6 + .../floor-extra-key-frame/index-v.wsix | Bin 0 -> 1728 bytes ...731ff239e0e6c861e2560be53aa536dfee1989.wai | Bin 0 -> 3032 bytes .../package.receipt.json | 1 + .../floor-framing-header/LISTING | 35 ++ .../floor-framing-header/index-v.wsix | Bin 0 -> 1728 bytes ...9ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai | Bin 0 -> 2503 bytes .../floor-framing-header/package.receipt.json | 1 + .../floor-object-corrupt/LISTING | 35 ++ ...8559277f926322dd6ddc345306b8231f2d1752.wai | Bin 0 -> 2503 bytes .../floor-object-missing/LISTING | 34 ++ .../floor-opens-on-inter/LISTING | 35 ++ .../floor-opens-on-inter/dash/classic.mpd | 24 ++ ...6aa28e783a8cd220eed53a8d84de9feeb622d3.m4s | Bin 0 -> 2400 bytes .../hls/classic/floor-v.m3u8 | 18 + .../floor-opens-on-inter/index-v.wsix | Bin 0 -> 1728 bytes ...c0ca749a38d4a80d85ff8f71dbc55ebab102ee.wai | Bin 0 -> 2503 bytes .../floor-opens-on-inter/package.receipt.json | 1 + .../floor-renderer-settings/LISTING | 35 ++ .../floor-renderer-settings/index-v.wsix | Bin 0 -> 1728 bytes ...25bfe3b538d1d6d814e01d3cacde80c38de31d.wai | Bin 0 -> 2440 bytes .../package.receipt.json | 1 + .../floor-sequence-header/LISTING | 35 ++ .../floor-sequence-header/dash/classic.mpd | 24 ++ ...555a0b25e0fc20ce598006cf95e03338628f61.m4s | Bin 0 -> 2400 bytes .../hls/classic/floor-v.m3u8 | 18 + .../floor-sequence-header/index-v.wsix | Bin 0 -> 1728 bytes ...18dccba83a80a0b2d863a797ca9afb4c9ba259.wai | Bin 0 -> 2503 bytes .../package.receipt.json | 1 + .../package-mutations/floor-target/LISTING | 35 ++ .../floor-target/index-v.wsix | Bin 0 -> 1728 bytes ...c422dc0b288c29a1e54c19993546752bdef5a5.wai | Bin 0 -> 2500 bytes .../floor-target/package.receipt.json | 1 + .../index-capability/LISTING | 35 ++ .../index-capability/index-v.wsix | Bin 0 -> 1728 bytes .../index-capability/package.receipt.json | 1 + .../index-class-plan/LISTING | 35 ++ .../index-class-plan/dash/mobile.mpd | 23 ++ .../index-class-plan/hls/mobile/wai-v.m3u8 | 19 ++ .../index-class-plan/hls/mobile/wai.m3u8 | 7 + .../index-class-plan/index-v.wsix | Bin 0 -> 1728 bytes .../index-class-plan/package.receipt.json | 1 + .../index-class-plan/stages/mobile.json | 1 + .../index-class-records/LISTING | 38 +++ .../index-class-records/dash/legacy.mpd | 27 ++ .../hls/legacy/master.m3u8 | 6 + .../index-class-records/hls/legacy/wai-v.m3u8 | 17 + .../index-class-records/hls/legacy/wai.m3u8 | 7 + .../index-class-records/index-v.wsix | Bin 0 -> 1728 bytes .../index-class-records/package.receipt.json | 1 + .../index-class-records/stages/legacy.json | 1 + .../index-string-order/LISTING | 35 ++ .../index-string-order/index-v.wsix | Bin 0 -> 1728 bytes .../index-string-order/package.receipt.json | 1 + .../live-window-past-2-53/LISTING | 19 ++ ...4de9a15d8454aa660068cac6725c15003b589e.m4s | Bin 0 -> 2605 bytes ...7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s | Bin 0 -> 981 bytes ...441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s | Bin 0 -> 2400 bytes ...0b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s | Bin 0 -> 2059 bytes ...90ee056d378708523ece116698b152b58e88e8.m4s | Bin 0 -> 981 bytes .../live-window-past-2-53/index-v.wsix | Bin 0 -> 1728 bytes .../package.receipt.json | 1 + .../live-window-past-2-53/plan-v.txt | 5 + .../no-component-live/LISTING | 3 + .../no-component-live/package.receipt.json | 1 + .../package-mutations/no-component/LISTING | 3 + .../no-component/package.receipt.json | 1 + .../patent-pool-floor/LISTING | 35 ++ .../patent-pool-floor/index-v.wsix | Bin 0 -> 1728 bytes .../patent-pool-floor/package.receipt.json | 1 + .../patent-pool-floor/policy.json | 1 + .../patent-pool-floor/policy.toml | 46 +++ .../patent-pool-floor/stages/legacy.json | 1 + .../patent-pool-floor/stages/mobile.json | 1 + .../patent-pool-floor/stages/tv.json | 1 + .../receipt-duplicate-member/LISTING | 35 ++ .../package.receipt.json | 1 + .../package-mutations/receipt-newline/LISTING | 35 ++ .../receipt-newline/package.receipt.json | 1 + .../package-mutations/receipt-pretty/LISTING | 35 ++ .../receipt-pretty/package.receipt.json | 35 ++ .../package-mutations/sha256-file/LISTING | 36 ++ .../sha256-file/payload.sha256 | 1 + wai/wai-pack/src/golden.rs | 308 ++++++++++++++++++ wai/wai-pack/tests/review.rs | 262 ++++----------- 131 files changed, 2037 insertions(+), 224 deletions(-) create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/LISTING create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/hls/classic/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/policy.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/policy.toml create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/stages/classic.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic/stages/tv.json create mode 100644 wai/pack-conformance/package-mutations/component-medium/LISTING create mode 100644 wai/pack-conformance/package-mutations/component-medium/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/component-medium/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/component-medium/policy.json create mode 100644 wai/pack-conformance/package-mutations/component-medium/policy.toml create mode 100644 wai/pack-conformance/package-mutations/component-medium/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/component-medium/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/component-medium/stages/tv.json create mode 100644 wai/pack-conformance/package-mutations/dot-directory/.cache/blob create mode 100644 wai/pack-conformance/package-mutations/dot-file/.hidden create mode 100644 wai/pack-conformance/package-mutations/dot-object/objects/.x.wai create mode 100644 wai/pack-conformance/package-mutations/expected.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/dash/classic.mpd create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/floor-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/classic/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/legacy/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/mobile/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/hls/tv/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/policy.toml create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame-allowed/stages/tv.json create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/dash/classic.mpd create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/floor/v/8aa0cf43207b7a4e743456f00f8f1bebe3424054b97a2b09bdde13068ea8036b.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/floor-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/classic/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/legacy/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/mobile/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/hls/tv/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/objects/6b08ae3db73aef6c1bb8b2a714731ff239e0e6c861e2560be53aa536dfee1989.wai create mode 100644 wai/pack-conformance/package-mutations/floor-extra-key-frame/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-framing-header/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-framing-header/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-framing-header/objects/504d5519c3690a705d7e2cb4089ef6bf474311b8809bfe1a7c6a3f00628e8b4c.wai create mode 100644 wai/pack-conformance/package-mutations/floor-framing-header/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-object-corrupt/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-object-corrupt/objects/8f748dc64787b5c5ece081270c8559277f926322dd6ddc345306b8231f2d1752.wai create mode 100644 wai/pack-conformance/package-mutations/floor-object-missing/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/dash/classic.mpd create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/floor-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/4e22514b486de10123a49b511cc0ca749a38d4a80d85ff8f71dbc55ebab102ee.wai create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-renderer-settings/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-renderer-settings/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-renderer-settings/objects/ac1051ad64db4dc790f34d5a2525bfe3b538d1d6d814e01d3cacde80c38de31d.wai create mode 100644 wai/pack-conformance/package-mutations/floor-renderer-settings/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/dash/classic.mpd create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/floor/v/b4afa19b0f5c520614c95ce53c555a0b25e0fc20ce598006cf95e03338628f61.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/hls/classic/floor-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/objects/092d88d17b4fd8de5ed3d820c618dccba83a80a0b2d863a797ca9afb4c9ba259.wai create mode 100644 wai/pack-conformance/package-mutations/floor-sequence-header/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/floor-target/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-target/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/floor-target/objects/c252c46801c1e52aaedfe1d4aec422dc0b288c29a1e54c19993546752bdef5a5.wai create mode 100644 wai/pack-conformance/package-mutations/floor-target/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/index-capability/LISTING create mode 100644 wai/pack-conformance/package-mutations/index-capability/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/index-capability/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/LISTING create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/dash/mobile.mpd create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/hls/mobile/wai.m3u8 create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/index-class-plan/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/index-class-records/LISTING create mode 100644 wai/pack-conformance/package-mutations/index-class-records/dash/legacy.mpd create mode 100644 wai/pack-conformance/package-mutations/index-class-records/hls/legacy/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai-v.m3u8 create mode 100644 wai/pack-conformance/package-mutations/index-class-records/hls/legacy/wai.m3u8 create mode 100644 wai/pack-conformance/package-mutations/index-class-records/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/index-class-records/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/index-class-records/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/index-string-order/LISTING create mode 100644 wai/pack-conformance/package-mutations/index-string-order/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/index-string-order/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/LISTING create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/033b524528e8b2b3c131680a854de9a15d8454aa660068cac6725c15003b589e.m4s create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/1c0d8fdbc265a9eee36dcdef6d7dc0203cd1736d1a32275d58d83e9bc4441a2c.m4s create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/a68544b402e80f271c8f7c7f03441e27f01b6fb2fa7cda83264c6143f8a5229a.m4s create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/d3b10cc7f2036337ecddd5fcc80b48b7ce313c0dc5c20d509d0076fb67a14d60.m4s create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/floor/v/e20df97a947c57a9aab52e0c1e90ee056d378708523ece116698b152b58e88e8.m4s create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/live-window-past-2-53/plan-v.txt create mode 100644 wai/pack-conformance/package-mutations/no-component-live/LISTING create mode 100644 wai/pack-conformance/package-mutations/no-component-live/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/no-component/LISTING create mode 100644 wai/pack-conformance/package-mutations/no-component/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/LISTING create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/policy.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/policy.toml create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor/stages/tv.json create mode 100644 wai/pack-conformance/package-mutations/receipt-duplicate-member/LISTING create mode 100644 wai/pack-conformance/package-mutations/receipt-duplicate-member/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/receipt-newline/LISTING create mode 100644 wai/pack-conformance/package-mutations/receipt-newline/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/receipt-pretty/LISTING create mode 100644 wai/pack-conformance/package-mutations/receipt-pretty/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/sha256-file/LISTING create mode 100644 wai/pack-conformance/package-mutations/sha256-file/payload.sha256 diff --git a/wai/pack-conformance/README.md b/wai/pack-conformance/README.md index 5b88d400c..653f7380e 100644 --- a/wai/pack-conformance/README.md +++ b/wai/pack-conformance/README.md @@ -48,6 +48,18 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi 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 and its reason): a receipt over no + component, an index that is not its policy's, the policy's own refusals, 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 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 @@ -74,7 +86,8 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. digests of each equal pair to be equal, decodes every encode payload through `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 its reason. `cargo test` + (`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 and, for Ed25519, `cryptography` (TOML through `tomllib`, so 3.11 or @@ -92,7 +105,8 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion. `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, and checks the version-1 receipts. + 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. diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index 9a3274e33..27476e6fa 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -50,7 +50,11 @@ where a signature is checked, and implements the rest: 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. + 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 @@ -59,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__)) @@ -2395,11 +2402,12 @@ def clip_digest(frames): return hashlib.sha256(b"".join(hashlib.sha256(f).digest() for f in frames)).digest() -def verify_pack(rep, d, name): - """packs//: package/ verified in §11.3's order, without the source; then what - only the source decides (the plan, each base's sse and step) and the supplied floor.""" +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 = os.path.join(root, "package") + pkg = pkg or os.path.join(root, "package") tag = f"packs/{name}" files = tree(pkg) if files is None: @@ -2435,7 +2443,8 @@ def verify_pack(rep, d, name): # The policy: as given, its effective canonical JSON, its digest (§3.2, §11.1). toml_b, pj = get("policy.toml"), get("policy.json") - rep.check(toml_b == load(os.path.join(root, "policy.toml")), f"{tag}: policy.toml is the policy as given") + 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)") try: @@ -2489,7 +2498,7 @@ def verify_pack(rep, d, name): 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) + 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})") @@ -2513,6 +2522,63 @@ def verify_pack(rep, d, name): 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, with its + overlay, 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 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) + got = package_verdict(d, pm["pack"], pkg) + 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.""" @@ -2552,8 +2618,9 @@ def index_against_policy(P, comp, c, ix): return None -def verify_component(rep, tag, root, pkg, files, P, j, c, ix): - """§11.3 per component, then the source's side. Returns the files it names.""" +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: @@ -2571,8 +2638,11 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): 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)") - rep.check(ix["first_piece"] == 0 and ix["first_entry"] == 0 and not any(ix["prev"]), - f"{t}: on-demand content, one window (§6.1)") + 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). @@ -2588,7 +2658,7 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): 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}: {x.code}") + 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 @@ -2685,13 +2755,20 @@ def verify_component(rep, tag, root, pkg, files, P, j, c, ix): f"{t}: {path} is the one the floor's temporal units give (floor-cmaf-mismatch)") # The catalogs (§8.3–§8.8). - cats = render_catalogs(P, ix, c["id"], (av1_codecs(seq), seq["max_w"], seq["max_h"], frags), size, - j["policy_digest"]) + 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)") - verify_sources(rep, t, root, P, comp, ix, decoded, units_of) + if inputs: + verify_sources(rep, t, root, P, comp, ix, decoded, units_of) return named @@ -2984,6 +3061,7 @@ def verify(d): 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/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 0000000000000000000000000000000000000000..0092ad47263987da89929ade4c9c38b9824bf07e GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j+}r%uXWlvAc|U*qvsO+r`K!ajy{GlUmX7)x zEiv2A1YZOyAqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurft;^*`5FzN5|Thf`X8-F-03NX0_I!piiqWkJ`mh#(s*Cx@1})f`%-7Uz-5>XRDucY zDugPSILQ4>V9W&a1{{P!Sw=vb1&D$AQRLg9@*w>nKQl3)>R$kr2YHkOERU*RrA*4% zXW}vSk5YS+Qq3jPwz3u{Xesbs-_7#iCP$}H?xfjJ1EAD@Ab_|7BJho;NK0^vC2Pyn zciuB6&a=HDwLWWKw&kwd$2YuuxiEZd#I**n!fjCf7iK{jP|Ec9l3?ZS>t9wLRQ}Zw zZ+a&5*Iq5jkkVPNpX`j?Q)VU~0g6l{0J5JI6jC5Z5CK5yLEthp)G9%NfCLhcAKrdu zHTzq}`_a5L3shKV>=e2E>D0cEs0EBK?g?E^y{86I3=4FSS;+qX?>x=9^x)p0@(Dso zkAzL9rmT}?I}!78%}N86I*nj`oe51+U;|+Omw}oGb3)Cb9V%-RSy%r#^{8O7B(vbQ zMJ63iPYW}=nWPkr9d~+Ng8CELeoFlR8I;J7z@c-J?5a^LG8SEb+0I;GcTUcDafv1U z?;9<-*SlLbSbx&$fhdM)L-#+|!AWl=FWjCbrgeW6@A0(E&y!yEY+iW%QJ%$ zX+7L5H(e~EuG;=I+`RQ&1lRyr{J%&-b#$4Go2OGvU}cte=G2Vx@YGG5?;0+B`6Khe zp}8krWtHwaXnsYupOW}Lj}~sQdG%qxk{%`eH!&*McBO)N76SpxvG!<5nh literal 0 HcmV?d00001 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..1754ad80a --- /dev/null +++ b/wai/pack-conformance/package-mutations/expected.json @@ -0,0 +1,246 @@ +{ + "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-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": "patent-pool-floor", + "reason": "license-refused:wai.video.avc:patent-pool", + "remove": [] + }, + { + "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": "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": [] + } + ] +} 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 0000000000000000000000000000000000000000..4d172a01862f09d598a03238136ed628e4ec0cfd GIT binary patch literal 2929 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7k$W>SG@1F;!I7$KTKq!JPvSzH+*j-po@A%+HaH>ABk6Dc*=7*y-jN7+qjeY#ilw{jUnp0&b{CF zqL7b6%3rb4e7VWGnK={f8freXEikQqkn1HPwV_S4RYK)^j;yr&wk1>i?Kl5XVEyo5 z#j4|BaI#w-0g&NKDvn^)LOQ&VvQt9#Xf?`Ej6M#2~SwIz#VeHb3jxXY}oyckdMAwAh^d zug6To^QRT@{bqc~zhmFrKU>B7Dl)fSX?-5ZK3n?_+oY+Re_T&gj8Jyuvu>Ty(`hU@ z$?(H(%d4;KjZ#@x`gsd}tC>{wY~PBxeET{UdH!NL(ODGLrWo8~vD-6KG)?v2VS_YF z-)#)HBxKiCJoPaWGGIQ?z`($E0hl&`bOXbTW+Mdy7Dg6iu{$w^*@`>(mA|VT$ zCjivN#>CNpq^qmJNYQ|Ug;xMc?82IB3mLerq+OOIX*aykUbSO1L5?O!f(cU6fJZ>g zpaYR0PwBq)D1KY(aKK2=fKNf{z(s^qnUSP{0K@!1DTL7REen6ATw7~10jOGtk^NyZ zLMq2d)@@gO(j-IwpWfEtvT*q}dmPP+{Tux&kcJ4a&CnB)4CdF_hS` z+0^a=P*#mYaOwwyhNG$W&Kl<*u6zA&qN0(wfrfyXNd-doZrV1V2P`0mX(>n@If0M} z0IJntm>(*E5V|w-OI~3LNUa{Kn*xkP4GiFJ0v9E>ljfu(096{HIwrtK!oUR81vk&+ zq$Izb05aAL)d>ke#TKYecn~n3F+gyh15mLQsv8V|ifvHcu=80{c$k|12B2a)RA(d@ zi5NIe00qPWc-beArU6vwq#$M!fbeo1Fcr8k%sX=dA+)*KBb>))%b|05yMXH5nAqci0O6gP-KwR4N){=LNs zpiquNwN$}K&L9SE9XOS5YSLbzs5v`r`O~@&$0C4Q;^20JQvxVR5))8D_4L7yVYA-m PgWR6PFwK4fBK(>FC17G& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a5aa0f86dc4020ba615fbbab88fa92a3653b72f6 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!jd?BV>OrXoAdUbU9gOAzwX2wN+Uo~OrY|V+v zmo4YN*%%B|LK28b|D*MYJ3Ylvzt9wLRQ}Zw zZ+a&5*Iq5jkkVPNpX`j?Q)VU~0g6l{0J5JI6jC5Z5CK5yLEthp)G9%NfCLhcAKrdu zHTzq}`_a5L3shKV>=e2E>D0cEs0EBK?g?E^y{86I3=4FSS;+qX?>x=9^x)p0@(Dso zkAzL9rmT}?I}!78%}N86I*nj`oe51+U;|+Omw}oGb3)Cb9V%-RSy%r#^{8O7B(vbQ zMJ63iPYW}=nWPkr9d~+Ng8CELeoFlR8I;J7z@c-J?5a^LG8SEb+0I;GcTUcDafv1U z?;9<-*SlLbSbx&$fhdM)L-#+|!AWl=FWjCbrgeW6@A0(E&y!yEY+iW%QJ%iHi^7A4tY+@r(zq%aVhJp}fy_cq|E)n{&9^U$yVp+h@bUPo6Iiy; za9R&H%S{)HsH?U=4L5In7XdZ^7XL4jP#s++9m>5dR*cfv2 vlQMHs*%)$C(-V^`MHtExGxf?cQ&RKwGV@B}bMs3w^YeI-L=(#lLDm2O?O>E> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c194a74dbad0bafa26c8cdc4137cc6afec18d16c GIT binary patch literal 3032 zcmeH||5sCW7{~7rWg_9`5o7~$yCRvJ^x2nt_eB#7BT$oBsiYCyFP|$r!6svZir|tU z1uCQ@%up;d@{p-05v74&!jz2C?2BR$QD#X{p_yWR#ya{WIb%o>j8;|#TuqV1V>_ZpOqEMtZ(VyIjlUZBMpr-QY-?M{c?u>!Z|<4@Qf zEXw;k%0ibt7qV%b%bDekW;xw<4uucW)(h~#)j5@|(Hd7o6NQPknFc}S`fiy!i zs|QBX#IcakQ#uG~nl>A0V4ykuFe|c3`xiLWU7EL+(}6Nw(1AFh3)Rh8wGwp;**KCW zAZtP9CXzy6*%0i1vU;fBd z`b*#B-s!Pd9t4paG_bjPePgk0;o|56mz$au)`9(o_-86lovDwX*R#C5MypfoNNWG1 zuZ${BEJ~T#cm7)6%1!TU*6y!AF|N{a^sY5itcjinuD6Gz7QT4sweCybt~WOny?d`@ zx12aJkoaEz9{TAo@^X%+;aG3b*%c+qtydra(3wMM*FO2kUCn2HK5{ubSgK4`uQ9!D z+`Bb%^K8$$U&quE`CVD_CMx%pgcOHGUC)dRpWL|Xh2+?VTdL9hU3Gife#w{`JZCp^vkpS$YjWu@s_&nsO;hd&#N*TkLe zS+p>t3bqUemQEQxRFjwb!qfM!UsRC##q|E>N%iMcrO(eCl`EkRDQ1wCg{8Ivpm>{eH9k@=IY)Bp=d=W zVEPGQREr10#&n7&K<$9Fz(GQ6}8SgO0PwE-r zQ0kfLb5H}R{~fXsk&gQFJF#)r@VUEajpwpvPt*VF`v1EAw{?y5t&{=;$wI+KVO>wn z_%Si(YC&8}h%Z47f>p|vPlSYAUMa(NEK&-Awu3uwvghBNRlzGKNCLVUAt6)kvy222 z{iFl+zJLckCCB6DD~4+8qHE#kK9X05`rjcR6x11$ed=%!CY9F~3W3gznpamTs^ZN* zd3dc7HT9g3k`e9G;vj-=rp#NpGij98;X1vRx_j)zNYBOV9>L;Had#;ypG5>vw~D&D zkPv^5q;;I6^M}4tzCP|3(3>vQFX6>e{y~Fv!+|pI%i)sZ?XwZOu%-rLb_bsmEtPNS z7sMxEZ2Yv3yZ8Qhd$T&+2QdjulZ6^r3pt%!t{Jy%^XQ;ZubHqnPr`_Dq)fp+YD}iZRo9 zp;?UCW!oXK$+ySx6|G`Uzzb)IIp6ti!_ph_9ISCf9@wC% vQ{TOfd%d@?|AQ31?BhI{$Gell(NgK6d7W+@Sn|?Ur_d{nRmD~aGuiMLth(!4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4d172a01862f09d598a03238136ed628e4ec0cfd GIT binary patch literal 2929 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7k$W>SG@1F;!I7$KTKq!JPvSzH+*j-po@A%+HaH>ABk6Dc*=7*y-jN7+qjeY#ilw{jUnp0&b{CF zqL7b6%3rb4e7VWGnK={f8freXEikQqkn1HPwV_S4RYK)^j;yr&wk1>i?Kl5XVEyo5 z#j4|BaI#w-0g&NKDvn^)LOQ&VvQt9#Xf?`Ej6M#2~SwIz#VeHb3jxXY}oyckdMAwAh^d zug6To^QRT@{bqc~zhmFrKU>B7Dl)fSX?-5ZK3n?_+oY+Re_T&gj8Jyuvu>Ty(`hU@ z$?(H(%d4;KjZ#@x`gsd}tC>{wY~PBxeET{UdH!NL(ODGLrWo8~vD-6KG)?v2VS_YF z-)#)HBxKiCJoPaWGGIQ?z`($E0hl&`bOXbTW+Mdy7Dg6iu{$w^*@`>(mA|VT$ zCjivN#>CNpq^qmJNYQ|Ug;xMc?82IB3mLerq+OOIX*aykUbSO1L5?O!f(cU6fJZ>g zpaYR0PwBq)D1KY(aKK2=fKNf{z(s^qnUSP{0K@!1DTL7REen6ATw7~10jOGtk^NyZ zLMq2d)@@gO(j-IwpWfEtvT*q}dmPP+{Tux&kcJ4a&CnB)4CdF_hS` z+0^a=P*#mYaOwwyhNG$W&Kl<*u6zA&qN0(wfrfyXNd-doZrV1V2P`0mX(>n@If0M} z0IJntm>(*E5V|w-OI~3LNUa{Kn*xkP4GiFJ0v9E>ljfu(096{HIwrtK!oUR81vk&+ zq$Izb05aAL)d>ke#TKYecn~n3F+gyh15mLQsv8V|ifvHcu=80{c$k|12B2a)RA(d@ zi5NIe00qPWc-beArU6vwq#$M!fbeo1Fcr8k%sX=dA+)*KBb>))%b|05yMXH5nAqci0O6gP-KwR4N){=LNs zpiquNwN$}K&L9SE9XOS5YSLbzs5v`r`O~@&$0C4Q;^20JQvxVR5))8D_4L7yVYA-m PgWR6PFwK4fBK(>FC17G& literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..72565ab5084acdc7d499d573076206390e72a20f GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgpvnvCek8)%zUj9h;Vm6w80IeDLf<;-fI`r&deN?!S}lyupoW zKPCSE$%$$|vGEToOF#hbe`tWvEB=w~2m2qC*@*xk_ksW{{iDZ!VjoxMzMi_!Yzt_XgKxQGQ|JER}=G&LW-D{_L_;~!)2`pP^ zIIV}9<)({8)K%M`hMTv(ivSw{i~ko%sE#g^ar1Pl39QW0&YYT29-g|1^IgNGFMnh{ zI5hX9tE|#p2hFd@_EQr7=h4Cqmaky00!3>Ij02>B;f}5Zmi uNtrpRYz#T6>50jeA`In;nR;cJDXIB-nRzAgx%nlT`FXrZqKRdOAZq|wG?Uo? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c194a74dbad0bafa26c8cdc4137cc6afec18d16c GIT binary patch literal 3032 zcmeH||5sCW7{~7rWg_9`5o7~$yCRvJ^x2nt_eB#7BT$oBsiYCyFP|$r!6svZir|tU z1uCQ@%up;d@{p-05v74&!jz2C?2BR$QD#X{p_yWR#ya{WIb%o>j8;|#TuqV1V>_ZpOqEMtZ(VyIjlUZBMpr-QY-?M{c?u>!Z|<4@Qf zEXw;k%0ibt7qV%b%bDekW;xw<4uucW)(h~#)j5@|(Hd7o6NQPknFc}S`fiy!i zs|QBX#IcakQ#uG~nl>A0V4ykuFe|c3`xiLWU7EL+(}6Nw(1AFh3)Rh8wGwp;**KCW zAZtP9CXzy6*%0i1vU;fBd z`b*#B-s!Pd9t4paG_bjPePgk0;o|56mz$au)`9(o_-86lovDwX*R#C5MypfoNNWG1 zuZ${BEJ~T#cm7)6%1!TU*6y!AF|N{a^sY5itcjinuD6Gz7QT4sweCybt~WOny?d`@ zx12aJkoaEz9{TAo@^X%+;aG3b*%c+qtydra(3wMM*FO2kUCn2HK5{ubSgK4`uQ9!D z+`Bb%^K8$$U&quE`CVD_CMx%pgcOHGUC)dRpWL|Xh2+?VTdL9hU3Gife#w{`JZCp^vkpS$YjWu@s_&nsO;hd&#N*TkLe zS+p>t3bqUemQEQxRFjwb!qfM!UsRC##q|E>N%iMcrO(eCl`EkRDQ1wCg{8Ivpm>{eH9k@=IY)Bp=d=W zVEPGQREr10#&n7&K<$9Fz(GQ6}8SgO0PwE-r zQ0kfLb5H}R{~fXsk&gQFJF#)r@VUEajpwpvPt*VF`v1EAw{?y5t&{=;$wI+KVO>wn z_%Si(YC&8}h%Z47f>p|vPlSYAUMa(NEK&-Awu3uwvghBNRlzGKNCLVUAt6)kvy222 z{iFl+zJLckCCB6DD~4+8qHE#kK9X05`rjcR6x11$ed=%!CY9F~3W3gznpamTs^ZN* zd3dc7HT9g3k`e9G;vj-=rp#NpGij98;X1vRx_j)zNYBOV9>L;Had#;ypG5>vw~D&D zkPv^5q;;I6^M}4tzCP|3(3>vQFX6>e{y~Fv!+|pI%i)sZ?XwZOu%-rLb_bsmEtPNS z7sMxEZ2Yv3yZ8Qhd$T&+2QdjulZ6^r3pt%!t{Jy%^XQ;ZubHqnPr`_Dq)fp+YD}iZRo9 zp;?UCW!oXK$+ySx6|G`Uzzb)IIp6ti!_ph_9ISCf9@wC% vQ{TOfd%d@?|AQ31?BhI{$Gell(NgK6d7W+@Sn|?Ur_d{nRmD~aGuiMLth(!4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4802a6aec003996a1e77cce33ca99f8162b3dfeb GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIglCP8!CA?ldB-MPG<|pyl?y6odtI^%>E};lV#74)Yt8EoDXfA zXzoc@S*5!UnqQIarzHN*qlFtRU%^}jiq;eu2S@|M9bE}95r7~V042+q7)r|67;^KI uGILVd7;;k66O$`N7|Ihf^~y3+QuFmP^Gf1#^Gh=G^LUX&6Uz)i)&Kws!jP%} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ab0a5c2ee552f0f26114bbd020eb7d2389d3ccb GIT binary patch literal 2503 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 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s b/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s new file mode 100644 index 0000000000000000000000000000000000000000..cfa347e0b86d023cfea1db3635fbbe144b07ae4b GIT binary patch literal 2400 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7kamJy;5L@FV%MUdFaNNiywwlor(7;$+V>Sb|=tKblqBSIWm zjU*2BxWvVAsF$LWxEKy|aD@Xddy&Ij0?ABV{=#KGF7*mXW+K~#!#zwYkR-^qAvYzl z#7M}1`9K2$1KR~)Y6H>@3^SUI6bx7xS&+r<#1v*L?%-Gct|AqA()67GP!}5$M+1_s zt_CAT0}d8m0VJ^tYpyM1;I@)>S(2pP@Irgl4kJMWE)Ky34hYNYjU)|t1jGzF5JIPP zUwah4t#vp6RL!R#b>JdGstl-FfMI^16hi3umW97luC2A108}l+$o?=HA(dk!YaqhJ z@o^1Ar~&A`BM#n@e;2G!Tae27&J3tSjD=V5D?&$xk+gvX(1CRbp}QU0i%TV^Ia|DG z0IHV)I<+4mnPH@4AOm+|H^_#&p3FLVMW?G0H919O8}58>0BMs$b#sJ~jDg}o6t~`X zbJk}($jx~7<-0DR1|=r;rwIsq+JG8VSa`m!01I`4V(Fgb_RBJc5<51V+Fbz3s&NQT z{eaMLG}Yc&`U-1*sz^5E21EwK@#*LnRPG zcV>RcD@*~Y)kAetfRU(y0o+aClyf_2PD%n$r4g!Q0*oXKOi*2L^Gr@k^2-SzW6e;V zkN{L{f$D?@0rMFH1m`&b6FKpcQ4 zb%8Vuph71FF`EE{m+OG3z=dJnnF|P^&CMR+JU&|voy*$=RPV;bepdh?sQ`3^2Mf<% zBqI)LF*umEJ`Dvb_Tmtni)4}lP{;?>J_RE&13y&zHofIfT;c$XjQ~`u*5B$CG&2VU zTM(*+3P3l9z-{UVW#whD$#opoCsN#wPY0?GL$_1hAOh9SEt3297AJs0ISSQM1tU3w z7`S!dRKBT6dxfIr?6l=i>pmQd0BVVY+YL?$pdd+1Knc~;2S0|*dYcb&dlJJm`w58f zYvST!0LIArkF(N?XPRW@Up?8g7mWt?mBFL-r}wRX)p zTh#UGo%~ZR`&YgOvQ65lyJpmHYYp%VRonDp$1X{$j~k7hdncVbx5nA)adgjI9Ye|a zo@ai&>rU+PXpXRcckSD|g3kTgRU6kF<(`yxNWD_o%A>*k`x(*Ly6Ib&KD_s&-ePm} zZd=A>f~H45dhGeIDyjWOdGV2WC5ERwm($y%X1Ib=AB2pXLL|Y|PzURnF%WqpU#ovDO9|hJA4_2%?{wYJ1 zPs8PhKv2!;Jrl2gJ#KL=!1(hj<89H0Tyy8{ZCRIeC3n~11gXTs$+{OFFL)@O_&ECb zhPhD|ZiTTj6T7)Q@0wmLe{}mGmw?2C?N{~b0+v-I7@a7#jV IZN*a`03!cfQ~&?~ literal 0 HcmV?d00001 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..fa6e0620d --- /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/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.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/index-v.wsix b/wai/pack-conformance/package-mutations/floor-opens-on-inter/index-v.wsix new file mode 100644 index 0000000000000000000000000000000000000000..48386d6bb9a4bb5f75443554bd7e2a0f6f73f496 GIT binary patch literal 1728 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

57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgq(VAaHHUZQtV)KKn+gs_uWh)#Bo{8zK*6ZPwgtINbYKwu2MX zescUj1sZ@a*ir2#HvU0n2?)Ub4-F7{#XqwBVE=nNu^$!&5hLzH7Mj z<&VqR{~4~AP5FP$ucH} zk}@`i-29}>oK!Z3oYeHh044!6SrL&$x=Bcgfl4e&!3P8@NJWHAvXP8=NQj^!kU~KP zv`7{4fuOaD4&nnvL`Cq0BDMIoDi%ty_(ISEDq_1Kon5vw{i~U|limH!ch2wJdv+Ji z4CQSh0YEC40;@p@$n$|fE~rJ6Y8bssRx1%bdau?Q5uFizU_e%A)C$;$n1l36L=$H~ z5?87Xh;~>{1r_iLSgzKnjjPdUG^NjQ3LiL`2c~gBgSRP2r!wfZW>u>@-5IyBON=ui&CV8+s|HYT8? zQXcv|2{9Vc>m>9!^gn;*oiM_NWeB>Xsi4XLYY{Z95aNOfphO_zf+?Uxl|*-$ckFG; zI(K&{h$Fq-5fPS9gw*%h=1#I6DoKgmvTk-pL|W_v@1EN66?a&<-}{eBCc!5e(?rw5 zUk)YjSuD!^^?9ntt?(T=>95~MaTvk3hAMe;_XoVsuqobjdiRqiQc-UC!u)4azusDbPRlfL4sM=XI`O`&6HGO_H@R06)cva^- z7j#4%0lp(Ns@?Qrz6RxU4aK^*p`~^ zkdqaqx56Tr&`r4{oy!<1EB7ozth+^++l4%q)`zYrf@YFF*?xx2BCOpLiVt3IjOA0rL-9YH- zQE<>_qy{@u>$C1$tUhRE(;1J~&%w+dWc$AIVIj{v3r`rQZoBd5HC4hHEm({vg|@W^ zH(y2qgS>ujuYB=oGuzbyF&^@wy7;cbV_H?!^V-Nbyf=$^=_)SEFdr8vjilqkZYfq#Lga)nV*ru!^wZ2mQJ&#PG2;v(YCPS7>IBU!FmGr*_L5RnUYO|% zmP>#P-?VgW5@!Lsn_F<9Dl24>{k#(wZs=+;Z?Kh3D-{o6VWc5s`=Jp@8@vIjFuOAn zGtQyVN=D>nVZuBj4U1`M!37E<(j0#^4&EolPF_X?Ir4BLP2`6k#R4J@3+r(q#*t_K zfbEY}q*MH_DuWxhVMI~?BEo7$6NyeR&Yw$)$4_xF5s3v_I+*}1!C;2p(&L$&a`x1n rsso+rJy)W!vP&^ClX)ibt%c(W=XJfIH)_W#J>IJvC<-jV&m{A2=he+a literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c587b338037d62df3299e213fd9a8b985fb42dcd GIT binary patch literal 2400 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7k$W>SG@1F;!I7$KTKq!JPvSzH-OTo{Qhjl?EKTpovdSsdaj zIK<_M5Jy%ci9hQl0O;eg9t^&G831-aG8%wy#kV%$aZ0I58H;^ zl*AG)E?_ERTK{o&yaA`tR2Bh-*+4Eg1JhkSsZ(8N8l=i*ER)^%Ke~)_P5uS1Zn4&` zIcJNyKE0EFs%8Jm*Fd&OJ9XEL`faTNexYicUhLQ(Yn!p43}x zZr*LnxJ=OW=tqw|A66x`-zYCW60gMYl;?7Ko7Bv=aVPVOO?9doL)3Sjd%y2RAs>g7 zzhb5Na+7s4b0*p~)O==JU|Rhk*GoidLz`%;gv$3ES!wxgOQ!hSZ~mje`r*NfRmVSN zsPbvJ{16DLIlX7%^{>Y*t_2u>US+&3`jBhx+`TR9lCI?LI-DStcsN=2!s7)Gr4t`V zAKx%H%EGNMR%T*1m*-v6i{+1QALJ5{n6TaIU;0Cx2MfMEUYQ!&<5K$jg_QZX$BP~` z@4fohS=-_A<3PWRX{iUka&;-Q{+(MMJEh&VKCm|ShSrB&;%lxbbWOHqE@MpGDyeH` z>Hp?$*}^EZuKTa5RWA9@YO8zyGfZ33?b6?MhTh9;e%7SH8izH-NOsp}IN3NX9_% zAc|XWyE*GK9^_`c`|@2EP=gW^`_lx3J#9b@Dl9x-SAd1OL9ujCa{FZ&Lx~-mP3J= z!h?n9FOm@lwHO>sTc3sk6?<_A&P6gw0Vw2yYM+9Un1LUveVg9$CoXXS#zp|DRqJo{ z3YwXNf-MNuLIt3kL*O=bgR=6n*yK76>k}z%$EO3;hoRdkZV-WL=N8HRdy5l5p&W&3 zse+N5K@8kFa4O%_q`g8>b9UPDr*$8WMF6$L!R-d81W=G9CZL4s>4P7`X1&b^xjl(t Ln*9Vs_%#6l?m1jl literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..928f4e521002846a15fd70db01aa8e114dfc4d2d GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgrVz+i|hl|Hi$z%QqB`N!&TT!m44xrW?u2r=Obj+h_Ko$m5)t z_EX~j7wo9^6C3}avIGR+{)Yw#z2YC)ez5;RnVkp#axVzL(m#6qC-!l5?(3-w&9-1x z+a!7D+wVV7JUh!nUKAcYWHoc|mc~`d5KCa$4P+K_`fm*qYrcJ1+`V?1hmXf!oxrk% zhSPeuS#G*mL|wK0X}Edoy9lrWu=szGgzD%r88=U-n!w5|?aZke<>9HDINvo~`tnES zgF|yqy2>iubDp0hhz&Jn}81CpwfQbME!2l>(#>7xk#>SAF vpOl%C%EpkBnx2?kDZ)^mn5kEmnUb2Xmzh@*pPOHjnV-jtB$`-e2(ktMjeC`x literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c1e84c060a4f7d22d56ed3fc84da209b38387226 GIT binary patch literal 2503 zcmZ{keN+_H8^>o61(9&qBgih0yCaghE8TsW*%wXLFakAME0r{YdATEdf?ZiwP!ZT9 zNP!9|2{RPSj67s2NBO0#|`1`$P;1Ql8-)?KzU!*ou6%-Nm!KHu;2dG5V4 zD;FebFGw&fAJ3!gxCPh70$hVeJQC1X{_8)L)x-@Survp{GpaXG07wVg{ZY`P?vR0BNAZte9 zMx&NCqkd*U8?-=+dQ&7va*#369N;J&MYCEhNl+}SheiU>fHsjVMWfgp=ys!_b5JbJ z(nf;jAiS>fgQ94?0U`^6f7tR z{q=iiXlELjsN3!@#df0GNtj<(tm150hewfHvoGqGGJv{mm_M%g`Zx{Bu=j&o{&`>Rnw?tI^7LCw2TeP)=1O z7N^V|xO8J+?bZ*~8xJ*{o>b*HanFVsmPF4(H# zHs7=9*9mn*es|WQsfvT8p(WwbH#4ImrZ??H zKG^ofaJ%V}P5xrOxSGOOmU1dthPL14F z9+cEYe46*o<+dPcc;&m6J8OD0-E}v5--6347OuP1uQ1-|eXYCr*ykhhYU_pGWlJ-v zVe4>E*^KccwRx#8J^NtMvVznvXAibaYq+Q^dvWf#TnTklK8G|X|1^@jXBAoY^|k!y z&yrs$E&Tb96>53HXCnu+i}%g@qbQ)}`S*5RJu9hB&2$cbM67gHyFzYBdL}m?uf2VA z+Vkt}O&3ePUly=aeS1RH_Qv18pj0U{7KF?#sj1kimse>AZ<#;87N>*b-&(p@c4Jf3 z#_x{T*M=P5@>BN{{xLBiz12|1RkmrL*DE9&C~&V&Ps|?dezVZw~bp%0N({tj;Px6h-+2IS7`9fh~e?cFvE9 zIoAuUt-Lctsc8LFD9GiVQf&7!g&?#a*>j7%^!B_;-Z@1Q(8CA?nJQn;C@|G8aHzp2 zc+jGpvM!d7)YZq-!SMqm?+)|7OExUHGp7685g;6mQYZ*rThy5=fy!JKTNr|b63(hjl*?rBX#e@sZpLQH$6hc-Qwv|l)i{aplTCMbvYsaK1tgo zN!JepWqg0!FQ6}7=wHg4qy2-2>qmuh?^(DifA>P9HoUo!nBU3Q#01K=4+`efFfM-9 z|9TGmcxRg`!Ur)8%#wx~)(bUVT+@cqgdNS`Yl7{rwS@5p4CA1Z}*q!=@uH=4wl-L~Bl zn{1br?`RQo0^T@J%=zATn^&%gNj}IM=ZjI(!+oez!GcQfg0}K6sg#?=n;uYx&9e!M zR>+^kh1jl+PC;laPFxwh08bl7X`{{ z$7CycVTqVVsql>u29}Cx4qu;4?X~hdZxe&O^VM@Q(kN`jBVrsSKjULON4fU{wl9?_ z44iw0O=y3aCtC3@BEDyenCOW7!(%z=!Y-~7BPns;BzJ=+c$m?*^zdeNz3Seb+?#!c ogCC{vZJ*@HJl-=YEG?BT+OKnMLn~gn?i6OFvFf-=VNW*y4bYk8RsaA1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b7971efd4e2beec89460f864397a62d22ee33581 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgoiM=tu_R!KYg5?mxV;?ugPIZjBzzg-?AXXPUZ|YTx_1^av-W z{p9%n0yF?$u%p^fZ2W`D5)gp<9~vO^ihpGL!Ttwjb|L`Cy&wQf|LF0b*vHkmuct0F z+k#nbljNaqzyCz>>?{v?QF!o>)y%zH8doJlEP-V=kXgv-zcom#`SxXT_u6S5J|2H{ z0?QT}PV3=jx#?mNb=CH#;pVOHBESZ~;{Qbws-w$f+&rCX0xPq$GpA;hho^4heAjU4 z%O9B!4$VF3DywwYLGvrJ{p7?y%>U=n!VQ+MV6Fm1YYL15q=Dg%t^}9}KoAUol4VQ` zC1q?3x%o+%IjL+6IjQN1$(14u<%yYkWtl0d`FfdoCGolWC7Jnoyhx&nWriSY0BvlP AEdT%j literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a34bdb22eae732974e6622bb03d8bda3e2ef52b5 GIT binary patch literal 2500 zcmZ{keN5seZ?)&?Gf1ls;Johe} z9j|ECpA!9<} z27`t&p?)Sn=`}!udXof8u#h%VEMQ43NiiA?fs+iQg9aQ>fHD#cNuk(G=yal?Gf^zX zPzIc0A}-Pnb9Rf7usel+zZ3TVZOfTr`HGl zJCcD|yVYBQ?LfDqF|YPW`Pt%DmprR-U+AH~S7bd>Yj2xZ5?ENg^9x7uo!*Jv)1q%K z1fd($u(2|~zRE9Ga;*T&52T2WdRp^@#5Z~d#cge;9KNSx7o z>2B|;&7Z2*9jZM&y4-eR%Gya$ajvKCwFV^Ry?OMV&TBvA8|w-_enPZQil6R}`=oC_ zweh>`%u~rKtlRIx$|Cu;TQ7WW&%`6vz4Yvq#`Cw2_hk5s-WBq5MB2`F|x0tW`E0_)XDx+=lmggdi}XiwtPF# zV!UM0{kC6Mzv9H)^r~YoAD}L#H=az9lPBrvZC%x!!DQEpmV;Hxjj?;W>`!MR^%Fc+6>bqU1Lg*T+?^o^v%s^EgbpYqWRLh>&w?&KVDPi ze|*zh@qc-p+3Z+eq#@s0q3^HhUJ!_m$T&`F z*NvXp}r&Xy2rMd$p`0#`vp?^UBdsukj_R;U2FF zgmA-@xI4GUkBG22&aERKdt`j5>*_rhAMvPgxU5TXfcsKaNqaW&y$>DE5pd>Tf_O#QO+z%LKC zD1$u^6TnnSpneTs)6Ui}=*P3yM!-MaHMhGeuKk8o2OxBys&!-XPhUNRt?KmaKZv1 zjY94jArLGQ(j2}umfUORcHSZc`QY*^Qo_J*#nVC@1vlff97l=!1-37V&J)i#!^F0{ z$q_C64-wZhQAl(|_UW57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurfXl`tq~_~o=9R?f=9gsV=kX$mCYFIi765TBk)Hqn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f6c3878a96d6ed542593ca88274f9f0fcf5412f9 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurf + + + + + + + + + + + + + + + + + + + + + + + + + + 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 0000000000000000000000000000000000000000..2fb58ef78d340c01fe42e0d91ac07ee06662995d GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurfb0JCYDE5r83Bh>@X;iJ_#7jUhKb uDKjUPjUgvBJu$gbgrPh!Q?D#DB{g3!Gp{5*H@_q^KaUqlG_lMOWB>pi9E@=Q literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0a53b96326370bf666dfc1ffcab8f57f3771f97c GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurf50jeA`In;nR;cJDXIB-nRzAgx%nlT`FXrZqKRdOAOit%+>vbn literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5e3a1fc1eb7fb65a07b4f07c1ea06adf5521dd5a GIT binary patch literal 2605 zcmZ`&X;4#F6uyKA5uqeB1_)c+BUSo%Ra=>`h^W*u6cwx@Drq31vWP~M8PT*QF$f5w zBdaWeMMOr67LlQ7P(j9#MG!&70T&$Wh$|EjWqMvnc$ds1Ki)g}zH`p^opbL)$XFN^ z6C*_kv7@9BVeqDYMhF?RWxK^1Tt?vx9ges%X_yQW5E5uGcrc#JcJGZw$drObP)ooY zVk4XJGAc$+M$}V-BRed1#9}@c+hQ@E?||Vp24cJxkFBwMLpa{kkT*QXP*1!+&wwtT zgZDHP<63w;3+s>X8P5@5Ik>K&o!evihWy|-u7#f$9^+bgE$#=$@%`g|@IHoW@jiHr z*W#Y>96Z+gH=Pm{CYG6*!3}0yc%QqKV3A1-Hp+#+(-5NzHS^aKtb4aTTzIczB+;e8 zYFp2dg@@vk=I1ZY6Rs{kEoe&;Nv=EfhtF}XYl>F&b8QD#h{tm44?d0EtY}u$vXXfb zJ*Q586ZqDug!3ijeUoKEdeg1E`iEm_nV0)LoZ|Rtvfwv6{iPq2$Gl7A4_;b-{3%no zu{~qpf#g(ipQq6w&i1LJ4fie67Mdw%ZgPBk;_CD#*+Z`+ci&WGR7?w07jPxa@c3i? zCy9}jIhj?euVXDcN)DTG7rNBCj0OZ29xh0(jLz~;sIt5Iv+KMAi)=bo>` zqsaY}RNPa&l?)(x1RE8s;nQT@M;k5C5YIjwUJ0JRI5s+^` z*>3f&)OB58$Z^7`#?Ym%ZDqc_u*n&9lZ>{mL0+9+M@1Eh3^{CaCd0HZloszOuuIqw zRM`vtW;4wPOKJV`(KY+;dx8917U#q}x}s7l{CFMg3fa6x`)Rm*-w~&&(+h!c7VvL2 z(nRM_UB>HgB+rbB zWv+5)yeysCm>N+uLM)VJ9N9=Z65axyC;-;-yS8y>iugqyInPhzxM|B6?7;`+-)RM2 zS6263`^JB$qg=@_eayy)3j;o}+f{K;;>$GuLbqh0#?-bsKD{KLSk2<(QN1WF%EG1W z6X_;}!`@0*lON`T;x5063M&QNUr)cx-eiEfF9CY;bwSn71Tf}Wt+>wbnQ3-6;d>0k zq*>6u+wHFa4~xEdpw{at7Z;B@|ML1jCd{}&Pu$mh&donJ+kN*k`Ng1D6_MW51#d&vb0b zO>v@IU8^tNJ`d)?7$z+^*VEB1UYc44CZtUB9-MKzkbV6XVRJp4`o3xHia0 z$cTxd391mN!GWRc3s8)ik;MU`86wsI6k}mx>p&7~Z!i)xVrAjDpa8Z6tgaO(#Kyrb z;errq0t&GU@Frw5gPa0Z1rp+5@L$o3P*r23V#LYN{9z+lmFoeZJ1z;-DZFQ7Ja(sI zX~UPwQ|He|^2h@zj(zyp}Oz^*eQZ{H}q5`~0DQIG14F0Qn V(8R=1+{3~UU?glLfoAVM2LS4mldu2) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b5246b9573e16575c66011a912ce79143eb6d50c GIT binary patch literal 2400 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IZa0*GJ; z1***{DJsolU|hQl0O;eg9t^&G831-aG8%wy#kV%$aZ0I z58H;^l*AG)E?`PxTK{o&ya9*NR2Bh-*+4Eg1JhkSsZ(8N8l=i*ER)^%Ke~)_P5uS1 zZn4&`IcJNyKE0EFs%8Jm*Fd&OJ9XEL`faTNexYicUhLQ(Yn! zp43}xZr*LnxJ=OW=tqw|A66x`-zYCW60gMYl;?7Ko7Bv=aVPVOO?9doL)3Sjd%y2R zAs>g7zhb5Na+7s4b0*p~)O==JU|Rhk*GoidLz`%;gv$3ES!wxgOQ!hSZ~mje`r*Nf zRmVSNsPbvJ{16DLIlX7%^{>Y*t_2u>US+&3`jBhx+`TR9lCI?LI-DStcsN=2!s7)G zr4t`VAKx%H%EGNMR%T*1m*-v6i{+1QALJ5{n6TaIU;0Cx2MfMEUYQ!&<5K$jg_QZX z$BP~`@4fohS=-_A<3PWRX{iUka&;-Q{+(MMJEh&VKCm|ShSrB&;%lxbbWOHqE@MpG zDyeH`>Hp?$*}^EZuKTa5RWA9@YO8zyGfZ33?b6?MhTh9;e%7SH8izH-NOsp}IN3 zNX9_%Ac|XWyE*GK9^_`c`|@2EP=gW^`_lx3J#9b@Dl9x-SAd1OL9ujCa{FZ&Lx~-m zP3J=!h?n9FOm@lwHO>sTc3sk6?<_A&P6gw0Vw2yYM+9Un1LUveVg9$CoXXS#zp|D zRqJo{3YwXNf-MNuLIt3kL*O=bgR=6n*yK76>k}z%$EO3;hoRdkZV-WL=N8HRdy5l5 zp&W&3se+N5K@8kFa4O%_q`g8>b9UPDr*$8WMF6$L!R-d81W=G9CZL4s>4P7`X1&b^ Pxjl(tn*9Vs_%#6l3>sW? literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7213822ae9baff6da41d1fdc00852e2690685166 GIT binary patch literal 2059 zcmZQzV6w=~&rf4uU=YYn%SZvzAi&DNz@$)8ln53t0f{q##TkL5NJ&~s35da<03sL` zFfcHFDJd$=V_;wh32OlLIsj=Pn~4*m4a8=UWdxC6aRyN&wg?hi1&J+;#8yUPBby_Q zBrbu(M%F7vqPQfIdSthWBZ(ucLADE*I4(CJ>or6&A6Xokt&2mA1`cs#_bcI0gKR!> zI3tV8Vd`bQoSTwZ!o>xQQKt1DXU7|G82w@vV3^I&1WXtk7!>YmM@^BC-4{NQrEt?0 zzvRjH?wNJ@P1wJQcUeSv!qa7jE47;vtD?L+PT50Ps`|4?No80@% zC*;#E@!B=W$$wa*6jooBFS}nR|AcJBlk>mMc{8pB$_lqOvEPx^{MSs@*`eIb z)?xpffXr0m0+IXLH`3Rdi9Rp5%d}cz&x_2PCm5Ror?LiUo}T`pPOtvOlX#7p5y5S@ zm}k0wm0zhN8o%%QZBt3fhIy&7dl#Kn@7$U!^i58dQ6t@N#zF&e)3~f#-aF66^`?l+ zE~&ZFsdzAL?}p&-AO4GKrv7w@^4H~-wu-vgecp6)J@@@IpYuGA=eAjS>-}(B!mgcW zu`yYnMU z-mStHefiy%?iVE-i#oUTK9xH6dq$xikGa_H9)@C(^x0EwJk7cJQthrV{@F6;$GnT{ z*S2R1|K;5iIeSx|!lZY$0ZWZjXKrb9KVp>?z31_^MGK;SPWXOnh2_^3n-|Jz$IV%{ zkhU<)3Vvf(?y?4OkdiP$awd7zr7$F>y42bTBXhbv7_`H5e%waIo+SK*YL1 z!skAGczyWZzxFeGB3mb?vMpccU?gb3#UZ%B0imzfNYQ{tK+K>6EY#fqH0lhmtP~sf z0f}aRAM05i4J^q(_wgx69k__lbwbas>+;8n0;?DWBS`}RhWUX~2+1S9p*2fBy}xE{ zBxWGQ$o?=HA+>M*vsm^5pesa}I6kgHh{qU-7>Kd(3VuZh?RWQI1XL&ibZ#9&Gy+$FcLSAgS!jtul+I;GRq7$0K-o4Ad0JI3K)qRC^4}=O@Nx( ztibTVzgKMr&p%Mt8eJ&+mfeQ7I904@J04QXD>Hq^HeFG!7)!hxiXg(0H z#KB%ycH;kKpN|U@wiRERmNh}vGh_BvfxHjXttJ4?FhMvBlvY5-nxQ%=!ARS{0`4Sm z>N)V)`OKf~5pt^l%-qJaks&tD{S_D;Qid*?J+ sONNZ-tF3jGmq7-3aR|;uF-XS12i5kyhi|)?9pjXYQ*Z})!4J)50QJQi@Bjb+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c69605cc50a80b59d6b0e717176cf5a288bd37a8 GIT binary patch literal 981 zcmZQzU~I|F&rf4uU=YYn%SZvzAi%`Hz?f4~ln53t0f_@efPfK*MM~0AN(yD+NE>gcJh_|6aBYy0 zkP#C@6I3Bkg9Ag?7oZq3BZ~t>GeoQbD8|CX)`29}-e4qX#LB{PK>=(DSY0bnh>e3= z!UZAJ1QcQy;7!PA1~~<+3M9nA;J>05p{mA6#fX!k`NKxAD%S%*cU%&vQ+UtFcmfn7i6gOQezAS3gq4M6h+92i7^DWBoWdM1^pca5Lf`X8?r+TU?s z^zQu!a)~x^tU&!jOl(zG;A&YIB7kDTEF7nPAc+M4#Y8x`rEJi|LE z!3N4^0=o(>0TovOyPpXt%*4P1@&+7)f>{a-MnIYch=KZ1jIMtHR9*u} zbAaVh^{bRgIr~gJrv6cCZ&IqcWZG8N;sh-P-s`(r9^B;UG|HVc8*Bia_zwgScR+-` z@f2wZPO)TdnflIq=EQlnSESZw?aQ{@b^G{+moFEFZ;iOt09LpSYQlwCPzIDTJ-#GZ zdHed8l?Ro7b;O&V3H`NKOERQ%*6SxbWA~Jq$wz=96A6IqX9fBa92iLAxR@aIAaEHP zYL%csKmv)!4{txSn*A-~{b=5r1uCpFc8c8obZTEn)B?sA_k=E|-cy4ph6OsvEM))x zcb?{4dT?)0`2?Y)N5ZC4Q`X6{orrn4W~G5jokp;}&V(i@umLdt%RtS8Iicpz4wbcu ztgHW=dQ>o3l38%uB9jiMr-d2bOj3%*jyt_Bfh+?8Wcw-c|7TDlLjs4+NwTX(vB+3- z{bf6If!#SdW>2B^F zlVG+T%JRD6LB}~U?We^5FW6D-CpP{;WeEtt{SOTgdc{Ao{b2utGCL6f9m>5dR*cfv2lQMHs*%)$C(-V^`MHtExGxf?cQ&RKwGV@B}bMs3w^YeI-L=(#l HLDm2OG$@m& literal 0 HcmV?d00001 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/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 0000000000000000000000000000000000000000..279be003148289aa27273d137d7f60d07623dba1 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j+-D(qwpHqY`~Tj$2BA)^%Ree!pFMGOsd(U& z?jm84vz{W@13qzW^$)0i-#=@~HY%%A}lq zCLUA&D77~!)m$=dD{FCrmICkf-7F7oa&#KyPMQrh080G_0*E^x0^fLwv;?PEvbIcp z=RI@cJliW$>$CP{Tkg7je8bC^3&XcYTx$R;+y>QuVHT7DrA&`630B^|{$=Gs-5dfqf1TI5Etr8RnNFed};q7Nu zv%h7$AI)2{K!tV2PLbQ6PVEbcTEO_?p3vpgdukBHus{cyh3xrpCFX< zNZ530$~sxL6EQE>tTa%m(+Jkrnb0H!HUQ>-8K`+MC)6C;p|UoSb@iW9j|wJBG7D~7 zWYXdEv@pY)NlMY!ai`ZMs6UbIr^NrCL5U0r96Be-t{TN6W6|}O?aT#s=j4nRmsryO zzR{9#FbpLZ5ob*=m!tGgNTK8A+9#6~sJn3c6=7rZE<%!%Dy`rD} zQ1VG8*Z`RSGrEy12QvFhdXKrcZ$0|vL8CfPYovPpq-3SLxpz#0*>)()>xu^*=ft$1 z692zoN41~W_y?6GAOQD2G(hMT|H$@({SV6QL;#R`K>(Ki(c?d{kE?TEPhDuX1+&^F z$wS|M|B2$+SswDD@Zce/`, 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}; @@ -598,6 +602,281 @@ pub fn catalogs() -> (Files, Value) { (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), and its files. +#[derive(Debug, Clone)] +pub struct PackageMutation { + pub name: &'static str, + pub reason: String, + pub files: Files, +} + +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 }); + + // 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), 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)); + 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)); + 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)); + 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; 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. + 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 rotated = units.clone(); + rotated.rotate_left(1); + add("floor-opens-on-inter", "floor-keyframe-mismatch", sealed(with(&man, hdr, &rotated))); + 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))); + // 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)); + } + out +} + +/// 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 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(); + cases.push(json!({"name": m.name, "reason": m.reason, "remove": remove})); + } + (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 +} + +/// The first reason the package verifier gives `files`, or `ok`. +pub fn package_verdict(files: &Files) -> String { + match crate::verify::verify_files(files) { + Ok(_) => "ok".into(), + Err(v) => v.into_iter().next().map_or_else(|| "refused".into(), |r| r.code), + } +} + // ─────────────────────────────── version-1 receipts ─────────────────────────────── /// The version-1 receipts and their JSON: the legacy receipt `wai-rs` pins @@ -669,6 +948,15 @@ pub fn regen(dir: &Path) -> std::io::Result<()> { 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(()) } @@ -741,5 +1029,25 @@ pub fn check(dir: &Path) -> std::io::Result> { 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()); + } + for c in want["cases"].as_array().into_iter().flatten() { + let name = c["name"].as_str().unwrap_or(""); + let remove: Vec = c["remove"].as_array().into_iter().flatten().filter_map(|p| p.as_str().map(String::from)).collect(); + let v = package_verdict(&apply_overlay(&golden, &held, name, &remove)); + if v != c["reason"] { + out.push(format!("package-mutations {name}: {v}, the corpus says {}", c["reason"])); + } + } Ok(out) } diff --git a/wai/wai-pack/tests/review.rs b/wai/wai-pack/tests/review.rs index b9758be7e..9ce1894af 100644 --- a/wai/wai-pack/tests/review.rs +++ b/wai/wai-pack/tests/review.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use wai::stage_index::{Role, StageIndex}; +use wai::stage_index::StageIndex; use wai_pack::golden; type Files = BTreeMap>; @@ -54,59 +54,19 @@ fn resealed(mut f: Files) -> Files { f } -fn policy_edit(f: &mut Files, from: &str, to: &str) { - let t = String::from_utf8(f["policy.toml"].clone()).unwrap(); - assert!(t.contains(from), "{from}"); - f.insert("policy.toml".into(), t.replacen(from, to, 1).into_bytes()); +/// 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() } -/// The floor object of piece `p`: its entry index and its bytes. -fn floor_of(f: &Files, p: usize) -> (usize, Vec) { - let ix = index(f); - let k = ix.pieces[p].first_entry as usize + ix.piece_entries(p).iter().position(|e| e.role == Role::Floor).unwrap(); - (k, f[&wai_pack::objects::object_path(&ix.entries[k].content_hash)].clone()) -} - -/// Replace entry `k`'s object with `bytes`, its entry following (hash and lengths). -fn replace_object(f: &mut Files, k: usize, bytes: Vec) { - let mut ix = index(f); - let old = ix.entries[k].content_hash; - let h = wai_pack::objects::object_hash(&bytes); - let man = u32::from_le_bytes(bytes[4..8].try_into().unwrap()); - for e in ix.entries.iter_mut().filter(|e| e.content_hash == old).take(1) { - 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; +/// Each named case 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!(first(&c.files), *reason, "{name}"); } - if ix.entries.iter().all(|e| e.content_hash != old) { - f.remove(&wai_pack::objects::object_path(&old)); - } - f.insert(wai_pack::objects::object_path(&h), bytes); - // Duplicate flags follow the new content. - let mut seen = std::collections::BTreeSet::new(); - for e in ix.entries.iter_mut() { - if seen.insert(e.content_hash) { - e.flags &= !wai::stage_index::ENTRY_DUP; - } else { - e.flags |= wai::stage_index::ENTRY_DUP; - } - } - set_index(f, &ix); -} - -/// A floor object's manifest JSON and its temporal units, and the envelope back. -fn floor_parts(b: &[u8]) -> (serde_json::Value, wai_pack::floor::Av1Header, Vec>) { - let ml = u32::from_le_bytes(b[4..8].try_into().unwrap()) as usize; - let man: serde_json::Value = serde_json::from_slice(&b[8..8 + ml]).unwrap(); - let (units, hdr) = wai_pack::floor::av1_units(&b[12 + ml..]).unwrap(); - (man, hdr, units) -} - -fn floor_envelope(man: &serde_json::Value, hdr: (u32, u32, u32, u32), units: &[Vec]) -> Vec { - let m = serde_json::to_vec(man).unwrap(); - let p = wai_pack::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() } #[test] @@ -119,107 +79,35 @@ fn the_golden_verifies_and_reseals_to_itself() { /// Item 1 of the review: a package is bound to the policy its receipt signs. #[test] fn a_package_is_bound_to_its_policy() { - // A version-2 receipt over no component at all, signed with the package key. - let g = golden_files(); - let mut r = wai::package::PackageReceiptV2::from_json(std::str::from_utf8(&g["package.receipt.json"]).unwrap()).unwrap(); - r.components.clear(); - r.total_entries = 0; - r.unique_objects = 0; - r.total_bytes = 0; - r.stored_bytes = 0; - r.work_units = 0; - for parent in [wai::package::Parent::None, wai::package::Parent::LiveWindow([9; 32])] { - r.parent = parent; - use ed25519_dalek::Signer; - r.sig = key().sign(&r.signing_payload()).to_bytes(); - let mut empty: Files = ["policy.toml", "policy.json"].iter().map(|p| (p.to_string(), g[*p].clone())).collect(); - empty.insert("package.receipt.json".into(), r.to_json().into_bytes()); - golden::relist(&mut empty); - assert_eq!(first(&empty), "policy-component-mismatch"); - } - // The index's class records are not the policy's (a floor-only class offered WAI). - let mut f = g.clone(); - let mut ix = index(&f); - ix.classes[2].flags = 0; - ix.plan[2] = ix.plan[0].clone(); - set_index(&mut f, &ix); - assert_eq!(first(&resealed(f)), "index-policy-mismatch"); - // An entry's capability is no rung of its component. - let mut f = g.clone(); - let mut ix = index(&f); - let at = ix.strings.iter().position(|s| s == "wai.video.av1").unwrap(); - ix.strings[at] = "wai.video.avc".into(); - set_index(&mut f, &ix); - assert_eq!(first(&resealed(f)), "index-policy-mismatch"); - // The string table in another order. - let mut f = g.clone(); - let mut ix = 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_index(&mut f, &ix); - assert_eq!(first(&resealed(f)), "index-policy-mismatch"); - // A class plan the policy does not give. - let mut f = g.clone(); - let mut ix = index(&f); - let p = ix.plan[1].iter().position(|&v| v == wai::stage_index::FLOOR_ONLY).unwrap(); - ix.plan[1][p] = 0; - set_index(&mut f, &ix); - assert_eq!(first(&resealed(f)), "index-policy-mismatch"); - // A class named classic, which would collide with the classical master. - let mut f = g.clone(); - policy_edit(&mut f, "id = \"legacy\"", "id = \"classic\""); - let mut ix = index(&f); - let at = ix.strings.iter().position(|s| s == "legacy").unwrap(); - ix.strings[at] = "classic".into(); - set_index(&mut f, &ix); - assert_eq!(first(&resealed(f)), "pack-id"); - // Under royalty_free_only, a patent-pool floor. - let mut f = g.clone(); - policy_edit(&mut f, "capability = \"wai.video.av1\"\nrole = \"floor\"\nparams = { source = \"supplied\" }", "capability = \"wai.video.avc\"\nrole = \"floor\""); - assert_eq!(first(&resealed(f)), "license-refused:wai.video.avc:patent-pool"); - // The policy's component of another medium. - let mut f = g.clone(); - policy_edit(&mut f, "media = \"video\"", "media = \"image\""); - assert_eq!(first(&resealed(f)), "policy-component-mismatch"); + 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, 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-capability", "index-policy-mismatch"), + ("index-string-order", "index-policy-mismatch"), + ("index-class-plan", "index-policy-mismatch"), + // A class named classic; under royalty_free_only, a patent-pool floor; the + // policy's component of another medium. + ("class-named-classic", "pack-id"), + ("patent-pool-floor", "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(); - // A floor object removed. - let mut f = g.clone(); - let (k, _) = floor_of(&f, 1); - f.remove(&wai_pack::objects::object_path(&index(&f).entries[k].content_hash)); - golden::relist(&mut f); - assert_eq!(first(&f), "object-missing"); - // A floor object corrupted. - let mut f = g.clone(); - let (k, mut b) = floor_of(&f, 2); - *b.last_mut().unwrap() ^= 1; - f.insert(wai_pack::objects::object_path(&index(&f).entries[k].content_hash), b); - golden::relist(&mut f); - assert!(codes(&f).iter().all(|c| c == "object-hash"), "{:?}", codes(&f)); - // A live window whose pieces start past 2^53 − 1: no stage view can be written. - let mut f = g.clone(); - let mut ix = index(&f); - ix.header.prev_index_digest = [3; 32]; - let shift = 1u64 << 53; - for p in ix.pieces.iter_mut() { - p.start += shift; - } - let starts: Vec = ix.pieces.iter().map(|p| p.start).collect(); - ix.header.plan_digest = wai::stage_index::plan_digest(&starts); - set_index(&mut f, &ix); - f.insert("plan-v.txt".into(), wai_pack::pack::plan_file(&starts).into_bytes()); - assert_eq!(first(&resealed(f)), "catalog-mismatch"); - // An on-demand index whose first piece does not start at 0. let mut f = g.clone(); let mut ix = index(&f); for p in ix.pieces.iter_mut() { @@ -229,8 +117,8 @@ fn malformed_packages_are_refused() { 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) = floor_of(&g, 0); - let (_, _, units) = floor_parts(&b); + 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()] }] }; @@ -241,73 +129,41 @@ fn malformed_packages_are_refused() { /// policy allows; every sequence header is the first's. #[test] fn a_floor_piece_opens_on_a_key_frame() { - let g = golden_files(); - let (k, b) = floor_of(&g, 2); - let (man, hdr, units) = floor_parts(&b); - let mut rotated = units.clone(); - rotated.rotate_left(1); - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&man, hdr, &rotated)); - assert_eq!(first(&resealed(f)), "floor-keyframe-mismatch"); - // A key frame inside the piece: refused, unless extra key frames are allowed. - let mut extra = units.clone(); - extra[5] = units[0].clone(); - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&man, hdr, &extra)); - assert_eq!(first(&resealed(f.clone())), "floor-keyframe-mismatch"); - policy_edit(&mut f, "[floor]\n", "[floor]\nextra_keyframes = true\n"); - assert_eq!(first(&resealed(f)), "ok"); - // A sequence header that is not the first's bytes. - let mut other = units.clone(); - let tu = wai_pack::av1::temporal_units(&other[0]).unwrap().remove(0); - let s = tu.sequence_header().unwrap(); - let at = other[0].windows(s.bytes.len()).position(|w| w == s.bytes).unwrap(); - let last = at + s.bytes.len() - 1; - other[0][last] ^= 0x01; - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&man, hdr, &other)); - assert_eq!(first(&resealed(f)), "floor-av1-malformed"); + 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. #[test] fn a_floor_object_is_bound_to_its_profile() { - let g = golden_files(); - let (k, b) = floor_of(&g, 2); - let (man, hdr, units) = floor_parts(&b); - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&man, (1080, 1920, 30000, 1001), &units)); - assert_eq!(first(&resealed(f)), "floor-object-mismatch"); - let mut m = man.clone(); - m["derived_from"]["renderer"] = "anything@1".into(); - m["derived_from"]["settings"] = "00".repeat(32).into(); - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&m, hdr, &units)); - assert_eq!(first(&resealed(f)), "floor-object-mismatch"); - let mut m = man.clone(); - m["target"] = serde_json::json!({"frames": 1, "h": 1, "w": 1}); - let mut f = g.clone(); - replace_object(&mut f, k, floor_envelope(&m, hdr, &units)); - assert_eq!(first(&resealed(f)), "floor-object-mismatch"); + holds(&[ + ("floor-framing-header", "floor-object-mismatch"), + ("floor-renderer-settings", "floor-object-mismatch"), + ("floor-target", "floor-object-mismatch"), + ]); } /// Item 7: a directory read strictly, dot-names and `*.sha256` files included. #[test] fn hidden_files_are_refused() { + holds(&[("dot-file", "unexpected-file"), ("dot-directory", "unexpected-file"), ("dot-object", "unexpected-file"), ("sha256-file", "unexpected-file")]); + // The same cases as directories, read by the verifier's own walk. let dir = std::env::temp_dir().join(format!("wai-pack-hidden-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); - for (name, extra) in [("dot", ".hidden"), ("dot-dir", ".git-objects/blob"), ("dot-object", "objects/.x.wai"), ("sha256", "payload.sha256")] { + 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 golden_files() { - let path = d.join(&p); + 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 path = d.join(extra); - std::fs::create_dir_all(path.parent().unwrap()).unwrap(); - std::fs::write(&path, b"anything").unwrap(); let got = wai_pack::verify::verify_dir(&d).err().map(|v| v[0].code.clone()); - assert!(matches!(got.as_deref(), Some("unexpected-file") | Some("listing-mismatch")), "{extra}: {got:?}"); + assert_eq!(got.as_deref(), Some("unexpected-file"), "{extra}"); let files = wai_pack::verify::read_strict(&d).unwrap(); assert!(files.contains_key(extra), "{extra} is read"); } @@ -317,15 +173,7 @@ fn hidden_files_are_refused() { /// Item 4: the receipt file is its canonical form; a member named twice is refused. #[test] fn the_receipt_file_is_canonical() { - let g = golden_files(); - let text = String::from_utf8(g["package.receipt.json"].clone()).unwrap(); - let v: serde_json::Value = serde_json::from_str(&text).unwrap(); - for bad in [serde_json::to_string_pretty(&v).unwrap(), format!("{text}\n"), text.replacen("{\"asset_id\"", "{\"asset_id\":\"video-base-01\",\"asset_id\"", 1)] { - let mut f = g.clone(); - f.insert("package.receipt.json".into(), bad.into_bytes()); - golden::relist(&mut f); - assert_eq!(first(&f), "receipt-json"); - } + 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 -- 2.54.0 (Apple Git-157) From 27b648044fc38097dcfb790da0af77207886a8b7 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 08:03:24 -0400 Subject: [PATCH 24/28] wai: the encoder-cap test holds the clip cap's own refusal; to_json's doc says an unmetered receipt writes energy_provenance null Co-Authored-By: Claude Opus 5.5 --- wai/wai-pack/tests/refusals.rs | 15 ++++++++++----- wai/wai-rs/src/package.rs | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs index 62e7c105d..69bbc19f2 100644 --- a/wai/wai-pack/tests/refusals.rs +++ b/wai/wai-pack/tests/refusals.rs @@ -155,14 +155,19 @@ fn a_meter_with_no_figure_refuses_the_pack() { 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 what the -/// encoder holds, refused before anything is allocated; the packer reports it -/// `pack-encode`. +/// 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 r = wai_pack::encode::choose_q(&frames, h, w, &p); - assert!(r.is_err(), "{:?}", r.map(|q| q.q)); + 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}"); } diff --git a/wai/wai-rs/src/package.rs b/wai/wai-rs/src/package.rs index 938879ead..d897f082e 100644 --- a/wai/wai-rs/src/package.rs +++ b/wai/wai-rs/src/package.rs @@ -503,8 +503,8 @@ impl PackageReceiptV2 { /// The receipt as JSON: RFC 8785 canonical (members in code-unit order, no /// insignificant whitespace), hexadecimal lowercase, integers as numbers. An energy - /// label adds its fields (`energy_provenance` and its evidence); an unmetered receipt - /// has none. + /// 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") -- 2.54.0 (Apple Git-157) From 83668b5f70f2594e75dc9514cd4b22e3a7db07ad Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 08:05:45 -0400 Subject: [PATCH 25/28] pack-conformance: a class record alone against its policy (index-class-rate) A class's rate one below the policy's, the plan, catalogs and receipt following the index: refused index-policy-mismatch by both verifiers, so the class-record check is held on its own and not only through the class plan. Co-Authored-By: Claude Opus 5.5 --- .../package-mutations/expected.json | 5 +++ .../index-class-rate/LISTING | 35 ++++++++++++++++++ .../index-class-rate/index-v.wsix | Bin 0 -> 1728 bytes .../index-class-rate/package.receipt.json | 1 + wai/wai-pack/src/golden.rs | 12 ++++-- wai/wai-pack/tests/review.rs | 5 ++- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 wai/pack-conformance/package-mutations/index-class-rate/LISTING create mode 100644 wai/pack-conformance/package-mutations/index-class-rate/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/index-class-rate/package.receipt.json diff --git a/wai/pack-conformance/package-mutations/expected.json b/wai/pack-conformance/package-mutations/expected.json index 1754ad80a..5484fdaaa 100644 --- a/wai/pack-conformance/package-mutations/expected.json +++ b/wai/pack-conformance/package-mutations/expected.json @@ -82,6 +82,11 @@ "reason": "index-policy-mismatch", "remove": [] }, + { + "name": "index-class-rate", + "reason": "index-policy-mismatch", + "remove": [] + }, { "name": "index-capability", "reason": "index-policy-mismatch", 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 0000000000000000000000000000000000000000..515c472ffda52298be062d1121c5a896516a739f GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j{P3XR)$|#!TlkcU6Uwf8a_m{OJtFGW^C!E- zW+?VBE{+2#AqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgr_3(tFIkee2OT4;s~ZS|io#CnYQ0&Anq1%(g>WURONmI47q4 zl=%MzJF5M}#y_Ym0Rg!Gp#eg#_(!%M?0-;ZCjx-n3j(n8j~@SteO#UUdg?;6Etu6d zNgn$4`%e_l&hn5Kg$EB=&D^`CaaA(J5?FQvnT4GGTZ6=!Z(kO7ubt-M zv>tAjn=TenS8ab9Zr=JX0&D;*{$C`aI=W28&C{tSurf Vec { 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), an - // entry's capability no rung's, the string table in another order, a class plan the - // policy does not give. + // 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"); diff --git a/wai/wai-pack/tests/review.rs b/wai/wai-pack/tests/review.rs index 9ce1894af..4df92cf63 100644 --- a/wai/wai-pack/tests/review.rs +++ b/wai/wai-pack/tests/review.rs @@ -83,9 +83,10 @@ fn a_package_is_bound_to_its_policy() { // 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, an entry's capability, the string table's order and - // a class plan, each not the policy's. + // 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"), -- 2.54.0 (Apple Git-157) From f3d9e6e56acaf885d6d99b1e66c60dd69d42183b Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 08:18:48 -0400 Subject: [PATCH 26/28] pack-conformance: a piece with no key frame, a floor presenting another base, a CMAF file altered, a patent-pool floor under the packer's override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Falsification found two cases that did not hold their rule alone: - floor-opens-on-inter rotated the piece, which also put a key frame inside it, so the extra-key-frame rule refused it whatever the opening rule did. The case (and its packer-side twin in refusals.rs) is now a piece whose first unit is an inter frame and which holds no key frame at all. - patent-pool-floor put royalty_free_only in policy.toml, which the policy reader already refuses in pack_verify.py, so the check under the effective policy was never reached alone. patent-pool-floor-by-override leaves it out of policy.toml and states it in policy.json, the packer's override (§3.2); reseal keeps policy.json's royalty_free_only, as the verifier reads it. New cases floor-motif (floor-motif-mismatch) and floor-cmaf (floor-cmaf-mismatch) give every floor reason a verifier names a package case, in Rust and Python. Co-Authored-By: Claude Opus 5.5 --- .../package-mutations/expected.json | 17 +++++++ .../package-mutations/floor-cmaf/LISTING | 35 ++++++++++++++ .../floor-cmaf/floor/v/init.mp4 | Bin 0 -> 628 bytes .../package-mutations/floor-motif/LISTING | 35 ++++++++++++++ .../floor-motif/index-v.wsix | Bin 0 -> 1728 bytes ...f50e2cfbd4062e81ed7029da687026ef4bd11.wai} | Bin 2503 -> 2503 bytes .../floor-motif/package.receipt.json | 1 + .../floor-opens-on-inter/LISTING | 20 ++++---- .../floor-opens-on-inter/dash/classic.mpd | 2 +- ...6aa28e783a8cd220eed53a8d84de9feeb622d3.m4s | Bin 2400 -> 0 bytes ...94a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s | Bin 0 -> 1872 bytes .../hls/classic/floor-v.m3u8 | 2 +- .../hls/classic/master.m3u8 | 5 ++ .../hls/legacy/master.m3u8 | 5 ++ .../hls/mobile/master.m3u8 | 6 +++ .../floor-opens-on-inter/hls/tv/master.m3u8 | 6 +++ .../floor-opens-on-inter/index-v.wsix | Bin 1728 -> 1728 bytes ...3df833e28d8bf88a7ad829d3f3c5ed5e623227.wai | Bin 0 -> 1975 bytes .../floor-opens-on-inter/package.receipt.json | 2 +- .../patent-pool-floor-by-override/LISTING | 35 ++++++++++++++ .../index-v.wsix | Bin 0 -> 1728 bytes .../package.receipt.json | 1 + .../patent-pool-floor-by-override/policy.json | 1 + .../patent-pool-floor-by-override/policy.toml | 45 ++++++++++++++++++ .../stages/legacy.json | 1 + .../stages/mobile.json | 1 + .../stages/tv.json | 1 + wai/wai-pack/src/golden.rs | 38 +++++++++++---- wai/wai-pack/tests/refusals.rs | 9 ++-- wai/wai-pack/tests/review.rs | 11 +++-- 30 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 wai/pack-conformance/package-mutations/floor-cmaf/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-cmaf/floor/v/init.mp4 create mode 100644 wai/pack-conformance/package-mutations/floor-motif/LISTING create mode 100644 wai/pack-conformance/package-mutations/floor-motif/index-v.wsix rename wai/pack-conformance/package-mutations/{floor-opens-on-inter/objects/4e22514b486de10123a49b511cc0ca749a38d4a80d85ff8f71dbc55ebab102ee.wai => floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai} (83%) create mode 100644 wai/pack-conformance/package-mutations/floor-motif/package.receipt.json delete mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/classic/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/legacy/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/mobile/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/hls/tv/master.m3u8 create mode 100644 wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/a3ff3effd00c6c5eaf8af992493df833e28d8bf88a7ad829d3f3c5ed5e623227.wai create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/LISTING create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/policy.toml create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/patent-pool-floor-by-override/stages/tv.json diff --git a/wai/pack-conformance/package-mutations/expected.json b/wai/pack-conformance/package-mutations/expected.json index 5484fdaaa..d68acb9ab 100644 --- a/wai/pack-conformance/package-mutations/expected.json +++ b/wai/pack-conformance/package-mutations/expected.json @@ -115,6 +115,11 @@ "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": "component-medium", "reason": "policy-component-mismatch", @@ -212,6 +217,18 @@ "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", 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 0000000000000000000000000000000000000000..7f51f9b0c3cc792442273d08d829c75b341e0379 GIT binary patch literal 628 zcmaJJ6)|*Rpb|p{RCR=(f*3sQU zRLUVZTZ+8J9GTaMK_tsn0noS7t>(`jiKaFbDrXHF*L!PK#RvSO%}WKm(?++q%$im6 z6@Z!^2mW1xvpj57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=ho3l38%u zB9jiMr-d2bOj3%*jyt_BLH&trKPCSE3`%53;LtfqcGV~r8H=vJY-cX8J11wnxWtnF z_l=g^>)kCItUqb>KorBYq5Gff;H0;b7jDlI)4IQk_jp?7=SeSnHZQ#XC{N_J=oS6! zhmub+!3MzmpV5tEIgsfZG3o62KPz`6ZuUCa@IK(t`MnNFFXXN>&0W^cl=o$?;Bih& z`zi7N3wBidiH(0ySpouZ|3d?WUh$7?KiL1E%uWOVxfcXr=^s7*6Z^P2_x03;W?L|; zZIV3n?f0K3o}J|(FA5JHvYNSfOXI3!h$XP>1~Lmd{kH~*HQ&B0?p{01!^h*VPGH$W z!)ZO-EH_;&qORKhG~B%PT?E(wSp2_8LUnYRjGL!ZO<-k~cIMQK^6=D6obMVgefcBv z!J)Y)U1gQ-I%s}Hwx5#tKaUn}uzUq`6)0L$U>qO~40m)Tz(fFoU;vaXV`3;NV`Iq8 vPs+?mWn;)mO;1d&6k#Y&%+xE(Oi9hx%gif@&&@B%%+KRR5=|^K1X%+B7kZUy literal 0 HcmV?d00001 diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/4e22514b486de10123a49b511cc0ca749a38d4a80d85ff8f71dbc55ebab102ee.wai b/wai/pack-conformance/package-mutations/floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai similarity index 83% rename from wai/pack-conformance/package-mutations/floor-opens-on-inter/objects/4e22514b486de10123a49b511cc0ca749a38d4a80d85ff8f71dbc55ebab102ee.wai rename to wai/pack-conformance/package-mutations/floor-motif/objects/455892cdcffca9b861b34ac980ef50e2cfbd4062e81ed7029da687026ef4bd11.wai index 913d490519b03c5e9189d58a8691d703b03106cd..6fa04d28ab37951b1aa02ba2f7a1ec0ad067ade4 100644 GIT binary patch delta 97 zcmX>ud|Y_K5>F!&b2Gz46JwKPOT%O{Bl9#v!^ET%Gb1xoV{=1GGqWU915ud|Y_K63=9# - + diff --git a/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s b/wai/pack-conformance/package-mutations/floor-opens-on-inter/floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s deleted file mode 100644 index cfa347e0b86d023cfea1db3635fbbe144b07ae4b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2400 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7kamJy;5L@FV%MUdFaNNiywwlor(7;$+V>Sb|=tKblqBSIWm zjU*2BxWvVAsF$LWxEKy|aD@Xddy&Ij0?ABV{=#KGF7*mXW+K~#!#zwYkR-^qAvYzl z#7M}1`9K2$1KR~)Y6H>@3^SUI6bx7xS&+r<#1v*L?%-Gct|AqA()67GP!}5$M+1_s zt_CAT0}d8m0VJ^tYpyM1;I@)>S(2pP@Irgl4kJMWE)Ky34hYNYjU)|t1jGzF5JIPP zUwah4t#vp6RL!R#b>JdGstl-FfMI^16hi3umW97luC2A108}l+$o?=HA(dk!YaqhJ z@o^1Ar~&A`BM#n@e;2G!Tae27&J3tSjD=V5D?&$xk+gvX(1CRbp}QU0i%TV^Ia|DG z0IHV)I<+4mnPH@4AOm+|H^_#&p3FLVMW?G0H919O8}58>0BMs$b#sJ~jDg}o6t~`X zbJk}($jx~7<-0DR1|=r;rwIsq+JG8VSa`m!01I`4V(Fgb_RBJc5<51V+Fbz3s&NQT z{eaMLG}Yc&`U-1*sz^5E21EwK@#*LnRPG zcV>RcD@*~Y)kAetfRU(y0o+aClyf_2PD%n$r4g!Q0*oXKOi*2L^Gr@k^2-SzW6e;V zkN{L{f$D?@0rMFH1m`&b6FKpcQ4 zb%8Vuph71FF`EE{m+OG3z=dJnnF|P^&CMR+JU&|voy*$=RPV;bepdh?sQ`3^2Mf<% zBqI)LF*umEJ`Dvb_Tmtni)4}lP{;?>J_RE&13y&zHofIfT;c$XjQ~`u*5B$CG&2VU zTM(*+3P3l9z-{UVW#whD$#opoCsN#wPY0?GL$_1hAOh9SEt3297AJs0ISSQM1tU3w z7`S!dRKBT6dxfIr?6l=i>pmQd0BVVY+YL?$pdd+1Knc~;2S0|*dYcb&dlJJm`w58f zYvST!0LIArkF(N?XPRW@Up?8g7mWt?mBFL-r}wRX)p zTh#UGo%~ZR`&YgOvQ65lyJpmHYYp%VRonDp$1X{$j~k7hdncVbx5nA)adgjI9Ye|a zo@ai&>rU+PXpXRcckSD|g3kTgRU6kF<(`yxNWD_o%A>*k`x(*Ly6Ib&KD_s&-ePm} zZd=A>f~H45dhGeIDyjWOdGV2WC5ERwm($y%X1Ib=AB2pXLL|Y|PzURnF%WqpU#ovDO9|hJA4_2%?{wYJ1 zPs8PhKv2!;Jrl2gJ#KL=!1(hj<89H0Tyy8{ZCRIeC3n~11gXTs$+{OFFL)@O_&ECb zhPhD|ZiTTj6T7)Q@0wmLe{}mGmw?2C?N{~b0+v-I7@a7#jV IZN*a`03!cfQ~&?~ 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 0000000000000000000000000000000000000000..09980c3849df4a78b46ed76b76fe87a43b11c2e1 GIT binary patch literal 1872 zcmZQzVA_zIpP$CSz#x#DmXQLaL4cWofoVobQ6gBp1SHM`7H0&KA|+`lB_IYAgaXy( zloXZbF)*-$gpGjw9Y7kamJy;5ovnmJTp3AR7>O;7#3n{u9*25a9O5cC#N~((M^+<= zLp?5WaUANUs3b0i!yH`UfXiOwFqc3w6PLemnU71o0+N}?c42W3%YxjL#1bPR1Lgw_ z3=C`+fT;~gH!#d-#ww;@z{1FatnN-sVYcE9e&z2fQjsT3-w6P9u`zKpAeq$FV5Dfk z!NMzmBz9rVwS^4aR?;p@lC&FMXs_B~Bxu0JA-KQ+;edK0Ndq1MF@p|-&?(*59>s5K z9S#6h^C?IjxQLJ{1F9Bam>(#G5IVkP;qR1dYi%X~RSPk)KTJkQ!o}Zin{bQpst~7H=AW>ZO2A z?MFyv7%3UZz@69)vf-{Lvrb;o>8eCcP7&FLJKq~X+T>8(9APA5pm-3)t+(Br^%)Ox zGv0mqt_!F^iHZGb0>Yj)pavBdp06vwLfxR4yC=E*vW%g`j?Jca7l5*A9D-9nAT%6J zwRhGy|8U*we-jmr#0@kA#7rs>vUk(A`8;3&IZR7I>c|O%L;z5&4#WIV353v{nP2h> zQ$T9562>aTH@e#gHr-1ND>oJLiO~)k72Xk=7Zdx#4yc%0wVmH0QtUG AEdT%j literal 0 HcmV?d00001 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 index fa6e0620d..55d12301e 100644 --- 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 @@ -10,7 +10,7 @@ #EXTINF:1.000, ../../floor/v/04c948608aaa7b5d78ad5355042a2ff65b0f75d9d4b1ad78f3ebffb886a74be6.m4s #EXTINF:2.000, -../../floor/v/575550f0d85481f0d5e0475be86aa28e783a8cd220eed53a8d84de9feeb622d3.m4s +../../floor/v/f8b42103a33523f5f17f06de0894a002a28d50d3b7ec01c4a97ee02c2b8385ac.m4s #EXTINF:1.000, ../../floor/v/06d5cd79ba3860cccdc252fe915de0902f941a3c6a000295714f7cab193aa6cf.m4s #EXTINF:1.600, 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 index 48386d6bb9a4bb5f75443554bd7e2a0f6f73f496..5fed22595e92b3f576e3eeb647c764b0f7448262 100644 GIT binary patch delta 61 zcmX@Wdw_St0cOp`|Ly)?;K_+w-}Q5nr|l2pN4?!Yx~gtyUjBUaZCsL(`gV2(FkqVe PlF^;%2J2=cmOdr`sLUFb delta 61 zcmX@Wdw_St0cK4m{7%)wK P$>`4Xf_<|QOCJ*e7X26B 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 0000000000000000000000000000000000000000..f108cfd0a6b0d211c85ffc64bce705210b3b010a GIT binary patch literal 1975 zcmZ{kZA=_R7{~YMwVamPJ3>$HfO@wgq2m$XFD#0aL^Y8%BgY1D#gKVWPsq)lx4!PGd*niHxTkXyjOase$Tt{32$Z=6rfxzj!qNoN0lCDDlWPz7>NkIyX z13{7vi1fJ1A*5-VB4~iujQBDuj_SAt9G*I!B#S@;nj%3}!?As+P{2d?;aCl7 zvZxtIgpw@k5>OKW>7vLRh9pRetO`Oxff|(Kye>;fka-PfsA^o}1*ph8Z*OEl&F@C| zKn7VOuXZCGD}or?$tHOz#_na4k}x#%^J<2PGYnIWR|>BXUihS$AYP?RycDdOwiss3 zEW;GxUyFHt->nrp1A8LC>+!Di%vHhr4XZ=z4!PEw)FaU1%1;$ep@A)c62!RrR5bUSI_xE0D?pqv6 zO%vxGk?{0Sv|xZZ{mj^-VJe)OJn}a@|6#)@ah7_DFX^M_(aV(TD>F@chI{oaGkn4?{eG~pd61fOojRVb?^3v4mB!&TErX1L%mE(I%^y+ z3o6Ml2kp-DJ66TA`7_U{n{F$svgU4BG-9KBy7q_@Q3;|muBk3GHQwi$S?ignKwRdx#*?vnUGKz8EvMvz!WZZ~T?*h16H?=1$wU9N*Lp z01$4==Dwi44KrlgxEnZK>A1>);HLcLY!}ookJ5IM8#6?{>d z>}zf-X_@@`hwK-`+f-CM@24dZLfY&txw#^AW;2r#Us%{lj1T$(@2tp<5TV6Q6A3#b z1i`~@n&XS>)uSnL@@Y57$G^T5kYsukkGXLoWX8t{$3gqk_TE$au^n)f aR?WE1>&(ox);AY(bXJWyv~`r8$;iK}+Tbn# literal 0 HcmV?d00001 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 index ad3fe796f..f72610e5d 100644 --- 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 @@ -1 +1 @@ -{"asset_id":"video-base-01","components":[{"id":"v","index":"index-v.wsix","index_digest":"a2fe882a6dc379c8e5013120d63eb68c4ee43eaa2db450f07fc9337547399d36","media_class":"video","stage_root":"35e4f438dd2ac959479f808e25b4802c0e2c66fea7ca74a199458faaed946cb4"}],"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":"aeef8bef65bba53f49129385f53e28e3a016701e9c22ffe09dc1de4cb1662c85","sig":"bd35750cf122d0b86401f04a1d974cf666ec1b5e3210feac5b32c587481a1e03b2565c8abda1cd47ab4cac9e0966ad22668e47e5ccd9913c9a718f9dc7b60f0b","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 +{"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/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 0000000000000000000000000000000000000000..279be003148289aa27273d137d7f60d07623dba1 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!j+-D(qwpHqY`~Tj$2BA)^%Ree!pFMGOsd(U& z?jm84vz{W@13qzW^$)0i-#=@~HY%%A}lq zCLUA&D77~!)m$=dD{FCrmICkf-7F7oa&#KyPMQrh080G_0*E^x0^fLwv;?PEvbIcp z=RI@cJliW$>$CP{Tkg7je8bC^3&XcYTx$R;+y>QuVHT7DrA&`630B^|{$=Gs-5dfqf1TI5Etr8RnNFed};q7Nu zv%h7$AI)2{K!tV2PLbQ6PVEbcTEO_?p3vpgdukBHus{cyh3xrpCFX< zNZ530$~sxL6EQE>tTa%m(+Jkrnb0H!HUQ>-8K`+MC)6C;p|UoSb@iW9j|wJBG7D~7 zWYXdEv@pY)NlMY!ai`ZMs6UbIr^NrCL5U0r96Be-t{TN6W6|}O?aT#s=j4nRmsryO zzR{9#FbpLZ5ob*=m!tGgNTK8A+9#6~sJn3c6=7rZE<%!%Dy`rD} zQ1VG8*Z`RSGrEy12QvFhdXKrcZ$0|vL8CfPYovPpq-3SLxpz#0*>)()>xu^*=ft$1 z692zoN41~W_y?6GAOQD2G(hMT|H$@({SV6QL;#R`K>(Ki(c?d{kE?TEPhDuX1+&^F z$wS|M|B2$+SswDD@Zce Result { /// (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`; an index whose +/// `policy.json` and every index's policy digest follow `policy.toml` (under the +/// `royalty_free_only` the old `policy.json` states, if any); 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> { use crate::catalog::{CompView, FloorView}; files.remove("LISTING"); - let policy = crate::policy::Policy::parse_bytes(files.get("policy.toml").ok_or("no policy.toml")?).map_err(|e| e.to_string())?; + // policy.json's royalty_free_only, where it states one, 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)); + 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 }); files.insert("policy.json".into(), policy.canonical_json().into_bytes()); let policy_digest = policy.digest(); let comp = policy.components.first().ok_or("no component")?; @@ -760,6 +764,12 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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)); let mut f = g.clone(); policy_edit(&mut f, "media = \"video\"", "media = \"image\""); add("component-medium", "policy-component-mismatch", sealed(f)); @@ -786,9 +796,10 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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; 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. + // 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]| { @@ -796,9 +807,9 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { replace_object(&mut f, k, floor_envelope(m, h, u)); f }; - let mut rotated = units.clone(); - rotated.rotate_left(1); - add("floor-opens-on-inter", "floor-keyframe-mismatch", sealed(with(&man, hdr, &rotated))); + 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))); @@ -819,6 +830,17 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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. diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs index 69bbc19f2..f33922103 100644 --- a/wai/wai-pack/tests/refusals.rs +++ b/wai/wai-pack/tests/refusals.rs @@ -83,10 +83,11 @@ fn every_floor_reason() { assert_eq!(case(Vec::new(), &id), "floor-av1-malformed"); // One unit short. assert_eq!(case(u[..u.len() - 1].concat(), &id), "floor-frame-count"); - // Piece 2 (frames 85 to 134) opening on an inter frame. - let mut moved = u.clone(); - moved[85..135].rotate_left(1); - assert_eq!(case(moved.concat(), &id), "floor-keyframe-mismatch"); + // 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(); diff --git a/wai/wai-pack/tests/review.rs b/wai/wai-pack/tests/review.rs index 4df92cf63..6254b5178 100644 --- a/wai/wai-pack/tests/review.rs +++ b/wai/wai-pack/tests/review.rs @@ -90,10 +90,12 @@ fn a_package_is_bound_to_its_policy() { ("index-capability", "index-policy-mismatch"), ("index-string-order", "index-policy-mismatch"), ("index-class-plan", "index-policy-mismatch"), - // A class named classic; under royalty_free_only, a patent-pool floor; the - // policy's component of another medium. + // A class named classic; under royalty_free_only (the policy's, and the + // packer's override as policy.json states it), a patent-pool floor; the policy's + // component of another medium. ("class-named-classic", "pack-id"), ("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"), ]); } @@ -138,13 +140,16 @@ fn a_floor_piece_opens_on_a_key_frame() { ]); } -/// Item 6: a supplied floor's members are §7.2's. +/// 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"), ]); } -- 2.54.0 (Apple Git-157) From 39375763c77e56908ad236fea292df4721fd429f Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 09:09:09 -0400 Subject: [PATCH 27/28] wai-pack: an override never relaxes royalty_free_only; empty and unreadable directories refused; pack only into an empty --out; classic in any case; deep alterations never panic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 3 of the package review. A. §3.2: an override only forces royalty_free_only on. Overrides.royalty_free_only is a bool (force on); Policy::effective ORs it in. Both verifiers read policy.json's royalty_free_only as that override, so a policy.json of false over a policy.toml of true is policy-json-mismatch. pack_verify.py parses policy.toml, applies the override, then checks §3.6 under the effective policy (§11.3 step 4), no longer refusing at the parse. Corpus case policy-json-relaxes-royalty-free (the review's pk-pol-rfo-false: a patent-pool floor relabelled through the objects and index, sealed with policy.json false). golden::reseal_as seals a policy.json given as bytes. The policy corpus's override case is now eq-defaults-a.toml forced on (example.toml forced off was a relaxation); packs.rs takes the line out of the policy instead. B. pack_verify.py's strict walk raises on a directory it cannot read (os.walk onerror) and refuses it, as read_strict does. C. Both verifiers refuse a directory below the package that holds nothing (unexpected-file); §11.1 says "no other file or directory". Corpus cases empty-directory, empty-dot-directory (.git/refs) and unreadable-directory (mode 0, or empty where the platform has no modes). Package cases are now written out and verified as directories by both verifiers (golden::package_case_verdict). D. wai-pack pack refuses an --out that exists and is not empty (pack-out-not-empty, pack::out_dir_ready), before any work; §11.4 and §12. refusals.rs runs the command line: a stale claim left alone, a file in the way, an empty and a new directory packed and verified. E. A class named classic in any case is pack-id (eq_ignore_ascii_case); corpus case class-named-classic-in-capitals. F. review.rs deep_alterations_never_panic: 300 seeded rounds of floor-unit, framing, base-payload and index-field alterations through golden::replace_object and a re-seal; never a panic; anything accepted is in a stated equivalence class (AV1 frame data, producer statements, equivalent WIV1 encodings) or the golden itself. Floors: review 9, refusals 5. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-pack.yml | 4 +- wai/extensions/packaging.md | 31 +-- wai/pack-conformance/README.md | 17 +- wai/pack-conformance/pack_verify.py | 50 +++-- .../class-named-classic-in-capitals/LISTING | 34 ++++ .../index-v.wsix | Bin 0 -> 1728 bytes .../package.receipt.json | 1 + .../policy.json | 1 + .../policy.toml | 47 +++++ .../stages/Classic.json | 1 + .../stages/mobile.json | 1 + .../stages/tv.json | 1 + .../package-mutations/expected.json | 42 ++++ .../policy-json-relaxes-royalty-free/LISTING | 35 ++++ .../index-v.wsix | Bin 0 -> 1728 bytes ...7c07efba99f455fc48b40a36f9a8303e10b428.wai | Bin 0 -> 2648 bytes ...41e1eebb9b7565cb1615ea1457936a974ebeed.wai | Bin 0 -> 2503 bytes ...a7e7cac75709acc00d304e5d42633d85aaf2ec.wai | Bin 0 -> 2222 bytes ...fa7c36576c8228ae1a631a08d9c766bbd0e355.wai | Bin 0 -> 1234 bytes .../package.receipt.json | 1 + .../policy.json | 1 + .../policy.toml | 46 +++++ .../stages/legacy.json | 1 + .../stages/mobile.json | 1 + .../stages/tv.json | 1 + wai/pack-conformance/policy/expected.json | 8 +- wai/wai-pack/src/bin/wai_pack.rs | 8 +- wai/wai-pack/src/conformance.rs | 13 +- wai/wai-pack/src/golden.rs | 138 +++++++++++-- wai/wai-pack/src/pack.rs | 19 +- wai/wai-pack/src/policy.rs | 26 ++- wai/wai-pack/src/verify.rs | 25 ++- wai/wai-pack/tests/packs.rs | 27 ++- wai/wai-pack/tests/refusals.rs | 54 +++++- wai/wai-pack/tests/review.rs | 183 +++++++++++++++++- 35 files changed, 715 insertions(+), 102 deletions(-) create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/LISTING create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/policy.toml create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/Classic.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/class-named-classic-in-capitals/stages/tv.json create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/LISTING create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/index-v.wsix create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/625a28c3601bc4f6806179f6247c07efba99f455fc48b40a36f9a8303e10b428.wai create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/63ce2c64c49f7e10935c342b6141e1eebb9b7565cb1615ea1457936a974ebeed.wai create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/cd24dd001a473e1c8925393968a7e7cac75709acc00d304e5d42633d85aaf2ec.wai create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/objects/fc49fcf2bd8fcd8da7cdc095b0fa7c36576c8228ae1a631a08d9c766bbd0e355.wai create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/package.receipt.json create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.json create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/policy.toml create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/legacy.json create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/mobile.json create mode 100644 wai/pack-conformance/package-mutations/policy-json-relaxes-royalty-free/stages/tv.json diff --git a/.github/workflows/wai-pack.yml b/.github/workflows/wai-pack.yml index 9d7b13dc3..2ce932959 100644 --- a/.github/workflows/wai-pack.yml +++ b/.github/workflows/wai-pack.yml @@ -97,8 +97,8 @@ jobs: --lib 28 --test=conformance 1 --test=packs 7 - --test=review 8 - --test=refusals 4 + --test=review 9 + --test=refusals 5 --test=scenecut 3 --test=source_alloc 1 EOF diff --git a/wai/extensions/packaging.md b/wai/extensions/packaging.md index caf7ac465..4abc02d86 100644 --- a/wai/extensions/packaging.md +++ b/wai/extensions/packaging.md @@ -40,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 @@ -870,7 +871,8 @@ 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: +**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 … @@ -882,8 +884,8 @@ royalty-free by its standards body"; it is not a freedom-to-operate opinion. `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` are -refused (`pack-id`). +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** @@ -898,18 +900,20 @@ 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 or anything else is - refused (`unexpected-file`). + 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); `policy.json` is its effective - canonical JSON with `policy.json`'s `royalty_free_only` (`policy-json-mismatch`); 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 + `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 @@ -961,7 +965,10 @@ piece past `2^32 − 1` units (`pack-piece-length`); catalogs it cannot write, t §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`). +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 @@ -981,4 +988,4 @@ write (`pack-measurement-claim`); and a listing it cannot write (`pack-listing`) | `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` | §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 653f7380e..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. @@ -54,12 +55,14 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi 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 and its reason): a receipt over no - component, an index that is not its policy's, the policy's own refusals, 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 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). + 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 diff --git a/wai/pack-conformance/pack_verify.py b/wai/pack-conformance/pack_verify.py index 27476e6fa..8008ab14f 100644 --- a/wai/pack-conformance/pack_verify.py +++ b/wai/pack-conformance/pack_verify.py @@ -2366,14 +2366,22 @@ def load(path): 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.""" + 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 = [] - for at, dirs, files in os.walk(root): - 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)): + try: + for at, dirs, files in os.walk(root, onerror=unreadable): + if at != root and not dirs and not files: return None - out += [os.path.relpath(os.path.join(at, f), root).replace(os.sep, "/") for f in files] + 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()) @@ -2447,14 +2455,17 @@ def verify_pack(rep, d, name, pkg=None, inputs=True): 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 = admit(toml_b) + 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 isinstance(given.get("royalty_free_only"), bool): - P["royalty_free_only"] = given["royalty_free_only"] + 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: @@ -2550,9 +2561,11 @@ def package_verdict(d, name, pkg): def verify_package_mutations(rep, d): - """package-mutations/: each case the golden less the files it removes, with its - overlay, written out and verified as a package (§11.3); refused with the corpus's - reason, or accepted where it says `ok`.""" + """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: @@ -2567,13 +2580,22 @@ def verify_package_mutations(rep, d): 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}") @@ -2912,8 +2934,8 @@ def verify(d): 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")) 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 0000000000000000000000000000000000000000..c90160d32707cc5c1ab744fbbcac0a445e202284 GIT binary patch literal 1728 zcmWF!_KaX;U|^60Vi3&=#9Tnk2E@!jY#&+M@u&HH|Ktq|%9K;|KZqop=UlIJP1w@h zdY{aWrguOkB!P(ZKU$Bt(^Cuu%(vVX5z7^QAh^?{@xIF6O$){LrOtYR%P{W@13qzW^$)0i-#=@~HY%%A}lq zCLUA&D77~!)m$=dD{FCrmICkf-7F7oa&#KyPMQrh080G_0*E^x0^fLwv;?PEvbIcp z=RI@cJliW$>$CP{Tkg7je8bC^3&XcYTx$R;+y>QuVHT7DrA&`630B^|{$=Gs-5dfqf1TI5Etr8RnNFed};q7Nu zv%h7$AI)2{K!tV2PLbQ6PVEbcTEO_?p3vpgdukBHus{cyh3xrpCFX< zNZ530$~sxL6EQE>tTa%m(+Jkrnb0H!HUQ>-8K`+MC)6C;p|UoSb@iW9j|wJBG7D~7 zWYXdEv@pY)NlMY!ai`ZMs6UbIr^NrCL5U0r96Be-t{TN6W6|}O?aT#s=j4nRmsryO zzR{9#FbpLZ5ob*=m!tGgNTK8A+9#6~sJn3c6=7rZE<%!%Dy`rD} zQ1VG8*Z`RSGrEy12QvFhdXKrcZ$0|vL8CfPYovPpq-3SLxpz#0*>)()>xu^*=ft$1 z692zoN41~W_y?6GAOQD2G(hMT|H$@({SV6QL;#R`K>(Ki(c?d{kE?TEPhDuX1+&^F z$wS|M|B2$+SswDD@Zce8_shZm zj~mZiDiZ)IAqhmJ|IvEHot|PSV7}$9h*+-Z1HqjpjrUdlZdxd|FLl-nT!z^|C78gj zLa2g?gWS&q#!Mh@z(FXKWdx*IfEcJBMZO&>57H0vGZO=<{smBZ4Is?{mPggEQYPi> zGx3=EN2$F@spgVtTUm<}v=n%+?`C;$lcUopchYRA0Z{5c5J21k5%|Vaq$N1TlC@>( zJMWnj=hb=hNQ)v%SmD9+!A4+*?vmm|2$f_!SWT%RiJ21fpLH|Fx=6V022WSf&oynjESM7jEx~T uKPfXOm5m`MH9ax8QiP#AF;lNBGbJ@&FEg(sJ~zK4Ge3_PNi?wx9I^l;fSnBh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7fb82a295e27a7d5b687e0878684c78d69455923 GIT binary patch literal 2648 zcmZ{kc~DbV6vm%1VnirQktJ;L)deZio9#WqAfVz}hT?*|!rLNGA%PH4MzLuth$7Vgz)b&mNp8OHobx;9++^nHN%HzW z7`7Z=O7VC&E)NBG2%gHZJOxjgJj?0fnb#RPoe_Rv;L=ifhB9&%lb+>L5)EAX65hb2 zcInG;hDxJoK7}`CK{rep+La=d%4G2D5S-ELSl-C%b-ZpN9=;r3%jVs+1I=CA1t9Mr(nVQa~)ZoKRCNtK>*EpaC65uoMl`W^hI$Y&rv`rCFL( z(^^i=5~P|T0HpyOqgE@mT0%vHk(5fM31exNR45se;8diNhB+uoK`T{k7^zfR25F=W z3pu!u<+#>BrE)MW0YdO(JX}eH;7j32m7Sd>!LSGn!-l{o8a_Voft$vp@WmZ-bc7#8 zN8j#91QNv>kt?R=9qn=K8^f2Yt4w=>RmyTOZUm@ z)brAz)WeOan7s7aoTse8^`Q7-F@cu{>Lprv+IjK+-3HZwnp{&*^M92 zarur*;Yi0^d~U$&&1VNa+SdM@x%KK;(XoNk&BY#!n9JH2w-bCCzkN$(-nZ#44f|I+ zdW;OM3hjuWUb4D4ze2Y)F1ynA>`(IHE7Y2PmzQFfwyYc_%7}4)lae;$_CZx%K}AQ! zKO>%0{lfj3ymxq20l1Ox|FUehc`zJ{JErglea$8ebGQko0^d#8jaSZcms*p+11W- zqqXtVoZVR-zE<4}I7-|~q6LyG2`<~7+)P3YPOhFHPmnLIYrsRiKKKU*TH}4eH!`XD zmSDKBchqMs_3(6}B3^2>cmh|fc#2f8RKL%44K*Rvy-)u~OqZH(f}=RM{nK}K;Rw$| zBzapd#MbHa`a9;BknAaTc54%Y&uBj0pVrrGxr}hkgMc?|Zlw@?hKbE^MX^%YTA?-8 zvV+Q$#rNX20u<}lv$4}@1=E@kbO36p3;IOU!b1~VWY)5NU~qMhmTpeLq8p>vM;u!v zGNHZTqW-N(0$M|{Z}zN&iWXF1h}fyEOrR_8n6m6nG=du@aohYxh_28Ey+4l{3v%^T zEfd7@rE3ER3@Sl#uuOWfMvxoZtBYRzVDh%Ydw>wEIKf+x5084Y7KMf)=&rUa!MmE) zv5%1q?3m%j&dEwF{LSGbYDv)(`hL!&G5y+*szTKHmUSEe$2YZ|WkbYqx_zgbn!uh6SC%g0OSVvEzKkjs1JiqK}|u zUp9e$v5_`-%49WOaKGF}Ke4fgdAvLZS z-621R55k>|?*&(#aQcZ14QHwit-G@W1AQ-||C#A_U=moiIsA|KNaxlpWSG&5tF*Ku zc;<_L#pnk$s~6ftr=b^rpYMmz=h)z6w%zRKB9E^_bDL*_8*LlR1aW%q;TjaYz=qc? zAFNxo$|SQ+)elff*3EWzcKvfq{l+iux)mpC(Bx^XC~AY<;%YM^t~5 zlC!V`f$408yDv?>Xu+Y*b_H5&nyqrTzT0?r7)ovERrt)r3GD)wu~*^kXU3m0p`B;i Rb!@p5O-st#*FE(z{|8n6=Y0SG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..097ddd162392a6c729c6737e32dc96ceac4228ae GIT binary patch literal 2503 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] [--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; -//! a refusal writes nothing and prints its reason. Sources default to each +//! 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 @@ -71,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())); @@ -135,6 +136,7 @@ fn read_key(path: &str) -> Result { 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(".")); @@ -151,7 +153,7 @@ fn cmd_pack(args: &[String]) -> Result<(), String> { 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").then_some(true) }; + o.overrides = Overrides { royalty_free_only: args.iter().any(|a| a == "--royalty-free-only") }; match opt(args, "--meter").unwrap_or("none") { "none" => {} m => { diff --git a/wai/wai-pack/src/conformance.rs b/wai/wai-pack/src/conformance.rs index bb3870572..aee17c136 100644 --- a/wai/wai-pack/src/conformance.rs +++ b/wai/wai-pack/src/conformance.rs @@ -65,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 } @@ -83,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); } diff --git a/wai/wai-pack/src/golden.rs b/wai/wai-pack/src/golden.rs index b390d1f0f..73d0d8f0e 100644 --- a/wai/wai-pack/src/golden.rs +++ b/wai/wai-pack/src/golden.rs @@ -489,18 +489,25 @@ pub fn case_verdict(c: &Value, files: &Files) -> Result { /// (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` (under the -/// `royalty_free_only` the old `policy.json` states, if any); an index whose +/// `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 states one, is the override the package + // 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)); + 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 }); - files.insert("policy.json".into(), policy.canonical_json().into_bytes()); - let policy_digest = policy.digest(); + 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); @@ -613,12 +620,16 @@ 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), and its files. +/// 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 { @@ -698,7 +709,7 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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 }); + 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. @@ -761,6 +772,19 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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)); @@ -770,6 +794,27 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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)); @@ -861,12 +906,65 @@ pub fn package_mutations(g: &Files, key: &SigningKey) -> Vec { 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 and its reason. +/// 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(); @@ -877,7 +975,14 @@ pub fn package_overlays(g: &Files, key: &SigningKey) -> (Files, Value) { } } let remove: Vec<&String> = g.keys().filter(|p| !m.files.contains_key(*p)).collect(); - cases.push(json!({"name": m.name, "reason": m.reason, "remove": remove})); + 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})) } @@ -897,14 +1002,6 @@ pub fn apply_overlay(g: &Files, overlay: &Files, name: &str, remove: &[String]) f } -/// The first reason the package verifier gives `files`, or `ok`. -pub fn package_verdict(files: &Files) -> String { - match crate::verify::verify_files(files) { - Ok(_) => "ok".into(), - Err(v) => v.into_iter().next().map_or_else(|| "refused".into(), |r| r.code), - } -} - // ─────────────────────────────── version-1 receipts ─────────────────────────────── /// The version-1 receipts and their JSON: the legacy receipt `wai-rs` pins @@ -1069,10 +1166,11 @@ pub fn check(dir: &Path) -> std::io::Result> { 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 remove: Vec = c["remove"].as_array().into_iter().flatten().filter_map(|p| p.as_str().map(String::from)).collect(); - let v = package_verdict(&apply_overlay(&golden, &held, name, &remove)); + 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"])); } diff --git a/wai/wai-pack/src/pack.rs b/wai/wai-pack/src/pack.rs index fe07f774e..b0ec71339 100644 --- a/wai/wai-pack/src/pack.rs +++ b/wai/wai-pack/src/pack.rs @@ -11,7 +11,8 @@ //! (`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). +//! `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: //! @@ -125,6 +126,22 @@ impl PackOutput { } } +/// 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(); 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 index 70322abaf..037d71dd5 100644 --- a/wai/wai-pack/src/verify.rs +++ b/wai/wai-pack/src/verify.rs @@ -7,16 +7,18 @@ //! //! 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 (`unexpected-file`). +//! 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, with `policy.json`'s `royalty_free_only`, is `policy.json` -//! (`policy-json-mismatch`); its ids can name files (`pack-id`); it asks for nothing a -//! packager may not emit (§3.6's reason). +//! 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). @@ -73,8 +75,9 @@ pub struct VerifyReport { /// 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, or a name that is not UTF-8 is -/// refused. +/// 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) }; @@ -84,6 +87,9 @@ pub fn read_strict(root: &std::path::Path) -> Result>, 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}") }; @@ -114,7 +120,7 @@ pub fn verify_dir(dir: &std::path::Path) -> Result> { /// 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 != "classic"); + 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")); @@ -160,7 +166,10 @@ pub fn verify_files(files: &BTreeMap>) -> Result = serde_json::from_slice::(pj).ok().and_then(|v| v.get("royalty_free_only").and_then(|b| b.as_bool())); + // 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"))); diff --git a/wai/wai-pack/tests/packs.rs b/wai/wai-pack/tests/packs.rs index 88910d599..2f665dc5a 100644 --- a/wai/wai-pack/tests/packs.rs +++ b/wai/wai-pack/tests/packs.rs @@ -81,12 +81,13 @@ fn a_repeated_piece_is_one_object() { assert_eq!(pkg.keys().filter(|k| k.starts_with("objects/")).count() as u64, r.unique_objects); } -fn pack_with(policy: &[u8], royalty_free_only: Option) -> Result { +/// 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 = royalty_free_only; + 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) @@ -100,6 +101,13 @@ 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. @@ -107,12 +115,15 @@ fn with_rung(cap: &str, role: &str) -> Vec { 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, None)), "license-refused:wai.video.avc:patent-pool"); - // Without royalty_free_only it is not the licence that stops it. - assert_ne!(code(pack_with(&avc, Some(false))), "license-refused:wai.video.avc:patent-pool"); - assert_eq!(code(pack_with(&with_rung("wai.video.package", "floor"), Some(false))), "capability-not-payload:wai.video.package"); - assert_eq!(code(pack_with(&with_rung("wai.splat.sog", "base"), Some(false))), "capability-candidate:wai.splat.sog"); - assert_eq!(code(pack_with(&with_rung("wai.video.nope", "base"), Some(false))), "capability-unknown:wai.video.nope"); + 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); diff --git a/wai/wai-pack/tests/refusals.rs b/wai/wai-pack/tests/refusals.rs index f33922103..f7c68978b 100644 --- a/wai/wai-pack/tests/refusals.rs +++ b/wai/wai-pack/tests/refusals.rs @@ -1,6 +1,7 @@ //! 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, and the encoder's caps. +//! 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}; @@ -172,3 +173,54 @@ fn the_encoder_refuses_a_piece_past_its_caps() { 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 index 6254b5178..6654d0a28 100644 --- a/wai/wai-pack/tests/review.rs +++ b/wai/wai-pack/tests/review.rs @@ -59,13 +59,14 @@ fn mutations() -> BTreeMap<&'static str, golden::PackageMutation> { golden::package_mutations(&golden_files(), &key()).into_iter().map(|m| (m.name, m)).collect() } -/// Each named case is refused with the reason the corpus gives it, first. +/// 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!(first(&c.files), *reason, "{name}"); + assert_eq!(golden::package_case_verdict(&c.files, &c.empty_dirs, &c.unreadable_dirs).unwrap(), *reason, "{name}"); } } @@ -90,10 +91,12 @@ fn a_package_is_bound_to_its_policy() { ("index-capability", "index-policy-mismatch"), ("index-string-order", "index-policy-mismatch"), ("index-class-plan", "index-policy-mismatch"), - // A class named classic; under royalty_free_only (the policy's, and the - // packer's override as policy.json states it), a patent-pool floor; the policy's - // component of another medium. + // 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"), @@ -153,11 +156,21 @@ fn a_floor_object_is_bound_to_its_profile() { ]); } -/// Item 7: a directory read strictly, dot-names and `*.sha256` files included. +/// 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")]); - // The same cases as directories, read by the verifier's own walk. + 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(); @@ -168,11 +181,11 @@ fn hidden_files_are_refused() { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); std::fs::write(path, b).unwrap(); } - let got = wai_pack::verify::verify_dir(&d).err().map(|v| v[0].code.clone()); - assert_eq!(got.as_deref(), Some("unexpected-file"), "{extra}"); 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); } @@ -243,3 +256,153 @@ fn random_alterations_never_panic() { // 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"); +} -- 2.54.0 (Apple Git-157) From 05783dbb8ddab2ffa8536037e16a9d466f2543e3 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Tue, 6 Oct 2026 09:23:31 -0400 Subject: [PATCH 28/28] =?UTF-8?q?wai.h:=20regenerated=20=E2=80=94=20CHAIN?= =?UTF-8?q?=5FREQUESTED=5FLINKS=20was=20removed=20in=20#171?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage receipts' last round replaced the requested-links set with the freshness edge, but the committed header still declared the constant. cbindgen output from the current tree. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/include/wai.h | 7 ------- 1 file changed, 7 deletions(-) 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). */ -- 2.54.0 (Apple Git-157)