The test named for the writers called manifest_json_within and u32_field directly and never called Wai::to_bytes or WaiMulti::to_bytes, so putting serde_json::to_vec and `as u32` back in both writers left every test green. Both writers now run to_bytes_within(WriteLimits), and to_bytes passes SPEC's limits. The test writes each envelope at limits equal to its manifest's and payloads' lengths, and requires manifest_too_large one byte below the manifest, the field error one byte below a payload, and the sum error when two payloads each fit and their total does not. MANIFEST_MAX_LEN's doc cites staged-delivery Appendix A.3 for the 2^20-unit cap (section 3 bounds a count only by 32 bits), and says the limit bounds the bytes read, not the memory a parse takes. wai.h regenerated. Co-Authored-By: Claude Opus 5.5 <[email protected]>
4512 lines
232 KiB
Rust
4512 lines
232 KiB
Rust
//! The WAI envelope: magic "WAI1" | u32 manifest_len | manifest JSON |
|
||
//! payload bytes. The manifest declares media, intent, the capability
|
||
//! REQUIREMENT (a name the sink resolves against its registered codecs)
|
||
//! and an optional fallback.
|
||
//!
|
||
//! For a *floor/zeroth* capability the name is enough: its bytes have
|
||
//! standardized semantics (a JXL bitstream decodes to the same pixels on
|
||
//! any conformant libjxl), so naming the codec pins the result. For a
|
||
//! capability that **decodes against a parameter set** (SPEC §3.1) the name is
|
||
//! not enough: the payload is meaningless except against a specific set of
|
||
//! networks and tables, and one capability name can front different sets that
|
||
//! reconstruct differently. So the manifest MAY carry a *pin* ([`Prior`]): the
|
||
//! SHA-256 of the whole parameter set, in a declared form, plus how the decode is
|
||
//! made reproducible. The pin names identity, not location — the envelope never
|
||
//! carries the set — and a sink verifies it before it decodes
|
||
//! ([`Wai::dispatch_verified`], [`WaiMulti::pick`]; the digest check itself is
|
||
//! [`crate::pin`]).
|
||
//!
|
||
//! Every optional manifest member is skipped when absent, and members this
|
||
//! revision does not define are kept (`extra`), so an envelope round-trips
|
||
//! through these types without losing what it carried.
|
||
//!
|
||
//! The manifest types are `#[non_exhaustive]`: build them with their
|
||
//! constructors and `with_*` builders ([`Manifest::new`], [`Prior::new`],
|
||
//! [`RenditionMeta::new`], …), so a member a later revision adds does not break
|
||
//! code that builds them.
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use serde_json::{Map, Value};
|
||
use std::fs;
|
||
use std::path::Path;
|
||
|
||
use crate::codecs::{DeclaredTier, DeterminismTier, ParameterSetShape};
|
||
|
||
pub const MAGIC: &[u8; 4] = b"WAI1"; // v1.0 single-payload
|
||
pub const MAGIC_V2: &[u8; 4] = b"WAI2"; // v1.1 multi-rendition
|
||
|
||
/// The longest manifest, in bytes, a reader must accept (SPEC §2, *Reader limits*):
|
||
/// 2^27 (128 MiB). It is sized to the largest manifest a conforming layer has: a `prev`
|
||
/// (staged-delivery §3) that names a different producer for each of the 2^20 units a
|
||
/// `WIR1` layer may cover (staged-delivery Appendix A.3, `n_units ≤ 2^20`), about 90
|
||
/// bytes a run. The reference readers refuse a longer one before they read it, and the
|
||
/// reference writers refuse to write one, so `man_len` never asks a reader to read
|
||
/// more. It bounds the bytes read, not the memory a parse takes: the reference readers
|
||
/// build a JSON tree, which can take some 40 times the manifest's length. The
|
||
/// manifest's nesting has its own limit: the JSON reader refuses 128 nested arrays and
|
||
/// objects and takes 127 (`check_unique_members`).
|
||
pub const MANIFEST_MAX_LEN: usize = 1 << 27;
|
||
|
||
/// SPEC §2, *Reader limits*: `Err` with the code `manifest_too_large` when a manifest
|
||
/// of `len` bytes is longer than [`MANIFEST_MAX_LEN`].
|
||
pub fn check_manifest_len(len: u64) -> Result<(), String> {
|
||
if len > MANIFEST_MAX_LEN as u64 {
|
||
return Err(format!(
|
||
"manifest_too_large: the manifest is {len} bytes; a reader need accept no more than {MANIFEST_MAX_LEN} (SPEC §2)"
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// What a container writer may write: the longest manifest, and the largest length a
|
||
/// `u32` field holds. [`Wai::to_bytes`] and [`WaiMulti::to_bytes`] write within
|
||
/// [`WriteLimits::SPEC`]; a test passes limits it can reach without serialising 128 MiB
|
||
/// or holding 4 GiB.
|
||
#[derive(Clone, Copy, Debug)]
|
||
struct WriteLimits {
|
||
manifest: usize,
|
||
field: u32,
|
||
}
|
||
|
||
impl WriteLimits {
|
||
/// SPEC §2's reader limit for the manifest, and the whole of a `u32`.
|
||
const SPEC: Self = Self { manifest: MANIFEST_MAX_LEN, field: u32::MAX };
|
||
}
|
||
|
||
/// A manifest's JSON as the writers emit it (no insignificant whitespace, members in
|
||
/// order), refused with `manifest_too_large` as soon as it would pass `cap` bytes
|
||
/// ([`MANIFEST_MAX_LEN`] in the writers), so a writer never builds more of it than that.
|
||
fn manifest_json<T: Serialize>(m: &T, cap: usize) -> std::io::Result<Vec<u8>> {
|
||
struct Capped(Vec<u8>, usize);
|
||
impl std::io::Write for Capped {
|
||
fn write(&mut self, b: &[u8]) -> std::io::Result<usize> {
|
||
if self.0.len().saturating_add(b.len()) > self.1 {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, format!(
|
||
"manifest_too_large: the manifest passes {} bytes, the most a reader need accept (SPEC §2)", self.1)));
|
||
}
|
||
self.0.extend_from_slice(b);
|
||
Ok(b.len())
|
||
}
|
||
fn flush(&mut self) -> std::io::Result<()> {
|
||
Ok(())
|
||
}
|
||
}
|
||
let mut w = Capped(Vec::new(), cap);
|
||
serde_json::to_writer(&mut w, m).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
Ok(w.0)
|
||
}
|
||
|
||
/// A length a container writes into a `u32` field (SPEC §2, §7.1), or an error
|
||
/// naming the field when it is past `max` (in the writers, `u32::MAX`: when it does not
|
||
/// fit).
|
||
fn u32_field(len: usize, max: u32, what: &str) -> std::io::Result<u32> {
|
||
u32::try_from(len).ok().filter(|&l| l <= max).ok_or_else(|| std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData, format!("{what} is {len} bytes, past the u32 field that holds it")))
|
||
}
|
||
|
||
fn is_none<T>(v: &Option<T>) -> bool {
|
||
v.is_none()
|
||
}
|
||
|
||
/// `model_requirement` (SPEC §3): the capability a sink must resolve, and an
|
||
/// optional fallback. Build one with [`ModelRequirement::new`].
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct ModelRequirement {
|
||
pub capability: String,
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub fallback: Option<String>,
|
||
/// Behavioral conformance threshold for the *perceptual* (semantic /
|
||
/// generative) path: the file requires a sink whose regenerated output
|
||
/// scores at least this similarity against the transmitted understanding,
|
||
/// in the embedding space [`Self::sim_metric`] names. Absent for floor and
|
||
/// bit-exact capabilities.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub min_sim: Option<f32>,
|
||
/// The embedding space `min_sim` is measured in. Absent ⇒ `"clip"` by
|
||
/// convention.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub sim_metric: Option<String>,
|
||
/// Members this revision does not define, kept so they survive a round trip.
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl ModelRequirement {
|
||
/// A requirement for `capability`, with no fallback and no perceptual threshold.
|
||
pub fn new(capability: impl Into<String>) -> Self {
|
||
Self { capability: capability.into(), fallback: None, min_sim: None, sim_metric: None, extra: Map::new() }
|
||
}
|
||
/// Declare a fallback capability (SPEC §4 step 5).
|
||
pub fn with_fallback(mut self, fallback: impl Into<String>) -> Self {
|
||
self.fallback = Some(fallback.into());
|
||
self
|
||
}
|
||
/// Declare a perceptual similarity threshold.
|
||
pub fn with_min_sim(mut self, min_sim: f32) -> Self {
|
||
self.min_sim = Some(min_sim);
|
||
self
|
||
}
|
||
/// Name the embedding space of the similarity threshold.
|
||
pub fn with_sim_metric(mut self, metric: impl Into<String>) -> Self {
|
||
self.sim_metric = Some(metric.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
/// The parameter set a pin's set **replaces** (SPEC §3.1, `supersedes`), named as a
|
||
/// pin names one. Informative: it changes no decision, only the reason a sink that
|
||
/// holds the predecessor reports (`digest_superseded`). A successor never satisfies
|
||
/// its predecessor's pin.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct Supersedes {
|
||
/// The predecessor's SHA-256, in its form, as 64 lowercase hexadecimal digits.
|
||
pub sha256: String,
|
||
/// The predecessor's declared form, when it was pinned in one.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub form: Option<String>,
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl Supersedes {
|
||
/// The predecessor, pinned form-less by `sha256`.
|
||
pub fn new(sha256: impl Into<String>) -> Self {
|
||
Self { sha256: sha256.into(), form: None, extra: Map::new() }
|
||
}
|
||
/// The predecessor's declared form.
|
||
pub fn with_form(mut self, form: impl Into<String>) -> Self {
|
||
self.form = Some(form.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
/// The **pin** (SPEC §3.1): the identity of the parameter set a payload was coded
|
||
/// against — the SHA-256 of the whole set, in its declared form — plus how the
|
||
/// decode is made reproducible. Carried as `conditioning.prior` in a `WAI1`
|
||
/// manifest and as a rendition entry's `prior` in a `WAI2` one.
|
||
///
|
||
/// ```
|
||
/// use wai::codecs::{DeclaredTier, CAP_PRIOR_SET};
|
||
/// use wai::container::Prior;
|
||
///
|
||
/// let legacy = Prior::new("int_hyper/q4", "ef".repeat(32), "int-rans/p16+int-synth");
|
||
/// // A pin written before `tier` existed reads its tier from `determinism`.
|
||
/// assert_eq!(legacy.declared_tier(), Some(wai::codecs::DeterminismTier::DecodeEquivalence));
|
||
/// assert!(legacy.is_cross_hardware_exact());
|
||
/// let set = Prior::new("int_mlicpp/a", "85".repeat(32), "int-rans/p16+int-mlicpp/i64")
|
||
/// .with_form(CAP_PRIOR_SET)
|
||
/// .with_tier(DeclaredTier::DecodeEquivalence);
|
||
/// assert_eq!(set.tier(), Some("decode-equivalence"));
|
||
/// assert!(set.is_cross_hardware_exact());
|
||
/// ```
|
||
///
|
||
/// `#[non_exhaustive]`: build one with [`Prior::new`] and the `with_*` builders.
|
||
/// Every optional member is skipped when absent, so a pin written without one keeps
|
||
/// its bytes, and members this revision does not define are kept in `extra`.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct Prior {
|
||
/// A name for people and logs. A sink never selects, resolves or accepts a
|
||
/// parameter set by it.
|
||
pub id: String,
|
||
/// The SHA-256 of the parameter set in its form (SPEC §3.1), as 64 lowercase
|
||
/// hexadecimal digits: with no `form`, of the one file that holds the set;
|
||
/// in the form `wai.prior.set`, of the set's listing.
|
||
pub sha256: String,
|
||
/// How the decode is made reproducible, in words, e.g.
|
||
/// `"int-rans/p16+int-mlicpp/i64"`. Informative, except that SPEC §7 reads it
|
||
/// for a pin without `tier` ([`Prior::determinism_class`]).
|
||
pub determinism: String,
|
||
/// The registered prior form the digest covers
|
||
/// ([`crate::codecs::prior_form::prior_forms`]). Absent: the digest covers the
|
||
/// one file that holds the set, which SPEC §3.1 admits only for a capability
|
||
/// registered with a one-file set.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub form: Option<String>,
|
||
/// The reproducibility tier the sender claims for the decode: a token of the
|
||
/// closed vocabulary [`crate::codecs::TIER_TOKENS`]. A token this revision does
|
||
/// not register reads as `none`. Set with [`Prior::with_tier`].
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub tier: Option<String>,
|
||
/// The parameter set this one replaces (informative, SPEC §3.1).
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub supersedes: Option<Supersedes>,
|
||
/// Members this revision does not define, kept so they survive a round trip.
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
/// Why a [`Prior`]'s declared form cannot be used at this sink. Either way the
|
||
/// pin is refused: a sink never resolves a pin against an artifact in a form
|
||
/// other than the one declared, even one whose unpacked parameters compare equal.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum PriorFormError {
|
||
/// The declared form is not a registered prior form (a payload capability,
|
||
/// or a form from a registry this sink does not know).
|
||
NotAPriorForm(String),
|
||
/// A registered prior form this sink has no reader for.
|
||
Unreadable(String),
|
||
}
|
||
|
||
impl std::fmt::Display for PriorFormError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
Self::NotAPriorForm(form) => write!(f, "declared prior form '{form}' is not a registered prior form"),
|
||
Self::Unreadable(form) => write!(f, "declared prior form '{form}' cannot be read by this sink"),
|
||
}
|
||
}
|
||
}
|
||
impl std::error::Error for PriorFormError {}
|
||
|
||
/// How reproducible a pin's decode is, read from its `determinism` words. This is
|
||
/// the **legacy reading** SPEC §7 fixes for a pin without `tier`: a pin declares
|
||
/// `decode-equivalence` exactly when its `determinism`, in ASCII lower case,
|
||
/// contains `int`, `fixed` or `integer` and none of `f32`, `f64` and `float`. It
|
||
/// reads a declaration and can misread one; the registry class, checked first, is
|
||
/// what guarantees the arithmetic.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum DeterminismClass {
|
||
/// The words name integer-only inference: reads as `decode-equivalence`.
|
||
IntegerExact,
|
||
/// The words name a float stage: reproducible on the same build at best.
|
||
FloatReproducible,
|
||
/// Unrecognized words: no tier.
|
||
Unknown,
|
||
}
|
||
|
||
/// Whether `s` is 64 lowercase hexadecimal digits — the grammar of every SHA-256
|
||
/// WAI writes (SPEC §7 *Digests*).
|
||
pub fn is_sha256_hex(s: &str) -> bool {
|
||
s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
|
||
}
|
||
|
||
impl Prior {
|
||
/// A pin by `id`, the `sha256` of the one file that holds the set, and its
|
||
/// `determinism` words, with every optional member absent. It serializes
|
||
/// exactly as a pin written before the optional members existed.
|
||
pub fn new(
|
||
id: impl Into<String>,
|
||
sha256: impl Into<String>,
|
||
determinism: impl Into<String>,
|
||
) -> Self {
|
||
Self {
|
||
id: id.into(),
|
||
sha256: sha256.into(),
|
||
determinism: determinism.into(),
|
||
form: None,
|
||
tier: None,
|
||
supersedes: None,
|
||
extra: Map::new(),
|
||
}
|
||
}
|
||
|
||
/// The same pin, declaring the form its digest covers ([`Prior::form`]). The
|
||
/// form is not checked here; see [`crate::pin::verify_bytes`].
|
||
pub fn with_form(mut self, form: impl Into<String>) -> Self {
|
||
self.form = Some(form.into());
|
||
self
|
||
}
|
||
|
||
/// The same pin, stating the tier the sender claims. The tier is typed, so this
|
||
/// crate writes only a registered token.
|
||
pub fn with_tier(mut self, tier: DeclaredTier) -> Self {
|
||
self.tier = Some(tier.token().to_string());
|
||
self
|
||
}
|
||
|
||
/// The same pin, naming the set it replaces.
|
||
pub fn with_supersedes(mut self, predecessor: Supersedes) -> Self {
|
||
self.supersedes = Some(predecessor);
|
||
self
|
||
}
|
||
|
||
/// The declared form, if any.
|
||
pub fn form(&self) -> Option<&str> {
|
||
self.form.as_deref()
|
||
}
|
||
|
||
/// The stated `tier` token, if any — registered or not.
|
||
pub fn tier(&self) -> Option<&str> {
|
||
self.tier.as_deref()
|
||
}
|
||
|
||
/// The stated predecessor, if any — well-formed or not
|
||
/// ([`Prior::supersedes_finding`]).
|
||
pub fn supersedes(&self) -> Option<&Supersedes> {
|
||
self.supersedes.as_ref()
|
||
}
|
||
|
||
/// Whether `tier` is absent or a registered token. A verifier reports an
|
||
/// unregistered one (`tier_unregistered`); a sink reads it as `none`.
|
||
pub fn tier_is_registered(&self) -> bool {
|
||
self.tier().is_none_or(|t| DeclaredTier::from_token(t).is_some())
|
||
}
|
||
|
||
/// The tier this pin declares (SPEC §7): its `tier` when present (an
|
||
/// unregistered token, or `none`, declares none); without `tier`, the legacy
|
||
/// reading of `determinism` ([`Prior::determinism_class`]):
|
||
/// `decode-equivalence` when it reads integer-exact, none otherwise.
|
||
pub fn declared_tier(&self) -> Option<DeterminismTier> {
|
||
match self.tier() {
|
||
Some(t) => DeterminismTier::from_token(t),
|
||
None => (self.determinism_class() == DeterminismClass::IntegerExact)
|
||
.then_some(DeterminismTier::DecodeEquivalence),
|
||
}
|
||
}
|
||
|
||
/// The legacy reading of `determinism` (see [`DeterminismClass`]). It is what a
|
||
/// pin without `tier` declares, frozen so a pin written before `tier` keeps its
|
||
/// verdict. So `"int-rans/p16+onnx-cpu-f32/1thread"` (integer entropy, float
|
||
/// synthesis) reads as [`DeterminismClass::FloatReproducible`].
|
||
pub fn determinism_class(&self) -> DeterminismClass {
|
||
let d = self.determinism.to_ascii_lowercase();
|
||
let floaty = d.contains("f32") || d.contains("f64") || d.contains("float");
|
||
let integer = d.contains("int") || d.contains("fixed") || d.contains("integer");
|
||
match (integer, floaty) {
|
||
(true, false) => DeterminismClass::IntegerExact,
|
||
(_, true) => DeterminismClass::FloatReproducible,
|
||
(false, false) => DeterminismClass::Unknown,
|
||
}
|
||
}
|
||
|
||
/// True iff this pin backs decode-equivalence: it declares
|
||
/// `decode-equivalence` ([`Prior::declared_tier`]) and its form, if any, is
|
||
/// one the registry lists as established exact
|
||
/// ([`Prior::form_preserves_exactness`]). The form rule lives here so every
|
||
/// `replicate` check that reads this predicate applies it.
|
||
pub fn is_cross_hardware_exact(&self) -> bool {
|
||
self.declared_tier() == Some(DeterminismTier::DecodeEquivalence)
|
||
&& self.form_preserves_exactness()
|
||
}
|
||
|
||
/// Whether reading the pinned set out of its declared form is established to
|
||
/// hand every conforming sink the same parameters: no form (the digest covers
|
||
/// the bytes), or a form the registry lists
|
||
/// ([`form_reconstruction_is_exact`](crate::codecs::prior_form::form_reconstruction_is_exact):
|
||
/// `wai.prior.set`; no coded form).
|
||
pub fn form_preserves_exactness(&self) -> bool {
|
||
self.form().is_none_or(crate::codecs::prior_form::form_reconstruction_is_exact)
|
||
}
|
||
|
||
/// Check the declared form: it must be a registered prior form, and one the
|
||
/// sink can read. An undeclared form passes.
|
||
pub fn check_form(&self, readable: &[&str]) -> Result<(), PriorFormError> {
|
||
match self.form() {
|
||
None => Ok(()),
|
||
Some(f) if !crate::codecs::prior_form::is_prior_form(f) => {
|
||
Err(PriorFormError::NotAPriorForm(f.to_string()))
|
||
}
|
||
Some(f) if !readable.contains(&f) => Err(PriorFormError::Unreadable(f.to_string())),
|
||
Some(_) => Ok(()),
|
||
}
|
||
}
|
||
|
||
/// SPEC §3.1 check 1: `sha256` is 64 lowercase hexadecimal digits.
|
||
pub fn check_syntax(&self) -> Result<(), PinRefusal> {
|
||
if is_sha256_hex(&self.sha256) {
|
||
Ok(())
|
||
} else {
|
||
Err(PinRefusal::Malformed {
|
||
why: format!("sha256 '{}' is not 64 lowercase hexadecimal digits", self.sha256),
|
||
})
|
||
}
|
||
}
|
||
|
||
/// Why `supersedes` is malformed, if it is (`supersedes_malformed`): its
|
||
/// `sha256` is not 64 lowercase hex, equals the pin's own, or its `form` is not
|
||
/// a registered prior form. A malformed `supersedes` is a static finding only:
|
||
/// a sink ignores the member, and never reports `digest_superseded` for it.
|
||
pub fn supersedes_finding(&self) -> Option<String> {
|
||
let s = self.supersedes()?;
|
||
if !is_sha256_hex(&s.sha256) {
|
||
return Some(format!("supersedes.sha256 '{}' is not 64 lowercase hexadecimal digits", s.sha256));
|
||
}
|
||
if s.sha256 == self.sha256 {
|
||
return Some("supersedes.sha256 equals the pin's own sha256".into());
|
||
}
|
||
if let Some(f) = s.form.as_deref() {
|
||
if !crate::codecs::prior_form::is_prior_form(f) {
|
||
return Some(format!("supersedes.form '{f}' is not a registered prior form"));
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The predecessor a sink may act on: `supersedes` when it is well-formed.
|
||
pub fn usable_supersedes(&self) -> Option<&Supersedes> {
|
||
self.supersedes_finding().is_none().then(|| self.supersedes()).flatten()
|
||
}
|
||
}
|
||
|
||
/// Why a sink refuses a pin (SPEC §3.1). The checks run in order, and the first
|
||
/// that fails is reported: `pin_malformed`, `form_not_registered`,
|
||
/// `form_unreadable`, `form_required`, `prior_revoked` or `prior_not_authorised`
|
||
/// (check 5, run only by a sink that holds the capability's pinned sets to
|
||
/// jwp-receipts §7.9, [`crate::pin::PinAuthority`]), `prior_not_held` or
|
||
/// `digest_superseded`, `set_malformed`, `part_mismatch`. `set_incomplete` is
|
||
/// raised during a pinned decode. Each makes the capability unsupported for that envelope at that sink;
|
||
/// none is a reason to decode anyway.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum PinRefusal {
|
||
/// Check 1: `sha256` is not 64 lowercase hexadecimal digits.
|
||
Malformed { why: String },
|
||
/// Check 2: `form` names no registered prior form.
|
||
FormNotRegistered { form: String },
|
||
/// Check 3: `form` is registered and this sink has no reader for it — the
|
||
/// sink's gap, never reported as a mismatch.
|
||
FormUnreadable { form: String },
|
||
/// Check 4: `form` is absent and `capability` is not registered with a
|
||
/// one-file set.
|
||
FormRequired { capability: String },
|
||
/// Check 5: the sink holds `capability`'s pinned sets to jwp-receipts §7.9, and a
|
||
/// key log it holds revokes the set's digest (§7.9 check 5). `revoked_by` names the
|
||
/// revoking origin where it is not the one whose grant was checked.
|
||
Revoked { capability: String, revoked_by: Option<String> },
|
||
/// Check 5: the sink holds `capability`'s pinned sets to jwp-receipts §7.9, and §7.9
|
||
/// does not hold the set for a reason other than revocation. `check` and `reason`
|
||
/// are §7.9's (for example `4` and `module_status_stale`: no fresh status, so the
|
||
/// sink is offline or has no trusted clock), or `None` when the sink holds no
|
||
/// grant of form `decoder` for the set.
|
||
NotAuthorised { capability: String, check: Option<&'static str>, reason: Option<&'static str> },
|
||
/// Check 6: the sink holds no parameter set for `capability` whose digest, in
|
||
/// the pin's form, equals the pin.
|
||
NotHeld { capability: String },
|
||
/// Check 6, refined: the sink holds the set the pin's `supersedes` names.
|
||
Superseded { capability: String, predecessor: String },
|
||
/// Check 7: the listing whose digest matches breaks a listing rule.
|
||
SetMalformed { why: String },
|
||
/// Check 8: in every held copy of the matching listing, a file it names is
|
||
/// missing, unreadable, or has another digest.
|
||
PartMismatch { path: String },
|
||
/// During a pinned decode: the decode needs `path`, which the verified set
|
||
/// does not contain.
|
||
SetIncomplete { path: String },
|
||
}
|
||
|
||
/// Every pin refusal reason, in the order SPEC §3.1 checks them (`set_incomplete`
|
||
/// last: it is raised during a decode, not by verification).
|
||
pub const PIN_REFUSAL_CODES: &[&str] = &[
|
||
"pin_malformed",
|
||
"form_not_registered",
|
||
"form_unreadable",
|
||
"form_required",
|
||
"prior_revoked",
|
||
"prior_not_authorised",
|
||
"prior_not_held",
|
||
"digest_superseded",
|
||
"set_malformed",
|
||
"part_mismatch",
|
||
"set_incomplete",
|
||
];
|
||
|
||
impl PinRefusal {
|
||
/// The reason SPEC §3.1 names (snake_case).
|
||
pub fn code(&self) -> &'static str {
|
||
match self {
|
||
Self::Malformed { .. } => "pin_malformed",
|
||
Self::FormNotRegistered { .. } => "form_not_registered",
|
||
Self::FormUnreadable { .. } => "form_unreadable",
|
||
Self::FormRequired { .. } => "form_required",
|
||
Self::Revoked { .. } => "prior_revoked",
|
||
Self::NotAuthorised { .. } => "prior_not_authorised",
|
||
Self::NotHeld { .. } => "prior_not_held",
|
||
Self::Superseded { .. } => "digest_superseded",
|
||
Self::SetMalformed { .. } => "set_malformed",
|
||
Self::PartMismatch { .. } => "part_mismatch",
|
||
Self::SetIncomplete { .. } => "set_incomplete",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for PinRefusal {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
Self::Malformed { why } => write!(f, "malformed pin: {why}"),
|
||
Self::FormNotRegistered { form } => write!(f, "declared prior form '{form}' is not a registered prior form"),
|
||
Self::FormUnreadable { form } => write!(f, "declared prior form '{form}' cannot be read by this sink"),
|
||
Self::FormRequired { capability } => write!(
|
||
f, "{capability} is not registered with a one-file parameter set, so a pin over it must declare a form (wai.prior.set)"),
|
||
Self::Revoked { capability, revoked_by } => {
|
||
write!(f, "the pinned parameter set for {capability} is revoked by a key log this sink holds (jwp-receipts §7.9 check 5)")?;
|
||
match revoked_by {
|
||
Some(o) => write!(f, ", in {o}'s"),
|
||
None => Ok(()),
|
||
}
|
||
}
|
||
Self::NotAuthorised { capability, check: Some(c), reason: Some(r) } => write!(
|
||
f, "the pinned parameter set for {capability} is not held under jwp-receipts §7.9: check {c}, {r}"),
|
||
Self::NotAuthorised { capability, .. } => write!(
|
||
f, "the pinned parameter set for {capability} is not held under jwp-receipts §7.9: this sink holds no grant for it"),
|
||
Self::NotHeld { capability } => write!(
|
||
f, "this sink holds no parameter set for {capability} whose digest equals the pin"),
|
||
Self::Superseded { capability, predecessor } => write!(
|
||
f, "this sink holds only the parameter set {predecessor} for {capability}, which the pinned set replaces"),
|
||
Self::SetMalformed { why } => write!(f, "the pinned listing is malformed: {why}"),
|
||
Self::PartMismatch { path } => write!(
|
||
f, "file '{path}' of the pinned set is missing, unreadable, or has another digest"),
|
||
Self::SetIncomplete { path } => write!(
|
||
f, "the decode needs '{path}', which the verified parameter set does not contain"),
|
||
}
|
||
}
|
||
}
|
||
impl std::error::Error for PinRefusal {}
|
||
|
||
impl From<PriorFormError> for PinRefusal {
|
||
fn from(e: PriorFormError) -> Self {
|
||
match e {
|
||
PriorFormError::NotAPriorForm(form) => Self::FormNotRegistered { form },
|
||
PriorFormError::Unreadable(form) => Self::FormUnreadable { form },
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Whether a pin applies to `cap` (SPEC §3.1, *Where a pin sits*): `cap` decodes
|
||
/// against a parameter set, or this revision does not register it — a sink that
|
||
/// advertises an unregistered capability and finds a pin on it verifies the pin as
|
||
/// several files, so it fails closed.
|
||
pub fn pin_applies_to(cap: &str) -> bool {
|
||
crate::codecs::decodes_against_parameter_set(cap) || !crate::codecs::is_known_capability(cap)
|
||
}
|
||
|
||
/// What the pin rule needs of `cap` before any file is hashed: the shape a pin over
|
||
/// it must take. An unregistered capability is treated as several files.
|
||
pub fn pin_shape(cap: &str) -> ParameterSetShape {
|
||
crate::codecs::parameter_set_shape(cap).unwrap_or(ParameterSetShape::SeveralFiles)
|
||
}
|
||
|
||
/// Why a `(intent, pin)` pair does not make a sound `replicate` claim over a
|
||
/// `NeuralIntegerExact` capability (SPEC §7). See [`replicate_prior_refusal`].
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum PriorRefusal {
|
||
/// `replicate` with no pin (`replicate_unpinned`).
|
||
Absent,
|
||
/// A pin without `tier` whose `determinism` does not read as integer-exact
|
||
/// (`replicate_tier`).
|
||
NotIntegerExact(DeterminismClass),
|
||
/// A pin whose `tier` (named here) is not `decode-equivalence`
|
||
/// (`replicate_tier`).
|
||
TierNotDecodeEquivalence(String),
|
||
/// A pin that declares a form (named here) the registry does not list as
|
||
/// established exact, or that is not a registered prior form at all
|
||
/// (`replicate_form_not_exact`).
|
||
FormNotExact(String),
|
||
}
|
||
|
||
impl PriorRefusal {
|
||
/// The reason SPEC §7 names.
|
||
pub fn code(&self) -> &'static str {
|
||
match self {
|
||
Self::Absent => "replicate_unpinned",
|
||
Self::NotIntegerExact(_) | Self::TierNotDecodeEquivalence(_) => "replicate_tier",
|
||
Self::FormNotExact(_) => "replicate_form_not_exact",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for PriorRefusal {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
const FLOAT: &str = "float decode is not byte-identical across hardware — tag this \
|
||
perceptual/improve, or pin an integer-only prior";
|
||
match self {
|
||
Self::Absent => write!(f, "no prior is pinned; {FLOAT}"),
|
||
Self::NotIntegerExact(class) => {
|
||
write!(f, "the prior is not integer-exact (determinism {class:?}); {FLOAT}")
|
||
}
|
||
Self::TierNotDecodeEquivalence(tier) => write!(
|
||
f,
|
||
"the pin declares tier '{tier}', not decode-equivalence; {FLOAT}"
|
||
),
|
||
Self::FormNotExact(form) if !crate::codecs::prior_form::is_prior_form(form) => write!(
|
||
f,
|
||
"the prior's contract is integer-exact, but its declared form '{form}' is not a \
|
||
registered prior form, so nothing states what reading the artifact out of it \
|
||
reconstructs — tag this perceptual/improve, or pin the set in the form \
|
||
wai.prior.set"
|
||
),
|
||
Self::FormNotExact(form) => write!(
|
||
f,
|
||
"the prior's contract is integer-exact, but its declared form '{form}' is not \
|
||
established to reconstruct identical tensors at every reader, so two sinks can \
|
||
read different weights out of it — tag this perceptual/improve, or pin the set \
|
||
in the form wai.prior.set"
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The pin half of SPEC §7's replicate rule, with its reason: `None` exactly when
|
||
/// [`replicate_is_sound`] holds for `(intent, prior)` (or `intent` is not
|
||
/// `replicate`). In order: no pin (`Absent`); a `tier` other than
|
||
/// `decode-equivalence` (`TierNotDecodeEquivalence`); with no `tier`, a
|
||
/// `determinism` that does not read as integer-exact (`NotIntegerExact`); a form
|
||
/// not established exact (`FormNotExact`). This reads the sender's declaration
|
||
/// only; [`crate::codecs::replicate_refusal`] checks the registry class first.
|
||
pub fn replicate_prior_refusal(intent: &str, prior: Option<&Prior>) -> Option<PriorRefusal> {
|
||
if intent != "replicate" {
|
||
return None;
|
||
}
|
||
let p = match prior {
|
||
None => return Some(PriorRefusal::Absent),
|
||
Some(p) => p,
|
||
};
|
||
match p.tier() {
|
||
Some(t) if DeterminismTier::from_token(t) != Some(DeterminismTier::DecodeEquivalence) => {
|
||
return Some(PriorRefusal::TierNotDecodeEquivalence(t.to_string()));
|
||
}
|
||
Some(_) => {}
|
||
None if p.determinism_class() != DeterminismClass::IntegerExact => {
|
||
return Some(PriorRefusal::NotIntegerExact(p.determinism_class()));
|
||
}
|
||
None => {}
|
||
}
|
||
if !p.form_preserves_exactness() {
|
||
return Some(PriorRefusal::FormNotExact(p.form().unwrap_or_default().to_string()));
|
||
}
|
||
None
|
||
}
|
||
|
||
/// The pin half of SPEC §7: whether `(intent, prior)` declares what a `replicate`
|
||
/// over a `NeuralIntegerExact` capability needs. [`Wai::replicate_is_sound`] and
|
||
/// [`crate::codecs::replicate_refusal`] apply the registry class too.
|
||
pub fn replicate_is_sound(intent: &str, prior: Option<&Prior>) -> bool {
|
||
replicate_prior_refusal(intent, prior).is_none()
|
||
}
|
||
|
||
/// A finding a verifier makes about a manifest's pins without holding any
|
||
/// parameter set ([`Wai::check_pin`], [`WaiMulti::check_pins`]).
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct PinFinding {
|
||
/// The reason: `prior_not_applicable`, `pin_malformed`, `tier_unregistered`,
|
||
/// `supersedes_malformed`, `form_not_registered`, `form_required`,
|
||
/// `tier_exceeds_class`, or a `replicate_*` reason.
|
||
pub code: &'static str,
|
||
pub detail: String,
|
||
}
|
||
|
||
impl std::fmt::Display for PinFinding {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "{}: {}", self.code, self.detail)
|
||
}
|
||
}
|
||
|
||
/// Every static pin finding, in the order a verifier checks them.
|
||
pub const PIN_FINDING_CODES: &[&str] = &[
|
||
"prior_not_applicable",
|
||
"pin_malformed",
|
||
"tier_unregistered",
|
||
"supersedes_malformed",
|
||
"form_not_registered",
|
||
"form_required",
|
||
"tier_exceeds_class",
|
||
"replicate_unpinned",
|
||
"replicate_tier",
|
||
"replicate_form_not_exact",
|
||
];
|
||
|
||
/// The static pin findings for a pin that would apply to the capabilities `caps`
|
||
/// (the first is the one decoded through first), under `intent`.
|
||
fn static_pin_findings(caps: &[&str], prior: Option<&Prior>, intent: &str) -> Result<(), PinFinding> {
|
||
let finding = |code: &'static str, detail: String| Err(PinFinding { code, detail });
|
||
if let Some(p) = prior {
|
||
let applies: Vec<&str> = caps.iter().copied().filter(|c| pin_applies_to(c)).collect();
|
||
if applies.is_empty() {
|
||
return finding("prior_not_applicable", format!(
|
||
"a pin over {} means nothing: no capability it applies to decodes against a parameter set",
|
||
caps.join(", ")));
|
||
}
|
||
if let Err(r) = p.check_syntax() {
|
||
return finding("pin_malformed", r.to_string());
|
||
}
|
||
if !p.tier_is_registered() {
|
||
return finding("tier_unregistered", format!(
|
||
"tier '{}' is not a registered token; a sink reads it as none", p.tier().unwrap_or_default()));
|
||
}
|
||
if let Some(why) = p.supersedes_finding() {
|
||
return finding("supersedes_malformed", why);
|
||
}
|
||
if let Some(f) = p.form() {
|
||
if !crate::codecs::prior_form::is_prior_form(f) {
|
||
return finding("form_not_registered", PinRefusal::FormNotRegistered { form: f.into() }.to_string());
|
||
}
|
||
} else if let Some(c) = applies.iter().find(|c| pin_shape(c) != ParameterSetShape::OneFile) {
|
||
return finding("form_required", PinRefusal::FormRequired { capability: c.to_string() }.to_string());
|
||
}
|
||
if let Some(c) = applies.iter().find(|c| crate::codecs::tier_exceeds_class(c, p)) {
|
||
return finding("tier_exceeds_class", format!(
|
||
"tier '{}' claims more than {c}'s registry class gives ({})",
|
||
p.tier().unwrap_or_default(),
|
||
crate::codecs::class_tier(c).map(DeclaredTier::token).unwrap_or("none")));
|
||
}
|
||
}
|
||
if let Some(&first) = caps.first() {
|
||
if let Some(crate::codecs::ReplicateRefusal::Prior(r)) =
|
||
crate::codecs::replicate_refusal(first, intent, prior)
|
||
{
|
||
return finding(r.code(), r.to_string());
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// The `conditioning` object (SPEC §3). Build one with [`Conditioning::new`].
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct Conditioning {
|
||
pub kind: String,
|
||
/// The pin (SPEC §3.1) of the parameter set the payload was coded against. It
|
||
/// applies to whichever of the capability and the fallback decodes the payload,
|
||
/// where that capability decodes against a parameter set.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub prior: Option<Prior>,
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl Conditioning {
|
||
/// Conditioning of `kind`, with no pin.
|
||
pub fn new(kind: impl Into<String>) -> Self {
|
||
Self { kind: kind.into(), prior: None, extra: Map::new() }
|
||
}
|
||
/// Pin the parameter set the payload was coded against.
|
||
pub fn with_prior(mut self, prior: Prior) -> Self {
|
||
self.prior = Some(prior);
|
||
self
|
||
}
|
||
}
|
||
|
||
/// The `security` object (SPEC §6.1). Absent, `null`, or `encryption: "none"`
|
||
/// means the payload is plaintext.
|
||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct Security {
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub encryption: Option<String>,
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub kid: Option<u64>,
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl Security {
|
||
/// `security` naming an encryption scheme and a key id.
|
||
pub fn new(encryption: impl Into<String>, kid: u64) -> Self {
|
||
Self { encryption: Some(encryption.into()), kid: Some(kid), extra: Map::new() }
|
||
}
|
||
/// Whether the payload is plaintext (`encryption` absent or `"none"`).
|
||
pub fn is_plaintext(&self) -> bool {
|
||
self.encryption.as_deref().is_none_or(|e| e == "none")
|
||
}
|
||
}
|
||
|
||
/// SPEC §6.4 step 2 at a sink that implements no encryption scheme, as the reference
|
||
/// dispatchers are: a manifest whose `security` declares an encryption other than
|
||
/// `"none"` makes the envelope inert here, and no ciphertext reaches a codec. A sink
|
||
/// that opens a scheme decrypts first and dispatches the plaintext envelope (§6.4
|
||
/// step 3), whose `security` is plaintext. `declared` is the encryption as written.
|
||
fn encryption_inert(declared: impl Into<String>) -> Inert {
|
||
Inert::Encrypted { encryption: declared.into() }
|
||
}
|
||
|
||
/// A `WAI1` manifest (SPEC §3). Build one with [`Manifest::new`].
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct Manifest {
|
||
pub wai: String,
|
||
pub media: String,
|
||
pub intent: String,
|
||
pub model_requirement: ModelRequirement,
|
||
pub conditioning: Conditioning,
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub security: Option<Security>,
|
||
/// `refines` (`extensions/staged-delivery.md` §3): what a refinement-only
|
||
/// capability's payload refines, by content hash. Kept as the JSON it is, so a
|
||
/// malformed value still parses ([`Refines::from_value`] reports it,
|
||
/// `refines-malformed`).
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub refines: Option<Value>,
|
||
/// `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<Value>,
|
||
#[serde(default)]
|
||
pub target: Value,
|
||
/// Members this revision does not define, kept so they survive a round trip.
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl Manifest {
|
||
/// A `"1.0"` manifest with a `null` target and no `security`.
|
||
pub fn new(
|
||
media: impl Into<String>,
|
||
intent: impl Into<String>,
|
||
model_requirement: ModelRequirement,
|
||
conditioning: Conditioning,
|
||
) -> Self {
|
||
Self {
|
||
wai: "1.0".into(),
|
||
media: media.into(),
|
||
intent: intent.into(),
|
||
model_requirement,
|
||
conditioning,
|
||
security: None,
|
||
refines: None,
|
||
derived_from: None,
|
||
target: Value::Null,
|
||
extra: Map::new(),
|
||
}
|
||
}
|
||
/// Set `target`.
|
||
pub fn with_target(mut self, target: Value) -> Self {
|
||
self.target = target;
|
||
self
|
||
}
|
||
/// Set `refines` (`extensions/staged-delivery.md` §3).
|
||
pub fn with_refines(mut self, refines: &Refines) -> Self {
|
||
self.refines = Some(refines.to_value());
|
||
self
|
||
}
|
||
/// Set `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<String>) -> Self {
|
||
self.wai = wai.into();
|
||
self
|
||
}
|
||
/// Set `security`.
|
||
pub fn with_security(mut self, security: Security) -> Self {
|
||
self.security = Some(security);
|
||
self
|
||
}
|
||
}
|
||
|
||
/// How the hashes of a `refines` member name objects: a content hash
|
||
/// (`jwp-receipts` §2.1, 64 lowercase hexadecimal digits) in a `WAI1` manifest, a
|
||
/// component `id` ([`is_component_id`]) in a `WAI2` entry, which names the entry
|
||
/// whose exploded envelope is the object (SPEC §7.1).
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub enum RefForm {
|
||
ContentHash,
|
||
ComponentId,
|
||
}
|
||
|
||
impl RefForm {
|
||
fn accepts(self, s: &str) -> bool {
|
||
match self {
|
||
RefForm::ContentHash => s.len() == 64 && s.bytes().all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)),
|
||
RefForm::ComponentId => is_component_id(s),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// `refines.prev` (`extensions/staged-delivery.md` §3): the producer each unit of
|
||
/// the span is expected to have — one for every unit, or runs in span order.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum Prev {
|
||
One(String),
|
||
Runs(Vec<PrevRun>),
|
||
}
|
||
|
||
/// `count` consecutive units of a span whose producer is `producer`.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct PrevRun {
|
||
pub producer: String,
|
||
pub count: u32,
|
||
}
|
||
|
||
/// `refines.span`: `count` consecutive units from `from`.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct Span {
|
||
pub from: u32,
|
||
pub count: u32,
|
||
}
|
||
|
||
/// A well-formed `refines` member (`extensions/staged-delivery.md` §3).
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct Refines {
|
||
pub base: String,
|
||
pub prev: Prev,
|
||
pub layer: u8,
|
||
pub span: Option<Span>,
|
||
}
|
||
|
||
/// Why a `refines` member is malformed (`refines-malformed`): what failed.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct RefinesMalformed(pub &'static str);
|
||
|
||
impl std::fmt::Display for RefinesMalformed {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "refines-malformed: {}", self.0)
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for RefinesMalformed {}
|
||
|
||
impl Refines {
|
||
/// Read `refines` as staged-delivery §3 defines it, with its references in `form`.
|
||
/// Members it does not define are ignored (SPEC §8). Malformed: a value that is
|
||
/// not an object; `base` or a producer that is not a string of `form`; `prev`
|
||
/// neither such a string nor a non-empty array of runs; a run that is not an
|
||
/// object of `producer` and an integer `count` ≥ 1; two adjacent runs with the
|
||
/// same producer, or an array of one run (a single producer is written as a
|
||
/// string); `layer` not an integer in 1..=255; at layer 1 a producer other than
|
||
/// `base`; `span` neither absent, `null`, nor an object of integer `from` ≥ 0 and
|
||
/// `count` ≥ 1, or past `u32`; with a span, run counts that do not sum to its
|
||
/// `count`. An integer is written as one (SPEC §2): `-0`, a fraction or an exponent
|
||
/// is not an integer, and `serde_json` reads each as a float, which no `as_u64`
|
||
/// accepts. Whether the
|
||
/// span lies within the base's units, and, with no span, whether the runs sum to
|
||
/// them, needs the base ([`Refines::check_units`]).
|
||
pub fn from_value(v: &Value, form: RefForm) -> Result<Refines, RefinesMalformed> {
|
||
let bad = |why: &'static str| RefinesMalformed(why);
|
||
let o = v.as_object().ok_or(bad("refines is not an object"))?;
|
||
let base = o.get("base").and_then(Value::as_str).filter(|b| form.accepts(b)).ok_or(bad("base is not a reference"))?;
|
||
let layer = o
|
||
.get("layer")
|
||
.and_then(Value::as_u64)
|
||
.filter(|l| (1..=255).contains(l))
|
||
.ok_or(bad("layer is not an integer in 1..=255"))? as u8;
|
||
let span = match o.get("span") {
|
||
// `null` in an OPTIONAL member is the member absent (SPEC §3.1).
|
||
None | Some(Value::Null) => None,
|
||
Some(sp) => {
|
||
let so = sp.as_object().ok_or(bad("span is not an object"))?;
|
||
let from = so.get("from").and_then(Value::as_u64).and_then(|f| u32::try_from(f).ok()).ok_or(bad("span.from is not an integer ≥ 0 within u32"))?;
|
||
let count = so
|
||
.get("count")
|
||
.and_then(Value::as_u64)
|
||
.and_then(|c| u32::try_from(c).ok())
|
||
.filter(|&c| c >= 1)
|
||
.ok_or(bad("span.count is not an integer ≥ 1 within u32"))?;
|
||
if from.checked_add(count).is_none() {
|
||
return Err(bad("the span runs past u32"));
|
||
}
|
||
Some(Span { from, count })
|
||
}
|
||
};
|
||
let prev = match o.get("prev") {
|
||
Some(Value::String(p)) if form.accepts(p) => Prev::One(p.clone()),
|
||
Some(Value::Array(a)) if !a.is_empty() => {
|
||
let mut runs = Vec::with_capacity(a.len().min(1 << 16));
|
||
for r in a {
|
||
let ro = r.as_object().ok_or(bad("a prev run is not an object"))?;
|
||
let producer = ro.get("producer").and_then(Value::as_str).filter(|p| form.accepts(p)).ok_or(bad("a run's producer is not a reference"))?;
|
||
let count = ro
|
||
.get("count")
|
||
.and_then(Value::as_u64)
|
||
.and_then(|c| u32::try_from(c).ok())
|
||
.filter(|&c| c >= 1)
|
||
.ok_or(bad("a run's count is not an integer ≥ 1 within u32"))?;
|
||
if runs.last().is_some_and(|l: &PrevRun| l.producer == producer) {
|
||
return Err(bad("two adjacent runs name the same producer"));
|
||
}
|
||
runs.push(PrevRun { producer: producer.to_string(), count });
|
||
}
|
||
if runs.len() == 1 {
|
||
return Err(bad("prev is an array of one run; a single producer is written as a string"));
|
||
}
|
||
Prev::Runs(runs)
|
||
}
|
||
_ => return Err(bad("prev is neither a reference nor an array of runs")),
|
||
};
|
||
let r = Refines { base: base.to_string(), prev, layer, span };
|
||
if layer == 1 && r.producers().any(|p| p != r.base) {
|
||
return Err(bad("at layer 1 every producer prev names must be base"));
|
||
}
|
||
if let (Some(sp), Prev::Runs(runs)) = (span, &r.prev) {
|
||
if runs.iter().map(|x| x.count as u64).sum::<u64>() != sp.count as u64 {
|
||
return Err(bad("the run counts do not sum to the span's count"));
|
||
}
|
||
}
|
||
Ok(r)
|
||
}
|
||
|
||
/// The JSON this member is written as: `base`, `prev`, `layer`, then `span` when
|
||
/// present.
|
||
pub fn to_value(&self) -> Value {
|
||
let mut o = Map::new();
|
||
o.insert("base".into(), Value::from(self.base.clone()));
|
||
o.insert(
|
||
"prev".into(),
|
||
match &self.prev {
|
||
Prev::One(p) => Value::from(p.clone()),
|
||
Prev::Runs(runs) => Value::Array(
|
||
runs.iter()
|
||
.map(|r| {
|
||
let mut m = Map::new();
|
||
m.insert("producer".into(), Value::from(r.producer.clone()));
|
||
m.insert("count".into(), Value::from(r.count));
|
||
Value::Object(m)
|
||
})
|
||
.collect(),
|
||
),
|
||
},
|
||
);
|
||
o.insert("layer".into(), Value::from(self.layer));
|
||
if let Some(sp) = self.span {
|
||
let mut m = Map::new();
|
||
m.insert("from".into(), Value::from(sp.from));
|
||
m.insert("count".into(), Value::from(sp.count));
|
||
o.insert("span".into(), Value::Object(m));
|
||
}
|
||
Value::Object(o)
|
||
}
|
||
|
||
/// Every producer `prev` names, in span order.
|
||
pub fn producers(&self) -> impl Iterator<Item = &str> {
|
||
let v: Vec<&str> = match &self.prev {
|
||
Prev::One(p) => vec![p.as_str()],
|
||
Prev::Runs(runs) => runs.iter().map(|r| r.producer.as_str()).collect(),
|
||
};
|
||
v.into_iter()
|
||
}
|
||
|
||
/// The units the layer covers, `(from, count)`, of a base of `base_units` units:
|
||
/// its span, or every unit.
|
||
pub fn units(&self, base_units: u32) -> (u32, u32) {
|
||
match self.span {
|
||
Some(sp) => (sp.from, sp.count),
|
||
None => (0, base_units),
|
||
}
|
||
}
|
||
|
||
/// The checks that need the base's unit count (staged-delivery §3): the span lies
|
||
/// within the base's units, and the runs sum to the units the layer covers.
|
||
pub fn check_units(&self, base_units: u32) -> Result<(), RefinesMalformed> {
|
||
let (from, count) = self.units(base_units);
|
||
if from as u64 + count as u64 > base_units as u64 || count == 0 {
|
||
return Err(RefinesMalformed("the span lies outside the base's units"));
|
||
}
|
||
if let Prev::Runs(runs) = &self.prev {
|
||
if runs.iter().map(|x| x.count as u64).sum::<u64>() != count as u64 {
|
||
return Err(RefinesMalformed("the run counts do not sum to the units the layer covers"));
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// The producer `prev` gives for the `i`-th unit of the span (0-based), or
|
||
/// `None` past the runs. Linear in the runs; a sink resolving every unit of a span
|
||
/// indexes the runs once (`staged::StageChain` keeps their ends and bisects).
|
||
pub fn producer_for(&self, i: u32) -> Option<&str> {
|
||
match &self.prev {
|
||
Prev::One(p) => Some(p),
|
||
Prev::Runs(runs) => {
|
||
let mut at = 0u64;
|
||
for r in runs {
|
||
at += r.count as u64;
|
||
if (i as u64) < at {
|
||
return Some(&r.producer);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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, `<id>@<version>`;
|
||
/// - `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<DerivedFrom, DerivedFromMalformed> {
|
||
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<u8>,
|
||
}
|
||
|
||
/// Where a `WAI1` envelope dispatches (SPEC §4): the capability, and what the
|
||
/// pin verifier returned for it when a pin applied.
|
||
#[derive(Debug)]
|
||
pub struct Dispatch<'a, V> {
|
||
pub capability: &'a str,
|
||
pub verified: Option<V>,
|
||
}
|
||
|
||
/// Why a `WAI1` envelope is inert at a sink (SPEC §4 step 6).
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum Inert {
|
||
/// Neither the capability nor the fallback is advertised (or dispatchable).
|
||
MissingCapability,
|
||
/// The pin was refused for `capability` (the first refusal kept, step 3 before
|
||
/// step 5).
|
||
PinRefused { capability: String, refusal: PinRefusal },
|
||
/// The manifest declares an encryption this sink cannot open (SPEC §6.4 step 2,
|
||
/// `unsupported_encryption`): the reference dispatchers implement no scheme, so an
|
||
/// encrypted envelope is inert at them before any capability is considered.
|
||
Encrypted { encryption: String },
|
||
}
|
||
|
||
impl Inert {
|
||
/// `missing_capability`, `unsupported_encryption`, or the pin's reason.
|
||
pub fn code(&self) -> &'static str {
|
||
match self {
|
||
Self::MissingCapability => "missing_capability",
|
||
Self::PinRefused { refusal, .. } => refusal.code(),
|
||
Self::Encrypted { .. } => "unsupported_encryption",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for Inert {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
Self::MissingCapability => write!(f, "missing_capability: the sink advertises neither the capability nor the fallback"),
|
||
Self::PinRefused { capability, refusal } => write!(f, "{}: {capability}: {refusal}", refusal.code()),
|
||
Self::Encrypted { encryption } => write!(
|
||
f,
|
||
"unsupported_encryption: the manifest declares the encryption {encryption}, which this sink cannot \
|
||
open (SPEC §6.4): the file is inert here, and its payload is not decoded"
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// A pin verifier for a sink that holds no parameter set: it refuses every pin
|
||
/// that applies with `prior_not_held`. For tools that select without decoding.
|
||
pub fn no_parameter_sets() -> impl FnMut(&str, &Prior) -> Result<std::convert::Infallible, PinRefusal> {
|
||
|capability: &str, _: &Prior| Err(PinRefusal::NotHeld { capability: capability.to_string() })
|
||
}
|
||
|
||
impl Wai {
|
||
pub fn new(manifest: Manifest, payload: Vec<u8>) -> Self {
|
||
Self { manifest, payload }
|
||
}
|
||
|
||
pub fn write<P: AsRef<Path>>(&self, path: P) -> std::io::Result<usize> {
|
||
let bytes = self.to_bytes()?;
|
||
fs::write(path, &bytes)?;
|
||
Ok(bytes.len())
|
||
}
|
||
|
||
/// The envelope's bytes. Refuses a manifest longer than SPEC §2's reader limit
|
||
/// (`manifest_too_large`), and a payload whose length does not fit its `u32` field.
|
||
pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
|
||
self.to_bytes_within(WriteLimits::SPEC)
|
||
}
|
||
|
||
/// [`Self::to_bytes`] within `lim`.
|
||
fn to_bytes_within(&self, lim: WriteLimits) -> std::io::Result<Vec<u8>> {
|
||
// No insignificant whitespace, key order preserved (serde_json's
|
||
// `preserve_order`), so the bytes round-trip identically.
|
||
let m = manifest_json(&self.manifest, lim.manifest)?;
|
||
let payload_len = u32_field(self.payload.len(), lim.field, "the payload")?;
|
||
let mut out = Vec::with_capacity(12 + m.len() + self.payload.len());
|
||
out.extend_from_slice(MAGIC);
|
||
out.extend_from_slice(&(m.len() as u32).to_le_bytes());
|
||
out.extend_from_slice(&m);
|
||
out.extend_from_slice(&payload_len.to_le_bytes());
|
||
out.extend_from_slice(&self.payload);
|
||
Ok(out)
|
||
}
|
||
|
||
pub fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||
let bytes = fs::read(path)?;
|
||
Self::from_bytes(&bytes)
|
||
}
|
||
|
||
pub fn from_bytes(bytes: &[u8]) -> std::io::Result<Self> {
|
||
// Every field is bounds-checked: malformed input (truncated /
|
||
// wrong-magic / length-prefix overrunning the buffer / etc.)
|
||
// must return Err, never panic. Fuzz smoke test enforces this.
|
||
let need = |off: usize, n: usize, what: &str| -> std::io::Result<()> {
|
||
if off.saturating_add(n) > bytes.len() {
|
||
Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("truncated WAI ({what} needs {n} bytes at offset {off}, have {})",
|
||
bytes.len())))
|
||
} else { Ok(()) }
|
||
};
|
||
need(0, 4, "magic")?;
|
||
if &bytes[..4] != MAGIC {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
"not a WAI file (magic mismatch)"));
|
||
}
|
||
need(4, 4, "manifest length")?;
|
||
let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||
check_manifest_len(ml as u64)
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
need(8, ml, "manifest body")?;
|
||
check_unique_members(&bytes[8..8 + ml])
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
let m: Manifest = serde_json::from_slice(&bytes[8..8 + ml])
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
let p_off = 8 + ml;
|
||
need(p_off, 4, "payload length")?;
|
||
let pl = u32::from_le_bytes(bytes[p_off..p_off + 4].try_into().unwrap()) as usize;
|
||
need(p_off + 4, pl, "payload body")?;
|
||
let payload = bytes[p_off + 4..p_off + 4 + pl].to_vec();
|
||
Ok(Self { manifest: m, payload })
|
||
}
|
||
|
||
pub fn capability(&self) -> &str {
|
||
&self.manifest.model_requirement.capability
|
||
}
|
||
|
||
pub fn fallback(&self) -> Option<&str> {
|
||
self.manifest.model_requirement.fallback.as_deref()
|
||
}
|
||
|
||
/// The manifest's `refines`, read with content-hash references, or `None` when it
|
||
/// carries none (`extensions/staged-delivery.md` §3).
|
||
pub fn refines(&self) -> Option<Result<Refines, RefinesMalformed>> {
|
||
self.manifest.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ContentHash))
|
||
}
|
||
|
||
/// SPEC §4 dispatch at a sink that advertises the capabilities `advertises`
|
||
/// accepts and verifies pins with `verify` (SPEC §3.1). First, SPEC §6.4 step 2:
|
||
/// a manifest that declares an encryption other than `"none"` is inert here
|
||
/// ([`Inert::Encrypted`]), since this dispatcher opens no scheme. Step 3 takes the
|
||
/// capability when the sink advertises it and, where the pin applies to it,
|
||
/// `verify` accepts the pin; step 5 does the same for the fallback. A refused
|
||
/// pin makes that capability unsupported for this envelope: dispatch moves on,
|
||
/// keeping the first refusal, and an inert envelope reports it
|
||
/// ([`Inert::PinRefused`]) — else [`Inert::MissingCapability`]. One pin applies
|
||
/// to whichever capability decodes the payload. Only a capability
|
||
/// [`crate::codecs::is_dispatchable`] accepts is ever taken, whatever
|
||
/// `advertises` says:
|
||
///
|
||
/// - a companion-only capability ([`crate::codecs::is_companion_only`]) is
|
||
/// never dispatched standalone (SPEC §5): named as the capability, the
|
||
/// payload is companion data and the envelope is inert; named as the
|
||
/// fallback, step 5 is skipped, and step 3 is unaffected. So an envelope that
|
||
/// [`Self::check_component_roles`] reports for its fallback alone still
|
||
/// decodes at a sink that advertises its capability;
|
||
/// - a refinement-only capability ([`crate::codecs::is_refinement_only`]) is
|
||
/// handled alike (SPEC §4): its payload refines the stage below it and is
|
||
/// processed by `extensions/staged-delivery.md` §4 (`crate::staged`), never
|
||
/// decoded here;
|
||
/// - a candidate ([`crate::codecs::is_candidate`]) is treated as unsupported
|
||
/// (SPEC §5, *Candidate capabilities*): as the capability, step 5 may still
|
||
/// take a usable fallback; as the fallback, it is never taken;
|
||
/// - a record or a derivation names no payload format, so it is never taken;
|
||
/// as the capability, step 5 may still take a usable fallback;
|
||
/// - a string this revision does not register is left to `advertises`: a
|
||
/// later MINOR revision may register it (SPEC §8), and a sink that has a
|
||
/// codec for it says so by advertising it (SPEC §4, step 3). A pin on it is
|
||
/// verified as a set of several files ([`pin_applies_to`]), so it fails
|
||
/// closed.
|
||
pub fn dispatch_verified<V>(
|
||
&self,
|
||
advertises: impl Fn(&str) -> bool,
|
||
mut verify: impl FnMut(&str, &Prior) -> Result<V, PinRefusal>,
|
||
) -> Result<Dispatch<'_, V>, Inert> {
|
||
// SPEC §6.4 step 2, before §4: an encrypted payload is inert at a sink that
|
||
// opens no scheme.
|
||
if let Some(sec) = self.manifest.security.as_ref().filter(|s| !s.is_plaintext()) {
|
||
return Err(encryption_inert(format!("{:?}", sec.encryption.as_deref().unwrap_or_default())));
|
||
}
|
||
let primary = self.capability();
|
||
// A companion-only payload is companion data, and a refinement-only payload
|
||
// refines the stage below it (staged-delivery §4 processes it): neither is
|
||
// decoded on its own, so step 5 is not taken either.
|
||
if crate::codecs::is_companion_only(primary) || crate::codecs::is_refinement_only(primary) {
|
||
return Err(Inert::MissingCapability);
|
||
}
|
||
let mut first_refusal: Option<Inert> = None;
|
||
for cap in [Some(primary), self.fallback()].into_iter().flatten() {
|
||
if !crate::codecs::is_dispatchable(cap) || !advertises(cap) {
|
||
continue;
|
||
}
|
||
match self.prior() {
|
||
Some(p) if pin_applies_to(cap) => match verify(cap, p) {
|
||
Ok(v) => return Ok(Dispatch { capability: cap, verified: Some(v) }),
|
||
Err(refusal) => {
|
||
if first_refusal.is_none() {
|
||
first_refusal = Some(Inert::PinRefused { capability: cap.to_string(), refusal });
|
||
}
|
||
}
|
||
},
|
||
_ => return Ok(Dispatch { capability: cap, verified: None }),
|
||
}
|
||
}
|
||
Err(first_refusal.unwrap_or(Inert::MissingCapability))
|
||
}
|
||
|
||
/// What a verifier can decide about this envelope's pin without holding a
|
||
/// parameter set — the first finding, in [`PIN_FINDING_CODES`] order.
|
||
pub fn check_pin(&self) -> Result<(), PinFinding> {
|
||
let caps: Vec<&str> = [Some(self.capability()), self.fallback()].into_iter().flatten().collect();
|
||
static_pin_findings(&caps, self.prior(), self.intent())
|
||
}
|
||
|
||
pub fn media(&self) -> &str {
|
||
&self.manifest.media
|
||
}
|
||
|
||
pub fn intent(&self) -> &str {
|
||
&self.manifest.intent
|
||
}
|
||
|
||
pub fn kind(&self) -> &str {
|
||
&self.manifest.conditioning.kind
|
||
}
|
||
|
||
/// The pin, if the manifest carries one.
|
||
pub fn prior(&self) -> Option<&Prior> {
|
||
self.manifest.conditioning.prior.as_ref()
|
||
}
|
||
|
||
/// The embedding space the perceptual conformance threshold is measured in.
|
||
/// Defaults to `"clip"` when unset (back-compat).
|
||
pub fn sim_metric(&self) -> &str {
|
||
self.manifest.model_requirement.sim_metric.as_deref().unwrap_or("clip")
|
||
}
|
||
|
||
/// Whether this envelope makes a *sound* `replicate` claim (SPEC §7) through
|
||
/// its capability. See [`crate::codecs::replicate_refusal`].
|
||
pub fn replicate_is_sound(&self) -> bool {
|
||
self.replicate_refusal().is_none()
|
||
}
|
||
|
||
/// Why this envelope's `replicate` claim through its capability is unsound, or
|
||
/// `None` if it is sound (or does not claim `replicate`).
|
||
pub fn replicate_refusal(&self) -> Option<crate::codecs::ReplicateRefusal> {
|
||
self.replicate_refusal_for(&self.manifest.model_requirement.capability)
|
||
}
|
||
|
||
/// Why this envelope's `replicate` claim is unsound when it decodes through
|
||
/// `capability` (the capability, or the fallback at SPEC §4 step 5).
|
||
pub fn replicate_refusal_for(&self, capability: &str) -> Option<crate::codecs::ReplicateRefusal> {
|
||
crate::codecs::replicate_refusal(capability, &self.manifest.intent, self.prior())
|
||
}
|
||
|
||
/// Check this v1.0 envelope against the component-role rules of SPEC §5.
|
||
/// A companion-only capability ([`crate::codecs::is_companion_only`]: HDR
|
||
/// dynamic metadata, an LCEVC enhancement) means nothing without the base
|
||
/// payload it accompanies, and a `WAI1` carries exactly one payload, so it
|
||
/// MUST NOT be the envelope's `capability` — nor its `fallback`, because
|
||
/// dispatch step 5 would decode the payload as that capability on its own.
|
||
/// The v1.1 counterpart is [`WaiMulti::check_component_roles`].
|
||
///
|
||
/// A refinement-only capability ([`crate::codecs::is_refinement_only`]) is carried
|
||
/// as a `WAI1` `capability` only with a well-formed `refines` and a `null`
|
||
/// `fallback`, and is never a `fallback`; a manifest whose capability is not
|
||
/// refinement-only carries no `refines` (`extensions/staged-delivery.md` §3; SPEC
|
||
/// §8). Checked in that order, after the companion rules.
|
||
pub fn check_component_roles(&self) -> Result<(), RoleError> {
|
||
let capability = self.capability();
|
||
if crate::codecs::is_companion_only(capability) {
|
||
return Err(RoleError::CompanionOnlyAsCapability { capability: capability.to_string() });
|
||
}
|
||
if let Some(fb) = self.fallback() {
|
||
if crate::codecs::is_companion_only(fb) {
|
||
return Err(RoleError::CompanionOnlyAsFallback { capability: fb.to_string() });
|
||
}
|
||
}
|
||
let refinement = crate::codecs::is_refinement_only(capability);
|
||
if let Some(fb) = self.fallback() {
|
||
if crate::codecs::is_refinement_only(fb) {
|
||
return Err(RoleError::RefinementOnlyAsFallback { capability: fb.to_string() });
|
||
}
|
||
}
|
||
match (refinement, self.refines()) {
|
||
(true, None) => Err(RoleError::RefinementWithoutRefines { capability: capability.to_string() }),
|
||
(true, Some(Err(e))) => Err(RoleError::RefinesMalformed { index: None, detail: e.0 }),
|
||
(true, Some(Ok(_))) if self.fallback().is_some() => {
|
||
Err(RoleError::RefinementWithFallback { capability: capability.to_string() })
|
||
}
|
||
(false, Some(_)) => Err(RoleError::RefinesWithoutRefinement { capability: capability.to_string() }),
|
||
_ => Ok(()),
|
||
}
|
||
}
|
||
|
||
/// Check that this envelope names no candidate capability
|
||
/// ([`crate::codecs::is_candidate`]), which an encoder MUST NOT emit (SPEC §5,
|
||
/// *Candidate capabilities*): first the `capability`, then the `fallback`.
|
||
/// Parsing stays format-agnostic, so such an envelope still parses; this is
|
||
/// what reports it.
|
||
pub fn check_candidates(&self) -> Result<(), CandidateError> {
|
||
check_candidates_v1(self.capability(), self.fallback())
|
||
}
|
||
|
||
/// Check that the payload is the format SPEC §5 pins for the `capability`, and for
|
||
/// the `fallback` (step 5 decodes the same payload through it), where either is an
|
||
/// integer capability with a pinned format ([`check_int_payload_framing`]). An
|
||
/// encrypted payload (`security.encryption` other than `"none"`) is ciphertext, so
|
||
/// it is not checked here: a sink decrypts it first (SPEC §6.4) and checks the
|
||
/// plaintext when it decodes it.
|
||
pub fn check_int_payload_framing(&self) -> Result<(), FramingError> {
|
||
if !self.manifest.security.as_ref().is_none_or(Security::is_plaintext) {
|
||
return Ok(());
|
||
}
|
||
check_int_payload_framing(self.capability(), &self.payload)?;
|
||
match self.fallback() {
|
||
Some(fb) => check_int_payload_framing(fb, &self.payload),
|
||
None => Ok(()),
|
||
}
|
||
}
|
||
|
||
/// The envelope's bytes, for an encoder to emit: [`Self::check_candidates`], then
|
||
/// [`Self::check_int_payload_framing`], then [`Self::to_bytes`]. The C packer
|
||
/// `wai_envelope_pack` emits through this, so an encoder that uses it cannot emit a
|
||
/// candidate, or an integer payload that is not its capability's pinned format
|
||
/// (SPEC §5).
|
||
pub fn to_emit_bytes(&self) -> Result<Vec<u8>, EmitError> {
|
||
self.check_candidates().map_err(EmitError::Candidate)?;
|
||
self.check_int_payload_framing().map_err(|error| EmitError::IntPayload { component: None, error })?;
|
||
self.to_bytes().map_err(EmitError::Serialize)
|
||
}
|
||
|
||
/// Why this envelope's `replicate` claim is unsound by its pin alone. See
|
||
/// [`replicate_prior_refusal`].
|
||
pub fn replicate_prior_refusal(&self) -> Option<PriorRefusal> {
|
||
replicate_prior_refusal(&self.manifest.intent, self.prior())
|
||
}
|
||
}
|
||
|
||
/// Whether the JSON text `text` names a member twice in one object, at any depth
|
||
/// (SPEC §2: member names are unique within an object). `serde_json` keeps the last of
|
||
/// two equal names in a `Value` and refuses them only in a struct's own fields, so a
|
||
/// manifest is read once more by this check, which refuses any repeat. Names compare
|
||
/// after escapes are read, so `"a"` and `"\u0061"` are one name. The text's own JSON
|
||
/// errors are reported as they are.
|
||
pub fn check_unique_members(text: &[u8]) -> Result<(), serde_json::Error> {
|
||
use serde::de::{DeserializeSeed, Deserializer, Error as _, MapAccess, SeqAccess, Visitor};
|
||
struct Unique;
|
||
impl<'de> DeserializeSeed<'de> for Unique {
|
||
type Value = ();
|
||
fn deserialize<D: Deserializer<'de>>(self, d: D) -> Result<(), D::Error> {
|
||
d.deserialize_any(Unique)
|
||
}
|
||
}
|
||
impl<'de> Visitor<'de> for Unique {
|
||
type Value = ();
|
||
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.write_str("a JSON value")
|
||
}
|
||
fn visit_bool<E>(self, _: bool) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_i64<E>(self, _: i64) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_u64<E>(self, _: u64) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_f64<E>(self, _: f64) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_str<E>(self, _: &str) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_unit<E>(self) -> Result<(), E> {
|
||
Ok(())
|
||
}
|
||
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<(), A::Error> {
|
||
while seq.next_element_seed(Unique)?.is_some() {}
|
||
Ok(())
|
||
}
|
||
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<(), A::Error> {
|
||
let mut seen = std::collections::HashSet::new();
|
||
while let Some(k) = map.next_key::<String>()? {
|
||
if !seen.insert(k) {
|
||
return Err(A::Error::custom("an object names a member twice"));
|
||
}
|
||
map.next_value_seed(Unique)?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
let mut de = serde_json::Deserializer::from_slice(text);
|
||
Unique.deserialize(&mut de)?;
|
||
de.end()
|
||
}
|
||
|
||
/// The manifest bytes of a `WAI1` or `WAI2` envelope, exactly as they were
|
||
/// written, bounds-checked as the parsers check them. A tool that hands a manifest
|
||
/// on (the C FFI's unpack calls) returns these, so nothing the Rust types do not
|
||
/// model is lost.
|
||
pub fn manifest_bytes(envelope: &[u8]) -> std::io::Result<&[u8]> {
|
||
let bad = |m: String| std::io::Error::new(std::io::ErrorKind::InvalidData, m);
|
||
if envelope.len() < 8 {
|
||
return Err(bad(format!("truncated envelope ({} bytes; need 8 for magic and manifest length)", envelope.len())));
|
||
}
|
||
if &envelope[..4] != MAGIC && &envelope[..4] != MAGIC_V2 {
|
||
return Err(bad("unknown magic (not WAI1/WAI2)".into()));
|
||
}
|
||
let ml = u32::from_le_bytes(envelope[4..8].try_into().unwrap()) as usize;
|
||
check_manifest_len(ml as u64).map_err(bad)?;
|
||
if 8usize.saturating_add(ml) > envelope.len() {
|
||
return Err(bad(format!("truncated envelope (manifest body needs {ml} bytes at offset 8, have {})", envelope.len())));
|
||
}
|
||
Ok(&envelope[8..8 + ml])
|
||
}
|
||
|
||
// ---- v1.1: multi-rendition envelope (WAI2) ----------------------
|
||
//
|
||
// One envelope carries several components: renditions of the SAME content,
|
||
// of which the sink picks one (SPEC §7.1), and companions applied with the
|
||
// rendition it picked. Each component may carry an identity: an `id`, its
|
||
// payload digest `sha256`, and its parameter-set pin `prior`.
|
||
|
||
/// Whether `id` is a component id (SPEC §7.1): 1 to 64 of `a`–`z`, `0`–`9`, `_`
|
||
/// and `-`. It contains no `.`, so it never equals a capability string.
|
||
pub fn is_component_id(id: &str) -> bool {
|
||
(1..=64).contains(&id.len())
|
||
&& id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-')
|
||
}
|
||
|
||
/// A `WAI2` component entry (SPEC §7.1). Build one with [`RenditionMeta::new`].
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct RenditionMeta {
|
||
pub capability: String,
|
||
pub kind: String,
|
||
/// The component's address: unique within the envelope ([`is_component_id`]).
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub id: Option<String>,
|
||
/// The SHA-256 of this entry's payload bytes, as 64 lowercase hexadecimal
|
||
/// digits. A sink checks it before it decodes or applies the entry.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub sha256: Option<String>,
|
||
/// The pin (SPEC §3.1) of the parameter set this entry's payload was coded
|
||
/// against.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub prior: Option<Prior>,
|
||
/// Component role. Absent ⇒ `"rendition"` (a selectable alternative of the
|
||
/// same content). `"companion"` marks a component applied *with* the chosen
|
||
/// rendition: a companion-only capability ([`crate::codecs::is_companion_only`]).
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub role: Option<String>,
|
||
/// For a companion: the capability of the component it accompanies.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub companion_of: Option<String>,
|
||
/// For a companion: the `id` of the one entry it binds to. That entry's
|
||
/// capability equals `companion_of`, so a sink that does not read this member
|
||
/// binds by capability.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub companion_of_id: Option<String>,
|
||
/// For a refinement (`role: "refinement"`): what it refines
|
||
/// (`extensions/staged-delivery.md` §3), its base and producers named by the `id`
|
||
/// of entries in this envelope (SPEC §7.1, *Refinement components*). Kept as the
|
||
/// JSON it is, so a malformed value still parses ([`Refines::from_value`] with
|
||
/// [`RefForm::ComponentId`] reports it).
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub refines: Option<Value>,
|
||
/// The exact manifest of this entry as a `WAI1` envelope (SPEC §7.1, *Exploding a
|
||
/// component*): what [`WaiMulti::explode`] writes as the exploded envelope's
|
||
/// manifest, verbatim from the envelope bytes.
|
||
#[serde(default, skip_serializing_if = "is_none")]
|
||
pub envelope_manifest: Option<Value>,
|
||
/// `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<Value>,
|
||
/// Members this revision does not define, kept so they survive a round trip.
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl RenditionMeta {
|
||
/// A selectable rendition entry of `capability` and `kind`, with no identity
|
||
/// members.
|
||
pub fn new(capability: impl Into<String>, kind: impl Into<String>) -> Self {
|
||
Self {
|
||
capability: capability.into(),
|
||
kind: kind.into(),
|
||
id: None,
|
||
sha256: None,
|
||
prior: None,
|
||
role: None,
|
||
companion_of: None,
|
||
companion_of_id: None,
|
||
refines: None,
|
||
envelope_manifest: None,
|
||
derived_from: None,
|
||
extra: Map::new(),
|
||
}
|
||
}
|
||
/// Pin the parameter set this entry's payload was coded against.
|
||
pub fn with_prior(mut self, prior: Prior) -> Self {
|
||
self.prior = Some(prior);
|
||
self
|
||
}
|
||
/// Set the role.
|
||
pub fn with_role(mut self, role: impl Into<String>) -> Self {
|
||
self.role = Some(role.into());
|
||
self
|
||
}
|
||
/// Mark this entry a companion of the component of capability `base`.
|
||
pub fn companion_to(mut self, base: impl Into<String>) -> Self {
|
||
self.role = Some("companion".into());
|
||
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<String>) -> Self {
|
||
self.id = Some(id.into());
|
||
self
|
||
}
|
||
/// State the entry's payload digest.
|
||
pub fn with_sha256(mut self, sha256: impl Into<String>) -> Self {
|
||
self.sha256 = Some(sha256.into());
|
||
self
|
||
}
|
||
/// Bind this companion to the entry whose `id` is `id`.
|
||
pub fn with_companion_of_id(mut self, id: impl Into<String>) -> Self {
|
||
self.companion_of_id = Some(id.into());
|
||
self
|
||
}
|
||
|
||
/// Whether this component is a companion descriptor (not a selectable
|
||
/// rendition).
|
||
pub fn is_companion(&self) -> bool {
|
||
self.role.as_deref() == Some("companion")
|
||
}
|
||
|
||
/// Whether this component is a refinement (`role: "refinement"`, SPEC §7.1).
|
||
pub fn is_refinement(&self) -> bool {
|
||
self.role.as_deref() == Some("refinement")
|
||
}
|
||
|
||
/// Whether `pick`/`primary` may select this component: it is not marked a
|
||
/// companion **and** [`crate::codecs::is_dispatchable`] accepts its
|
||
/// capability — so not a companion-only capability, a candidate, a record or
|
||
/// a derivation. An HDR metadata block or an LCEVC enhancement mislabelled as
|
||
/// a rendition is still never selected — it has no picture or sound of its
|
||
/// own. [`WaiMulti::check_component_roles`] reports the mislabelling, and
|
||
/// [`WaiMulti::check_candidates`] a candidate.
|
||
pub fn is_selectable(&self) -> bool {
|
||
!self.is_companion() && !self.is_refinement() && crate::codecs::is_dispatchable(&self.capability)
|
||
}
|
||
|
||
/// Whether `payload` matches this entry's stated `sha256` (true when none is
|
||
/// stated). A malformed digest matches nothing.
|
||
pub fn payload_digest_matches(&self, payload: &[u8]) -> bool {
|
||
match self.sha256.as_deref() {
|
||
None => true,
|
||
Some(want) => is_sha256_hex(want) && crate::pin::sha256_hex(payload) == want,
|
||
}
|
||
}
|
||
|
||
/// Whether this entry's `sha256` is absent or well formed — 64 lowercase hex digits
|
||
/// ([`is_sha256_hex`]) — read from the entry alone: one that is not matches no payload
|
||
/// ([`Self::payload_digest_matches`]), so no sink decodes the entry whatever its
|
||
/// payload. Of an encrypted envelope, whose payloads a verifier without the key does
|
||
/// not read, this is what the entry gives of its digest (jwp-receipts §2.4.12 rule 4).
|
||
pub fn digest_well_formed(&self) -> bool {
|
||
self.sha256.as_deref().is_none_or(is_sha256_hex)
|
||
}
|
||
}
|
||
|
||
/// An envelope that breaks the component rules of SPEC §5 / §7.1. Returned by
|
||
/// [`Wai::check_component_roles`] (v1.0) and [`WaiMulti::check_component_roles`]
|
||
/// (v1.1); `index` is the component's position in `manifest.renditions`.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum RoleError {
|
||
/// v1.0: a companion-only capability named as the `WAI1` `capability`. A
|
||
/// `WAI1` carries one payload, so there is no base for it to accompany.
|
||
CompanionOnlyAsCapability { capability: String },
|
||
/// v1.0: a companion-only capability named as the `WAI1` `fallback`.
|
||
/// Dispatch step 5 would decode the payload as that capability on its own.
|
||
CompanionOnlyAsFallback { capability: String },
|
||
/// A companion-only capability (HDR dynamic metadata, an enhancement
|
||
/// layer) listed as a selectable rendition instead of as a companion.
|
||
CompanionOnlyAsRendition { index: usize, capability: String },
|
||
/// A registered payload capability, standalone media (one that is not
|
||
/// companion-only, such as `wai.audio.iamf` or `wai.video.hevc`), marked
|
||
/// `role = "companion"`.
|
||
StandaloneAsCompanion { index: usize, capability: String },
|
||
/// A component, in any role, whose capability is a record or a derivation (SPEC §5,
|
||
/// *Kinds of registered string*), which names no payload format.
|
||
RecordOrDerivationAsComponent { index: usize, capability: String },
|
||
/// A companion without `companion_of`, or whose `companion_of` (or
|
||
/// `companion_of_id`) names no selectable rendition in this envelope.
|
||
UnboundCompanion { index: usize, capability: String },
|
||
/// A companion whose `companion_of` names a capability it does not apply to
|
||
/// ([`crate::codecs::companion_applies_to`]).
|
||
IncompatibleBase { index: usize, capability: String, base: String },
|
||
/// An enhancement layer, bound by capability alone, whose `companion_of` does
|
||
/// not resolve to exactly one selectable rendition. With two renditions of the
|
||
/// base capability, `companion_of_id` names the one it was encoded against.
|
||
EnhancementBaseNotUnique { index: usize, capability: String, matches: usize },
|
||
/// A second enhancement layer bound to a base that already has one (the
|
||
/// component at `first`).
|
||
DuplicateEnhancementLayer { index: usize, capability: String, base: String, first: usize },
|
||
/// A component `id` that is not 1 to 64 of `a`–`z`, `0`–`9`, `_`, `-`.
|
||
ComponentIdMalformed { index: usize },
|
||
/// A component `id` an earlier component (at `first`) already has.
|
||
DuplicateComponentId { index: usize, first: usize },
|
||
/// A companion whose `companion_of_id` names no component in the envelope.
|
||
CompanionIdUnresolved { index: usize, id: String },
|
||
/// A companion whose `companion_of_id` names a component whose capability is
|
||
/// `capability`, not the companion's `companion_of`.
|
||
CompanionIdCapabilityMismatch { index: usize, id: String, capability: String },
|
||
/// v1.0: a refinement-only capability named as the `WAI1` `fallback`. It is never
|
||
/// decoded on its own.
|
||
RefinementOnlyAsFallback { capability: String },
|
||
/// v1.0: a refinement-only `capability` with no `refines`
|
||
/// (`extensions/staged-delivery.md` §3).
|
||
RefinementWithoutRefines { capability: String },
|
||
/// v1.0: a refinement-only `capability` with a `fallback`; its fallback MUST be
|
||
/// `null`.
|
||
RefinementWithFallback { capability: String },
|
||
/// v1.0: `refines` on a manifest whose capability is not refinement-only.
|
||
RefinesWithoutRefinement { capability: String },
|
||
/// A `refines` member that is malformed (`refines-malformed`): the `WAI2`
|
||
/// component's index, or `None` for a `WAI1` manifest.
|
||
RefinesMalformed { index: Option<usize>, detail: &'static str },
|
||
/// v1.1: a refinement-only capability carried as a rendition or a companion.
|
||
RefinementOnlyAsComponent { index: usize, capability: String },
|
||
/// v1.1: `refines` on an entry whose role is not `"refinement"`.
|
||
RefinesOnNonRefinement { index: usize },
|
||
/// v1.1: a `role: "refinement"` entry whose capability is not refinement-only.
|
||
RefinementNotRefinementOnly { index: usize, capability: String },
|
||
/// v1.1: a refinement, or an entry it names, without an `id` or a `sha256`.
|
||
RefinementIdentityMissing { index: usize },
|
||
/// v1.1: a refinement whose `refines.base` names no selectable entry.
|
||
RefinementBaseMissing { index: usize, base: String },
|
||
/// v1.1: a refinement whose capability does not admit its base entry's (SPEC §5,
|
||
/// *Base admission*).
|
||
RefinementBaseNotAdmitted { index: usize, capability: String, base: String },
|
||
/// v1.1: a producer in a refinement's `prev` that names neither a selectable entry
|
||
/// nor a refinement before it.
|
||
RefinementPrevMissing { index: usize, producer: String },
|
||
/// v1.1: a refinement placed before a selectable entry.
|
||
RefinementBeforeSelectable { index: usize, selectable: usize },
|
||
}
|
||
|
||
impl RoleError {
|
||
/// A token for the error, for tools and corpora.
|
||
pub fn code(&self) -> &'static str {
|
||
match self {
|
||
RoleError::CompanionOnlyAsCapability { .. } => "companion-only-as-capability",
|
||
RoleError::CompanionOnlyAsFallback { .. } => "companion-only-as-fallback",
|
||
RoleError::CompanionOnlyAsRendition { .. } => "companion-only-as-rendition",
|
||
RoleError::StandaloneAsCompanion { .. } => "standalone-as-companion",
|
||
RoleError::RecordOrDerivationAsComponent { .. } => "record-or-derivation-as-component",
|
||
RoleError::UnboundCompanion { .. } => "unbound-companion",
|
||
RoleError::IncompatibleBase { .. } => "incompatible-base",
|
||
RoleError::EnhancementBaseNotUnique { .. } => "enhancement-base-not-unique",
|
||
RoleError::DuplicateEnhancementLayer { .. } => "duplicate-enhancement-layer",
|
||
RoleError::ComponentIdMalformed { .. } => "component-id-malformed",
|
||
RoleError::DuplicateComponentId { .. } => "duplicate-component-id",
|
||
RoleError::CompanionIdUnresolved { .. } => "companion-id-unresolved",
|
||
RoleError::CompanionIdCapabilityMismatch { .. } => "companion-id-capability-mismatch",
|
||
RoleError::RefinementOnlyAsFallback { .. } => "refinement-only-as-fallback",
|
||
RoleError::RefinementWithoutRefines { .. } => "refinement-without-refines",
|
||
RoleError::RefinementWithFallback { .. } => "refinement-with-fallback",
|
||
RoleError::RefinesWithoutRefinement { .. } => "refines-without-refinement",
|
||
RoleError::RefinesMalformed { .. } => "refines-malformed",
|
||
RoleError::RefinementOnlyAsComponent { .. } => "refinement-only-as-component",
|
||
RoleError::RefinesOnNonRefinement { .. } => "refines-on-non-refinement",
|
||
RoleError::RefinementNotRefinementOnly { .. } => "refinement-not-refinement-only",
|
||
RoleError::RefinementIdentityMissing { .. } => "refinement-identity-missing",
|
||
RoleError::RefinementBaseMissing { .. } => "refinement-base-missing",
|
||
RoleError::RefinementBaseNotAdmitted { .. } => "refinement-base-not-admitted",
|
||
RoleError::RefinementPrevMissing { .. } => "refinement-prev-missing",
|
||
RoleError::RefinementBeforeSelectable { .. } => "refinement-before-selectable",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for RoleError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
RoleError::CompanionOnlyAsCapability { capability } => write!(
|
||
f, "{capability} is companion-only and cannot be a WAI1 capability; carry it as a WAI2 companion bound to its base rendition"),
|
||
RoleError::CompanionOnlyAsFallback { capability } => write!(
|
||
f, "{capability} is companion-only and cannot be a WAI1 fallback; it is never dispatched on its own"),
|
||
RoleError::DuplicateEnhancementLayer { index, capability, base, first } => write!(
|
||
f, "enhancement layer {index} ({capability}) binds to {base}, which already has enhancement layer {first}; a base takes at most one"),
|
||
RoleError::CompanionOnlyAsRendition { index, capability } => write!(
|
||
f, "component {index} ({capability}) is companion-only but is not marked role=\"companion\""),
|
||
RoleError::StandaloneAsCompanion { index, capability } => write!(
|
||
f, "component {index} ({capability}) is standalone media, not a companion; carry it as a rendition"),
|
||
RoleError::RecordOrDerivationAsComponent { index, capability } => write!(
|
||
f, "component {index} ({capability}) is a record or a derivation, which names no payload format: no component of a WAI2"),
|
||
RoleError::UnboundCompanion { index, capability } => write!(
|
||
f, "companion {index} ({capability}) names no selectable rendition in companion_of"),
|
||
RoleError::IncompatibleBase { index, capability, base } => {
|
||
let bases = crate::codecs::enhancement_bases(capability);
|
||
if bases.is_empty() {
|
||
write!(f, "companion {index} ({capability}) does not apply to {base}; \
|
||
it describes a video payload (a wai.video.* capability, \
|
||
a neural video codec or a film)")
|
||
} else {
|
||
write!(f, "companion {index} ({capability}) does not apply to {base}; \
|
||
its base codecs are {}", bases.join(", "))
|
||
}
|
||
}
|
||
RoleError::EnhancementBaseNotUnique { index, capability, matches } => write!(
|
||
f, "enhancement layer {index} ({capability}) must bind to exactly one selectable video rendition; companion_of matches {matches} (name the base by companion_of_id)"),
|
||
RoleError::ComponentIdMalformed { index } => write!(
|
||
f, "component {index} has an id that is not 1 to 64 of a-z, 0-9, '_' and '-'"),
|
||
RoleError::DuplicateComponentId { index, first } => write!(
|
||
f, "component {index} repeats the id of component {first}"),
|
||
RoleError::CompanionIdUnresolved { index, id } => write!(
|
||
f, "companion {index} names companion_of_id '{id}', which no component has"),
|
||
RoleError::CompanionIdCapabilityMismatch { index, id, capability } => write!(
|
||
f, "companion {index} names companion_of_id '{id}', whose capability {capability} is not its companion_of"),
|
||
RoleError::RefinementOnlyAsFallback { capability } => write!(
|
||
f, "{capability} is refinement-only and cannot be a WAI1 fallback; it is never decoded on its own"),
|
||
RoleError::RefinementWithoutRefines { capability } => write!(
|
||
f, "{capability} is refinement-only and its manifest carries no refines (extensions/staged-delivery.md \u{a7}3)"),
|
||
RoleError::RefinementWithFallback { capability } => write!(
|
||
f, "{capability} is refinement-only, so its fallback must be null"),
|
||
RoleError::RefinesWithoutRefinement { capability } => write!(
|
||
f, "refines is carried with {capability}, which is not refinement-only"),
|
||
RoleError::RefinesMalformed { index: None, detail } => write!(f, "refines-malformed: {detail}"),
|
||
RoleError::RefinesMalformed { index: Some(i), detail } => write!(f, "component {i}: refines-malformed: {detail}"),
|
||
RoleError::RefinementOnlyAsComponent { index, capability } => write!(
|
||
f, "component {index} ({capability}) is refinement-only and is not carried with role=\"refinement\""),
|
||
RoleError::RefinesOnNonRefinement { index } => write!(
|
||
f, "component {index} carries refines but its role is not \"refinement\""),
|
||
RoleError::RefinementNotRefinementOnly { index, capability } => write!(
|
||
f, "component {index} has role=\"refinement\" but {capability} is not refinement-only"),
|
||
RoleError::RefinementIdentityMissing { index } => write!(
|
||
f, "refinement {index}, or an entry it names, carries no id or no sha256 (SPEC \u{a7}7.1)"),
|
||
RoleError::RefinementBaseMissing { index, base } => write!(
|
||
f, "refinement {index} names base '{base}', which is no selectable entry"),
|
||
RoleError::RefinementBaseNotAdmitted { index, capability, base } => write!(
|
||
f, "refinement {index} ({capability}) does not apply over {base} (SPEC \u{a7}5, Base admission)"),
|
||
RoleError::RefinementPrevMissing { index, producer } => write!(
|
||
f, "refinement {index} names producer '{producer}', which is neither a selectable entry nor a refinement before it"),
|
||
RoleError::RefinementBeforeSelectable { index, selectable } => write!(
|
||
f, "refinement {index} precedes selectable entry {selectable}; refinements follow every selectable entry"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for RoleError {}
|
||
|
||
/// An envelope that names a candidate capability ([`crate::codecs::is_candidate`]),
|
||
/// which an encoder MUST NOT emit (SPEC §5, *Candidate capabilities*). Returned by
|
||
/// [`Wai::check_candidates`] and [`WaiMulti::check_candidates`]. The envelope still
|
||
/// parses.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum CandidateError {
|
||
/// v1.0: a candidate named as the `WAI1` `capability`.
|
||
AsCapability { capability: String },
|
||
/// v1.0: a candidate named as the `WAI1` `fallback`.
|
||
AsFallback { capability: String },
|
||
/// v1.1: a `WAI2` component, in either role, whose capability is a candidate.
|
||
AsComponent { index: usize, capability: String },
|
||
}
|
||
|
||
const CANDIDATE_TAIL: &str = "is a candidate capability: no payload format is pinned for it, so an encoder MUST NOT emit it (SPEC \u{a7}5, Candidate capabilities)";
|
||
|
||
impl std::fmt::Display for CandidateError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
CandidateError::AsCapability { capability } => write!(f, "the capability {capability} {CANDIDATE_TAIL}"),
|
||
CandidateError::AsFallback { capability } => write!(f, "the fallback {capability} {CANDIDATE_TAIL}"),
|
||
CandidateError::AsComponent { index, capability } => write!(f, "component {index} ({capability}) {CANDIDATE_TAIL}"),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for CandidateError {}
|
||
|
||
/// [`Wai::check_candidates`] over a `WAI1` manifest's two fields: the
|
||
/// `capability`, then the `fallback`. For a writer that holds the manifest fields
|
||
/// rather than a [`Wai`] (the wasm `pack`).
|
||
pub fn check_candidates_v1(capability: &str, fallback: Option<&str>) -> Result<(), CandidateError> {
|
||
if crate::codecs::is_candidate(capability) {
|
||
return Err(CandidateError::AsCapability { capability: capability.to_string() });
|
||
}
|
||
match fallback {
|
||
Some(fb) if crate::codecs::is_candidate(fb) => Err(CandidateError::AsFallback { capability: fb.to_string() }),
|
||
_ => Ok(()),
|
||
}
|
||
}
|
||
|
||
/// [`WaiMulti::check_candidates`] over a `WAI2` component table's capabilities,
|
||
/// in table order. For a writer that holds the table rather than a [`WaiMulti`]
|
||
/// (the wasm `packMulti`).
|
||
pub fn check_component_candidates<'a>(capabilities: impl IntoIterator<Item = &'a str>) -> Result<(), CandidateError> {
|
||
match capabilities.into_iter().enumerate().find(|(_, c)| crate::codecs::is_candidate(c)) {
|
||
Some((index, capability)) => Err(CandidateError::AsComponent { index, capability: capability.to_string() }),
|
||
None => Ok(()),
|
||
}
|
||
}
|
||
|
||
/// Whether a `WAI2` manifest's payloads are plaintext: no `security` member, or one
|
||
/// whose `encryption` is absent, `null` or `"none"` (SPEC §6). A `security` member
|
||
/// that is not a security object is not plaintext: a sink treats the payloads as
|
||
/// ciphertext it cannot open, and an encoder does not write it
|
||
/// ([`manifest_v2_security`]).
|
||
pub fn manifest_v2_plaintext(m: &ManifestV2) -> bool {
|
||
matches!(manifest_v2_security(m), Ok(s) if s.as_ref().is_none_or(Security::is_plaintext))
|
||
}
|
||
|
||
/// A `WAI2` manifest's `security` member (SPEC §6.1, §7.1): `None` when absent or
|
||
/// `null`; the object; or why the member is not a security object (`true`, a string,
|
||
/// an array, an `encryption` that is not a string, …), which an encoder does not write
|
||
/// ([`WaiMulti::to_emit_bytes`]).
|
||
pub fn manifest_v2_security(m: &ManifestV2) -> Result<Option<Security>, String> {
|
||
match m.extra.get("security") {
|
||
None | Some(Value::Null) => Ok(None),
|
||
Some(v) => serde_json::from_value::<Security>(v.clone()).map(Some)
|
||
.map_err(|e| format!("the manifest's `security` member {v} is not a security object (SPEC §6.1): {e}")),
|
||
}
|
||
}
|
||
|
||
/// Why a payload is not the format SPEC §5 pins its capability to (*Payloads of the
|
||
/// WAI-native integer codecs*, `extensions/integer-payloads.md`): the capability, the
|
||
/// code the extension's §8 names and what failed. The payload is parsed as a sink parses
|
||
/// it (§2–§6), so every code a parse can give is possible here: `bad_magic` (none of the
|
||
/// four formats; a JSON form lands here), `format_capability_mismatch`,
|
||
/// `keyframe_capability_mismatch`, `version`, `reserved_nonzero`, `field_range`,
|
||
/// `too_large`, `cdf_malformed`, `stream_length`, `nested_length` and `trailing`. What
|
||
/// only a decode checks, against a parameter set and through the streams (§7), is not.
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct FramingError {
|
||
pub capability: String,
|
||
pub code: &'static str,
|
||
pub detail: &'static str,
|
||
}
|
||
|
||
impl std::fmt::Display for FramingError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
let want = if crate::codecs::is_refinement_only(&self.capability) {
|
||
Some("WIR1".to_string())
|
||
} else {
|
||
crate::codecs::pinned_int_payload(&self.capability).map(|m| String::from_utf8_lossy(&m).into_owned())
|
||
};
|
||
write!(f, "the payload is not the {} payload SPEC \u{a7}5 pins {} to ({}: {}; extensions/integer-payloads.md)",
|
||
want.as_deref().unwrap_or("?"), self.capability, self.code, self.detail)
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for FramingError {}
|
||
|
||
/// Check that `payload` is the format SPEC §5 pins `capability`'s payload to
|
||
/// ([`crate::codecs::PINNED_INT_PAYLOADS`]): it is parsed with the wire parser a sink
|
||
/// runs (`int_payload::wire`, with the capability's keyframe kind for `WIV1`), which
|
||
/// checks every rule of the extension's §2–§6 in its order and reports the first that
|
||
/// fails ([`FramingError`]). `Ok` for a capability with no pinned integer format. It
|
||
/// needs no parameter set and no integer decoder, so every emit path runs it
|
||
/// ([`Wai::to_emit_bytes`], [`WaiMulti::to_emit_bytes`], `wai wrap`, the browser's
|
||
/// writers). `payload` is plaintext: an encrypted envelope's payload is checked after
|
||
/// it is decrypted (SPEC §6.4), and its emit paths skip the check.
|
||
pub fn check_int_payload_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> {
|
||
use crate::int_wire as w;
|
||
if crate::codecs::is_refinement_only(capability) {
|
||
return check_refinement_framing(capability, payload);
|
||
}
|
||
let parsed = match crate::codecs::pinned_int_payload(capability) {
|
||
None => return Ok(()),
|
||
Some(w::MAGIC_WIV1) => w::parse_wiv1(payload, Some((capability == crate::codecs::CAP_VIDEO_INT_HYPER) as u8)).map(|_| ()),
|
||
Some(w::MAGIC_WIH1) => w::parse_wih1(payload).map(|_| ()),
|
||
Some(w::MAGIC_WIA1) => w::parse_wia1(payload).map(|_| ()),
|
||
Some(_) => w::parse_wis1(payload).map(|_| ()),
|
||
};
|
||
parsed.map_err(|e| FramingError { capability: capability.to_string(), code: e.code(), detail: e.detail })
|
||
}
|
||
|
||
/// The framing check of a refinement-only capability's payload: `WIR1`, parsed for
|
||
/// the capability's unit kind (`extensions/staged-delivery.md` Appendix A.1). A build
|
||
/// without the `staged` feature has no `WIR1` parser, so it refuses to emit one
|
||
/// (`unsupported_capability`).
|
||
#[cfg(feature = "staged")]
|
||
fn check_refinement_framing(capability: &str, payload: &[u8]) -> Result<(), FramingError> {
|
||
let fail = |code: &'static str, detail: &'static str| FramingError { capability: capability.to_string(), code, detail };
|
||
let kind = crate::staged::unit_kind(capability).ok_or(fail("unsupported_capability", "no unit kind for this refinement"))?;
|
||
crate::staged::wir1::parse(payload, kind).map(|_| ()).map_err(|e| fail(e.code(), e.detail))
|
||
}
|
||
|
||
#[cfg(not(feature = "staged"))]
|
||
fn check_refinement_framing(capability: &str, _: &[u8]) -> Result<(), FramingError> {
|
||
Err(FramingError {
|
||
capability: capability.to_string(),
|
||
code: "unsupported_capability",
|
||
detail: "this build cannot check a WIR1 payload (feature staged)",
|
||
})
|
||
}
|
||
|
||
/// What an integer payload's header declares about its decode, read before any
|
||
/// symbol is decoded and with no parameter set (`extensions/integer-payloads.md` §7).
|
||
/// A sink applies its own size policy to it and refuses a payload past that policy
|
||
/// before decoding; `int_payload::declared_cost` adds what the parameter set fixes
|
||
/// (the networks' buffers, a `WIH1` image's size).
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub struct DeclaredSize {
|
||
/// The decoded output's bytes, where the header alone fixes it: the frames of a
|
||
/// `WIV1` (`n_frames·h·w·3`), the PCM of a `WIA1` (`2·t_out`), the attribute tensor
|
||
/// of a `WIS1` (`attr·s·s`). `None` for a `WIH1`, whose image size is its
|
||
/// synthesis network's.
|
||
pub output_bytes: Option<u64>,
|
||
/// The symbols the payload's streams code: one entropy-decode step each.
|
||
pub symbols: u64,
|
||
/// The most bytes the reference decode holds at once beyond the payload, where the
|
||
/// header alone fixes it: a `WIV1` with keyframe 0
|
||
/// ([`crate::int_wire::Wiv1::keyframe0_working_bytes`]). `None` where a network
|
||
/// runs, whose buffers the parameter set sizes.
|
||
pub working_bytes: Option<u64>,
|
||
/// The networks' multiply-accumulates, where the header alone fixes them: 0 for a
|
||
/// `WIV1` with keyframe 0, which runs no network; `None` otherwise.
|
||
pub macs: Option<u128>,
|
||
}
|
||
|
||
/// [`DeclaredSize`] for `payload` under `capability`, after the parse
|
||
/// [`check_int_payload_framing`] makes. `Ok(None)` for a capability with no pinned
|
||
/// integer format.
|
||
pub fn declared_size(capability: &str, payload: &[u8]) -> Result<Option<DeclaredSize>, FramingError> {
|
||
use crate::int_wire as w;
|
||
let fail = |e: w::WireError| FramingError { capability: capability.to_string(), code: e.code(), detail: e.detail };
|
||
let wih1 = |k: &w::Wih1<'_>| k.n_z as u64 * k.zh as u64 * k.zw as u64 + k.m as u64 * k.sh as u64 * k.sw as u64;
|
||
Ok(Some(match crate::codecs::pinned_int_payload(capability) {
|
||
None => return Ok(None),
|
||
Some(w::MAGIC_WIV1) => {
|
||
let v = w::parse_wiv1(payload, Some((capability == crate::codecs::CAP_VIDEO_INT_HYPER) as u8)).map_err(fail)?;
|
||
let frame = v.h as u64 * v.w as u64 * 3;
|
||
let output_bytes = Some(frame * v.n_frames as u64);
|
||
match v.wih1 {
|
||
None => DeclaredSize { output_bytes, symbols: frame * v.n_frames as u64, working_bytes: Some(v.keyframe0_working_bytes()), macs: Some(0) },
|
||
Some(k) => {
|
||
let symbols = frame * (v.n_frames as u64 - 1) + wih1(&w::parse_wih1(k).map_err(fail)?);
|
||
DeclaredSize { output_bytes, symbols, working_bytes: None, macs: None }
|
||
}
|
||
}
|
||
}
|
||
Some(w::MAGIC_WIH1) => DeclaredSize { output_bytes: None, symbols: wih1(&w::parse_wih1(payload).map_err(fail)?), working_bytes: None, macs: None },
|
||
Some(w::MAGIC_WIA1) => {
|
||
let a = w::parse_wia1(payload).map_err(fail)?;
|
||
DeclaredSize { output_bytes: Some(2 * a.t_out as u64), symbols: a.n_ch as u64 * a.lat_len as u64, working_bytes: None, macs: None }
|
||
}
|
||
Some(_) => {
|
||
let v = w::parse_wis1(payload).map_err(fail)?;
|
||
DeclaredSize { output_bytes: Some(v.attr as u64 * v.s as u64 * v.s as u64), symbols: v.c as u64 * v.sh as u64 * v.sw as u64, working_bytes: None, macs: None }
|
||
}
|
||
}))
|
||
}
|
||
|
||
/// Why [`Wai::to_emit_bytes`] or [`WaiMulti::to_emit_bytes`] wrote nothing.
|
||
#[derive(Debug)]
|
||
#[non_exhaustive]
|
||
pub enum EmitError {
|
||
/// The envelope names a candidate capability (SPEC §5).
|
||
Candidate(CandidateError),
|
||
/// The envelope did not serialise.
|
||
Serialize(std::io::Error),
|
||
/// A payload is not the format SPEC §5 pins its integer capability to
|
||
/// ([`check_int_payload_framing`]): the `WAI2` component's index, or `None` for a
|
||
/// `WAI1`'s payload.
|
||
IntPayload { component: Option<usize>, error: FramingError },
|
||
/// A `WAI2` manifest's `security` member is not a security object
|
||
/// ([`manifest_v2_security`]): an encoder that writes one could claim an
|
||
/// encryption no sink can read, or none.
|
||
Security(String),
|
||
}
|
||
|
||
impl std::fmt::Display for EmitError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
EmitError::Candidate(e) => e.fmt(f),
|
||
EmitError::Serialize(e) => write!(f, "envelope serialize: {e}"),
|
||
EmitError::IntPayload { component: Some(i), error } => write!(f, "component {i}: {error}"),
|
||
EmitError::IntPayload { component: None, error } => error.fmt(f),
|
||
EmitError::Security(why) => f.write_str(why),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for EmitError {}
|
||
|
||
/// A `WAI2` manifest (SPEC §7.1). Build one with [`ManifestV2::new`].
|
||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||
#[non_exhaustive]
|
||
pub struct ManifestV2 {
|
||
pub wai: String, // "1.1"
|
||
pub media: String,
|
||
pub intent: String,
|
||
pub renditions: Vec<RenditionMeta>,
|
||
#[serde(default)]
|
||
pub target: Value,
|
||
/// Members this revision does not define, kept so they survive a round trip.
|
||
#[serde(flatten)]
|
||
pub extra: Map<String, Value>,
|
||
}
|
||
|
||
impl ManifestV2 {
|
||
/// The index of the base a companion at `index` binds to, when it binds to one
|
||
/// entry: the entry `companion_of_id` names, or the one selectable entry of
|
||
/// capability `companion_of`.
|
||
fn bound_base(&self, index: usize) -> Option<usize> {
|
||
let metas = &self.renditions;
|
||
let m = metas.get(index)?;
|
||
if let Some(id) = m.companion_of_id.as_deref() {
|
||
return metas.iter().position(|b| b.id.as_deref() == Some(id));
|
||
}
|
||
let target = m.companion_of.as_deref()?;
|
||
let mut it = metas.iter().enumerate().filter(|(_, b)| b.is_selectable() && b.capability == target);
|
||
match (it.next(), it.next()) {
|
||
(Some((i, _)), None) => Some(i),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// SPEC §7.1, *Refinement components*, for the entry at `index`, in this order:
|
||
/// its capability is refinement-only; it carries `id` and `sha256`; its `refines`
|
||
/// is well formed with component ids; `refines.base` names a selectable entry with
|
||
/// an `id` and a `sha256`; its capability admits that entry's (SPEC §5, *Base
|
||
/// admission*); every producer of `prev` names a selectable entry or a refinement
|
||
/// before it, with an `id` and a `sha256`; and it follows every selectable entry.
|
||
fn check_refinement(&self, index: usize) -> Result<(), RoleError> {
|
||
let metas = &self.renditions;
|
||
let m = &metas[index];
|
||
if !crate::codecs::is_refinement_only(&m.capability) {
|
||
return Err(RoleError::RefinementNotRefinementOnly { index, capability: m.capability.clone() });
|
||
}
|
||
if m.id.is_none() || m.sha256.is_none() {
|
||
return Err(RoleError::RefinementIdentityMissing { index });
|
||
}
|
||
let refines = match m.refines.as_ref().map(|v| Refines::from_value(v, RefForm::ComponentId)) {
|
||
Some(Ok(r)) => r,
|
||
Some(Err(e)) => return Err(RoleError::RefinesMalformed { index: Some(index), detail: e.0 }),
|
||
None => return Err(RoleError::RefinesMalformed { index: Some(index), detail: "a refinement carries no refines" }),
|
||
};
|
||
let by_id = |id: &str| metas.iter().position(|e| e.id.as_deref() == Some(id));
|
||
let Some(b) = by_id(&refines.base).filter(|&b| metas[b].is_selectable()) else {
|
||
return Err(RoleError::RefinementBaseMissing { index, base: refines.base.clone() });
|
||
};
|
||
if metas[b].sha256.is_none() {
|
||
return Err(RoleError::RefinementIdentityMissing { index });
|
||
}
|
||
if !crate::codecs::admits(&m.capability, &metas[b].capability) {
|
||
return Err(RoleError::RefinementBaseNotAdmitted { index, capability: m.capability.clone(), base: metas[b].capability.clone() });
|
||
}
|
||
for p in refines.producers() {
|
||
let ok = by_id(p).filter(|&e| metas[e].is_selectable() || (e < index && metas[e].is_refinement()));
|
||
match ok {
|
||
None => return Err(RoleError::RefinementPrevMissing { index, producer: p.to_string() }),
|
||
Some(e) if metas[e].sha256.is_none() => return Err(RoleError::RefinementIdentityMissing { index }),
|
||
Some(_) => {}
|
||
}
|
||
}
|
||
if let Some(selectable) = (index + 1..metas.len()).find(|&j| metas[j].is_selectable()) {
|
||
return Err(RoleError::RefinementBeforeSelectable { index, selectable });
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Check the component table against SPEC §5 / §7.1, using the capability
|
||
/// registry, in component order (the first violation is returned):
|
||
///
|
||
/// - every `id` is a component id ([`is_component_id`]) and unique
|
||
/// ([`RoleError::ComponentIdMalformed`], [`RoleError::DuplicateComponentId`]);
|
||
/// - a companion-only capability is carried with `role = "companion"`
|
||
/// ([`RoleError::CompanionOnlyAsRendition`]);
|
||
/// - a `role = "refinement"` entry obeys *Refinement components*
|
||
/// ([`Self::check_refinement`]'s rules); `refines` sits on no other entry, and a
|
||
/// refinement-only capability on no other role
|
||
/// ([`RoleError::RefinesOnNonRefinement`], [`RoleError::RefinementOnlyAsComponent`]);
|
||
///
|
||
/// and, for each `role = "companion"` component, SPEC §7.1's three rules:
|
||
///
|
||
/// 1. its capability is companion-only or unregistered: not a payload
|
||
/// ([`RoleError::StandaloneAsCompanion`]); a record or a derivation is no component
|
||
/// in any role ([`RoleError::RecordOrDerivationAsComponent`], checked first);
|
||
/// 2. `companion_of` is present ([`RoleError::UnboundCompanion`]) and names a
|
||
/// capability it applies to ([`RoleError::IncompatibleBase`]); a
|
||
/// `companion_of_id` resolves to an entry ([`RoleError::CompanionIdUnresolved`])
|
||
/// whose capability is `companion_of`
|
||
/// ([`RoleError::CompanionIdCapabilityMismatch`]);
|
||
/// 3. a selectable rendition of that capability is present
|
||
/// ([`RoleError::UnboundCompanion`]) — with `companion_of_id`, exactly that
|
||
/// entry; for an enhancement layer bound by capability alone, exactly one
|
||
/// ([`RoleError::EnhancementBaseNotUnique`]); and a base takes at most one
|
||
/// enhancement layer, checked by index ([`RoleError::DuplicateEnhancementLayer`]).
|
||
///
|
||
/// A descriptor companion (HDR dynamic metadata) bound by capability may bind
|
||
/// to several renditions of one capability, and several descriptors may bind
|
||
/// to one base. A companion whose capability this revision does not register is
|
||
/// held to rule 3's presence check only.
|
||
pub fn check_component_roles(&self) -> Result<(), RoleError> {
|
||
let metas = &self.renditions;
|
||
for (index, m) in metas.iter().enumerate() {
|
||
if let Some(id) = m.id.as_deref() {
|
||
if !is_component_id(id) {
|
||
return Err(RoleError::ComponentIdMalformed { index });
|
||
}
|
||
if let Some(first) = metas[..index].iter().position(|e| e.id.as_deref() == Some(id)) {
|
||
return Err(RoleError::DuplicateComponentId { index, first });
|
||
}
|
||
}
|
||
}
|
||
for (index, m) in metas.iter().enumerate() {
|
||
let capability = m.capability.clone();
|
||
// A record or a derivation names no payload format: no component in any role.
|
||
if matches!(crate::codecs::capability_kind(&m.capability), Some(crate::codecs::Kind::Record | crate::codecs::Kind::Derivation)) {
|
||
return Err(RoleError::RecordOrDerivationAsComponent { index, capability });
|
||
}
|
||
if m.is_refinement() {
|
||
self.check_refinement(index)?;
|
||
continue;
|
||
}
|
||
if m.refines.is_some() {
|
||
return Err(RoleError::RefinesOnNonRefinement { index });
|
||
}
|
||
if crate::codecs::is_refinement_only(&m.capability) {
|
||
return Err(RoleError::RefinementOnlyAsComponent { index, capability });
|
||
}
|
||
let companion_only = crate::codecs::is_companion_only(&m.capability);
|
||
if !m.is_companion() {
|
||
if companion_only {
|
||
return Err(RoleError::CompanionOnlyAsRendition { index, capability });
|
||
}
|
||
continue;
|
||
}
|
||
// Rule 1: only a companion-only (or unregistered) capability is a companion; a
|
||
// payload is standalone media (a record or a derivation was refused above).
|
||
if !companion_only && crate::codecs::capability_kind(&m.capability).is_some() {
|
||
return Err(RoleError::StandaloneAsCompanion { index, capability });
|
||
}
|
||
let Some(target) = m.companion_of.as_deref() else {
|
||
return Err(RoleError::UnboundCompanion { index, capability });
|
||
};
|
||
// Rule 2: the named base is one this companion applies to.
|
||
if companion_only && !crate::codecs::companion_applies_to(&m.capability, target) {
|
||
let base = target.to_string();
|
||
return Err(RoleError::IncompatibleBase { index, capability, base });
|
||
}
|
||
// Rule 2, by id: the named entry exists and has the named capability.
|
||
if let Some(id) = m.companion_of_id.as_deref() {
|
||
let Some(b) = metas.iter().position(|e| e.id.as_deref() == Some(id)) else {
|
||
return Err(RoleError::CompanionIdUnresolved { index, id: id.to_string() });
|
||
};
|
||
if metas[b].capability != target {
|
||
return Err(RoleError::CompanionIdCapabilityMismatch {
|
||
index, id: id.to_string(), capability: metas[b].capability.clone() });
|
||
}
|
||
// Rule 3: that entry is a selectable rendition.
|
||
if !metas[b].is_selectable() {
|
||
return Err(RoleError::UnboundCompanion { index, capability });
|
||
}
|
||
} else {
|
||
// Rule 3: the base is present, and unique for an enhancement layer.
|
||
let bases = metas.iter()
|
||
.filter(|b| b.is_selectable() && b.capability == target)
|
||
.count();
|
||
if crate::codecs::is_enhancement_layer(&m.capability) {
|
||
if bases != 1 {
|
||
return Err(RoleError::EnhancementBaseNotUnique { index, capability, matches: bases });
|
||
}
|
||
} else if bases == 0 {
|
||
return Err(RoleError::UnboundCompanion { index, capability });
|
||
}
|
||
}
|
||
// At most one enhancement layer per base, by index.
|
||
if crate::codecs::is_enhancement_layer(&m.capability) {
|
||
let base_index = self.bound_base(index);
|
||
let earlier = (0..index).find(|&e| {
|
||
metas[e].is_companion()
|
||
&& crate::codecs::is_enhancement_layer(&metas[e].capability)
|
||
&& self.bound_base(e).is_some()
|
||
&& self.bound_base(e) == base_index
|
||
});
|
||
if let Some(first) = earlier {
|
||
let base = target.to_string();
|
||
return Err(RoleError::DuplicateEnhancementLayer { index, capability, base, first });
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// A `"1.1"` manifest with a `null` target.
|
||
pub fn new(media: impl Into<String>, intent: impl Into<String>, renditions: Vec<RenditionMeta>) -> Self {
|
||
Self {
|
||
wai: "1.1".into(),
|
||
media: media.into(),
|
||
intent: intent.into(),
|
||
renditions,
|
||
target: Value::Null,
|
||
extra: Map::new(),
|
||
}
|
||
}
|
||
/// Set `target`.
|
||
pub fn with_target(mut self, target: Value) -> Self {
|
||
self.target = target;
|
||
self
|
||
}
|
||
}
|
||
|
||
/// One component to pack into a `WAI2`: its entry and its payload.
|
||
#[derive(Debug, Clone)]
|
||
#[non_exhaustive]
|
||
pub struct Rendition {
|
||
pub meta: RenditionMeta,
|
||
pub payload: Vec<u8>,
|
||
}
|
||
|
||
impl Rendition {
|
||
/// A selectable media rendition (an alternative of the same content).
|
||
pub fn media(capability: impl Into<String>, kind: impl Into<String>, payload: Vec<u8>) -> Self {
|
||
Self { meta: RenditionMeta::new(capability, kind), payload }
|
||
}
|
||
|
||
/// A companion descriptor bound to the component of capability
|
||
/// `companion_of` — e.g. an HDR10+ metadata block for a `wai.video.av1`
|
||
/// payload, carried in the same envelope.
|
||
pub fn companion(capability: impl Into<String>, payload: Vec<u8>, companion_of: impl Into<String>) -> Self {
|
||
Self { meta: RenditionMeta::new(capability, "descriptor").companion_to(companion_of), payload }
|
||
}
|
||
|
||
/// A component from an entry and its payload.
|
||
pub fn from_meta(meta: RenditionMeta, payload: Vec<u8>) -> Self {
|
||
Self { meta, payload }
|
||
}
|
||
|
||
/// Pin the parameter set the payload was coded against.
|
||
pub fn with_prior(mut self, prior: Prior) -> Self {
|
||
self.meta.prior = Some(prior);
|
||
self
|
||
}
|
||
/// Give the component an id.
|
||
pub fn with_id(mut self, id: impl Into<String>) -> Self {
|
||
self.meta.id = Some(id.into());
|
||
self
|
||
}
|
||
/// State the payload's SHA-256, computed from the payload.
|
||
pub fn with_payload_digest(mut self) -> Self {
|
||
self.meta.sha256 = Some(crate::pin::sha256_hex(&self.payload));
|
||
self
|
||
}
|
||
/// Bind this companion to the component whose `id` is `id`.
|
||
pub fn with_companion_of_id(mut self, id: impl Into<String>) -> Self {
|
||
self.meta.companion_of_id = Some(id.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
pub struct WaiMulti {
|
||
pub manifest: ManifestV2,
|
||
pub renditions: Vec<Vec<u8>>, // payload bytes, parallel to manifest.renditions
|
||
}
|
||
|
||
/// Why [`WaiMulti::pick`] skipped an entry (SPEC §7.1 step 5).
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
#[non_exhaustive]
|
||
pub enum Skip {
|
||
/// A companion, or a capability that is not [`crate::codecs::is_dispatchable`]
|
||
/// (companion-only, a candidate, a record or a derivation): never presented as
|
||
/// media.
|
||
NotSelectable,
|
||
/// The sink does not advertise the capability (`missing_capability`).
|
||
NotEligible,
|
||
/// The entry's payload does not match its `sha256`
|
||
/// (`component_digest_mismatch`).
|
||
ComponentDigest,
|
||
/// The entry's pin was refused.
|
||
Pin(PinRefusal),
|
||
/// A policy that serves only sound `replicate` claims left the entry out:
|
||
/// SPEC §7's rule refuses `replicate` through it (`replicate_*`).
|
||
Replicate(crate::codecs::ReplicateRefusal),
|
||
/// The manifest declares an encryption the sink cannot open (SPEC §6.4 step 2,
|
||
/// `unsupported_encryption`): every entry's payload is ciphertext here.
|
||
Encrypted,
|
||
}
|
||
|
||
impl Skip {
|
||
/// The reason: `not_selectable`, `missing_capability`,
|
||
/// `component_digest_mismatch`, the pin's reason, the `replicate_*` reason, or
|
||
/// `unsupported_encryption`.
|
||
pub fn code(&self) -> &'static str {
|
||
match self {
|
||
Self::NotSelectable => "not_selectable",
|
||
Self::NotEligible => "missing_capability",
|
||
Self::ComponentDigest => "component_digest_mismatch",
|
||
Self::Pin(r) => r.code(),
|
||
Self::Replicate(r) => r.code(),
|
||
Self::Encrypted => "unsupported_encryption",
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::fmt::Display for Skip {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
Self::Pin(r) => write!(f, "{}: {r}", r.code()),
|
||
Self::Replicate(r) => write!(f, "{}: {r}", r.code()),
|
||
other => f.write_str(other.code()),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The rendition [`WaiMulti::pick`] chose: its index, entry and payload, what the
|
||
/// pin verifier returned for it (when a pin applied), and the entries skipped
|
||
/// before it with their reasons.
|
||
#[derive(Debug)]
|
||
pub struct Picked<'a, V> {
|
||
pub index: usize,
|
||
pub meta: &'a RenditionMeta,
|
||
pub payload: &'a [u8],
|
||
pub verified: Option<V>,
|
||
pub skipped: Vec<(usize, Skip)>,
|
||
}
|
||
|
||
impl WaiMulti {
|
||
pub fn new(media: impl Into<String>, intent: impl Into<String>,
|
||
target: Value, renditions: Vec<Rendition>) -> Self {
|
||
let (metas, payloads): (Vec<RenditionMeta>, Vec<Vec<u8>>) =
|
||
renditions.into_iter().map(|r| (r.meta, r.payload)).unzip();
|
||
Self { manifest: ManifestV2::new(media, intent, metas).with_target(target), renditions: payloads }
|
||
}
|
||
|
||
/// An envelope from a manifest and the payloads parallel to its entries. Every
|
||
/// member of the manifest is kept, including ones this revision does not define.
|
||
pub fn from_parts(manifest: ManifestV2, renditions: Vec<Vec<u8>>) -> std::io::Result<Self> {
|
||
if manifest.renditions.len() != renditions.len() {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("manifest has {} renditions but payload list has {}",
|
||
manifest.renditions.len(), renditions.len())));
|
||
}
|
||
Ok(Self { manifest, renditions })
|
||
}
|
||
|
||
pub fn write<P: AsRef<Path>>(&self, path: P) -> std::io::Result<usize> {
|
||
let bytes = self.to_bytes()?;
|
||
fs::write(path, &bytes)?;
|
||
Ok(bytes.len())
|
||
}
|
||
|
||
pub fn read<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
|
||
let bytes = fs::read(path)?;
|
||
Self::from_bytes(&bytes)
|
||
}
|
||
|
||
/// The envelope's bytes. Refuses a manifest longer than SPEC §2's reader limit
|
||
/// (`manifest_too_large`), and a payload, or the payloads' total, that does not fit
|
||
/// a `u32` field.
|
||
pub fn to_bytes(&self) -> std::io::Result<Vec<u8>> {
|
||
self.to_bytes_within(WriteLimits::SPEC)
|
||
}
|
||
|
||
/// [`Self::to_bytes`] within `lim`.
|
||
fn to_bytes_within(&self, lim: WriteLimits) -> std::io::Result<Vec<u8>> {
|
||
if self.manifest.renditions.len() != self.renditions.len() {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("manifest has {} renditions but payload list has {}",
|
||
self.manifest.renditions.len(), self.renditions.len())));
|
||
}
|
||
if self.renditions.len() > u16::MAX as usize {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
"too many renditions for u16 count field"));
|
||
}
|
||
let m = manifest_json(&self.manifest, lim.manifest)?;
|
||
let table_bytes = 2 + self.renditions.len() * 8;
|
||
let payload_bytes: usize = self.renditions.iter().map(|p| p.len()).sum();
|
||
|
||
let mut out = Vec::with_capacity(8 + m.len() + table_bytes + payload_bytes);
|
||
out.extend_from_slice(MAGIC_V2);
|
||
out.extend_from_slice(&(m.len() as u32).to_le_bytes());
|
||
out.extend_from_slice(&m);
|
||
out.extend_from_slice(&(self.renditions.len() as u16).to_le_bytes());
|
||
|
||
// Rendition table: (u32 offset, u32 length); offsets are relative to
|
||
// the start of the payloads block (the byte right after the table).
|
||
let mut off: u32 = 0;
|
||
for (i, p) in self.renditions.iter().enumerate() {
|
||
let len = u32_field(p.len(), lim.field, &format!("rendition {i}'s payload"))?;
|
||
out.extend_from_slice(&off.to_le_bytes());
|
||
out.extend_from_slice(&len.to_le_bytes());
|
||
off = off.checked_add(len).filter(|&o| o <= lim.field).ok_or_else(|| std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData, "payload sum overflows u32"))?;
|
||
}
|
||
for p in &self.renditions {
|
||
out.extend_from_slice(p);
|
||
}
|
||
Ok(out)
|
||
}
|
||
|
||
pub fn from_bytes(bytes: &[u8]) -> std::io::Result<Self> {
|
||
let need = |off: usize, n: usize, what: &str| -> std::io::Result<()> {
|
||
if off.saturating_add(n) > bytes.len() {
|
||
Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("truncated WAI2 ({what} needs {n} bytes at offset {off}, have {})",
|
||
bytes.len())))
|
||
} else { Ok(()) }
|
||
};
|
||
need(0, 4, "magic")?;
|
||
if &bytes[..4] != MAGIC_V2 {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
"not a WAI2 file (magic mismatch)"));
|
||
}
|
||
need(4, 4, "manifest length")?;
|
||
let ml = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||
check_manifest_len(ml as u64)
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
need(8, ml, "manifest body")?;
|
||
check_unique_members(&bytes[8..8 + ml])
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
let manifest: ManifestV2 = serde_json::from_slice(&bytes[8..8 + ml])
|
||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||
|
||
let mut o = 8 + ml;
|
||
need(o, 2, "rendition count")?;
|
||
let n = u16::from_le_bytes(bytes[o..o + 2].try_into().unwrap()) as usize;
|
||
o += 2;
|
||
if n != manifest.renditions.len() {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("rendition table count {} != manifest renditions {}",
|
||
n, manifest.renditions.len())));
|
||
}
|
||
need(o, n * 8, "rendition table")?;
|
||
let mut entries: Vec<(u32, u32)> = Vec::with_capacity(n);
|
||
for i in 0..n {
|
||
let off = u32::from_le_bytes(bytes[o + i*8..o + i*8 + 4].try_into().unwrap());
|
||
let len = u32::from_le_bytes(bytes[o + i*8 + 4..o + i*8 + 8].try_into().unwrap());
|
||
entries.push((off, len));
|
||
}
|
||
o += n * 8;
|
||
let payload_block_start = o;
|
||
// In u64, so the sum cannot wrap where usize is 32 bits (wasm32): at most
|
||
// u16::MAX lengths of at most u32::MAX each.
|
||
let total_payloads: u64 = entries.iter().map(|(_, l)| u64::from(*l)).sum();
|
||
if (payload_block_start as u64).saturating_add(total_payloads) > bytes.len() as u64 {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("truncated WAI2 (payload block needs {total_payloads} bytes at offset \
|
||
{payload_block_start}, have {})", bytes.len())));
|
||
}
|
||
|
||
// Validate offsets: each entry's (off, len) must lie within the
|
||
// payload block and not overlap previous entries' ranges (we
|
||
// require monotonic contiguous packing to keep parsing simple).
|
||
let mut expected = 0u32;
|
||
let mut renditions = Vec::with_capacity(n);
|
||
for (off, len) in &entries {
|
||
if *off != expected {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("rendition offset {off} not contiguous (expected {expected})")));
|
||
}
|
||
// The offsets are contiguous and their lengths sum within the file
|
||
// (checked above), so these cannot overflow; checked all the same.
|
||
let range = payload_block_start.checked_add(*off as usize)
|
||
.and_then(|s| s.checked_add(*len as usize).map(|e| (s, e)));
|
||
let Some((start, end)) = range else {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, "offset arithmetic overflows"));
|
||
};
|
||
if end > bytes.len() {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
format!("rendition byte range [{start}, {end}) exceeds file length {}",
|
||
bytes.len())));
|
||
}
|
||
renditions.push(bytes[start..end].to_vec());
|
||
expected = expected.checked_add(*len).ok_or_else(|| std::io::Error::new(
|
||
std::io::ErrorKind::InvalidData, "offset arithmetic overflows"))?;
|
||
}
|
||
Ok(Self { manifest, renditions })
|
||
}
|
||
|
||
/// SPEC §7.1 selection at a sink that advertises the capabilities `advertises`
|
||
/// accepts and verifies pins with `verify` (SPEC §3.1). Walks the entries in
|
||
/// order and picks the first that is selectable ([`RenditionMeta::is_selectable`]:
|
||
/// never a companion, a companion-only capability, a candidate, a record or a
|
||
/// derivation), whose capability is advertised (a string this revision does not
|
||
/// register is selected when `advertises` accepts it, SPEC §4, §8), whose
|
||
/// payload matches its `sha256` when it states one, and whose `prior`, where a
|
||
/// pin applies ([`pin_applies_to`]), `verify` accepts. An entry whose pin is
|
||
/// refused is skipped as if its capability were not advertised, and its reason
|
||
/// kept. When nothing is picked the envelope is inert at this sink, and the
|
||
/// error gives every entry's index and reason. An envelope whose manifest declares
|
||
/// an encryption other than `"none"` is inert before any entry is considered (SPEC
|
||
/// §6.4 step 2; this dispatcher opens no scheme): every entry is
|
||
/// [`Skip::Encrypted`].
|
||
pub fn pick<V>(
|
||
&self,
|
||
advertises: impl Fn(&str) -> bool,
|
||
verify: impl FnMut(&str, &Prior) -> Result<V, PinRefusal>,
|
||
) -> Result<Picked<'_, V>, Vec<(usize, Skip)>> {
|
||
self.pick_where(|_, m| if advertises(&m.capability) { Ok(()) } else { Err(Skip::NotEligible) }, verify)
|
||
}
|
||
|
||
/// [`Self::pick`] at a sink that advertises every capability — the first entry
|
||
/// in deployer-preferred order that passes the component digest and pin checks.
|
||
pub fn primary<V>(
|
||
&self,
|
||
verify: impl FnMut(&str, &Prior) -> Result<V, PinRefusal>,
|
||
) -> Result<Picked<'_, V>, Vec<(usize, Skip)>> {
|
||
self.pick(|_| true, verify)
|
||
}
|
||
|
||
/// [`Self::pick`] with a per-entry eligibility test in place of the
|
||
/// capability test: a deployer policy MAY restrict the entries a sink
|
||
/// considers (SPEC §7.1 step 2), and gives the reason it leaves one out; every
|
||
/// entry it considers passes the digest and pin checks.
|
||
pub(crate) fn pick_where<V>(
|
||
&self,
|
||
eligible: impl Fn(usize, &RenditionMeta) -> Result<(), Skip>,
|
||
verify: impl FnMut(&str, &Prior) -> Result<V, PinRefusal>,
|
||
) -> Result<Picked<'_, V>, Vec<(usize, Skip)>> {
|
||
let order: Vec<usize> = (0..self.manifest.renditions.len()).collect();
|
||
let digest_ok = |i: usize, m: &RenditionMeta| self.renditions.get(i).is_some_and(|p| m.payload_digest_matches(p));
|
||
let (index, verified, skipped) = select_entries(&self.manifest, &order, eligible, digest_ok, verify)?;
|
||
Ok(Picked {
|
||
index,
|
||
meta: &self.manifest.renditions[index],
|
||
payload: &self.renditions[index],
|
||
verified,
|
||
skipped,
|
||
})
|
||
}
|
||
|
||
/// Check that no component, in either role, names a candidate capability
|
||
/// ([`crate::codecs::is_candidate`]), which an encoder MUST NOT emit (SPEC §5,
|
||
/// *Candidate capabilities*). Reports the first such component in table order.
|
||
pub fn check_candidates(&self) -> Result<(), CandidateError> {
|
||
check_component_candidates(self.manifest.renditions.iter().map(|m| m.capability.as_str()))
|
||
}
|
||
|
||
/// [`check_int_payload_framing`] over every component, in table order: the
|
||
/// first component whose payload is not its capability's pinned format, with its
|
||
/// index. Skipped for an encrypted envelope (a `security` member whose
|
||
/// `encryption` is not `"none"`), whose payloads are ciphertext until a sink
|
||
/// decrypts them (SPEC §6.4).
|
||
pub fn check_int_payload_framing(&self) -> Result<(), (usize, FramingError)> {
|
||
if !manifest_v2_plaintext(&self.manifest) {
|
||
return Ok(());
|
||
}
|
||
for (i, (m, p)) in self.manifest.renditions.iter().zip(&self.renditions).enumerate() {
|
||
check_int_payload_framing(&m.capability, p).map_err(|e| (i, e))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// The envelope's bytes, for an encoder to emit: [`Self::check_candidates`], then
|
||
/// [`manifest_v2_security`], then [`Self::check_int_payload_framing`], then
|
||
/// [`Self::to_bytes`]. The C packer `wai_envelope_pack_multi` emits through this, so
|
||
/// an encoder that uses it cannot emit a candidate, a `security` member that is not
|
||
/// a security object (SPEC §6.1), or a component whose integer payload is not its
|
||
/// capability's pinned format (SPEC §5).
|
||
pub fn to_emit_bytes(&self) -> Result<Vec<u8>, EmitError> {
|
||
self.check_candidates().map_err(EmitError::Candidate)?;
|
||
manifest_v2_security(&self.manifest).map_err(EmitError::Security)?;
|
||
self.check_int_payload_framing().map_err(|(i, error)| EmitError::IntPayload { component: Some(i), error })?;
|
||
self.to_bytes().map_err(EmitError::Serialize)
|
||
}
|
||
|
||
/// Why a `replicate` claim through entry `index` is unsound (SPEC §7), or
|
||
/// `None` if it is sound, does not claim `replicate`, or `index` is out of range.
|
||
pub fn replicate_refusal(&self, index: usize) -> Option<crate::codecs::ReplicateRefusal> {
|
||
let m = self.manifest.renditions.get(index)?;
|
||
crate::codecs::replicate_refusal(&m.capability, &self.manifest.intent, m.prior.as_ref())
|
||
}
|
||
|
||
/// What a verifier can decide about each entry's pin without holding a
|
||
/// parameter set: the first finding, with the entry's index.
|
||
pub fn check_pins(&self) -> Result<(), (usize, PinFinding)> {
|
||
for (i, m) in self.manifest.renditions.iter().enumerate() {
|
||
if m.is_companion() && m.prior.is_none() {
|
||
continue;
|
||
}
|
||
static_pin_findings(&[m.capability.as_str()], m.prior.as_ref(), &self.manifest.intent)
|
||
.map_err(|f| (i, f))?;
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
|
||
/// The refinements of the entry at `index`, in table order: each entry with
|
||
/// `role: "refinement"` whose `refines.base` names that entry's `id`, and whose
|
||
/// payload matches its `sha256` (a refinement that does not match is not applied,
|
||
/// SPEC §7.1). A sink offers their exploded envelopes to staged-delivery §4
|
||
/// ([`WaiMulti::explode`]) after [`Self::check_component_roles`] passes.
|
||
pub fn refinements_of(&self, index: usize) -> Vec<(usize, &RenditionMeta, &[u8])> {
|
||
self.with_matching_payloads(refinement_indices(&self.manifest.renditions, index))
|
||
}
|
||
|
||
/// The entries at `indices` whose payload matches its `sha256`, with their
|
||
/// payloads.
|
||
fn with_matching_payloads(&self, indices: Vec<usize>) -> Vec<(usize, &RenditionMeta, &[u8])> {
|
||
indices.into_iter()
|
||
.filter_map(|i| {
|
||
let (m, p) = (self.manifest.renditions.get(i)?, self.renditions.get(i)?);
|
||
m.payload_digest_matches(p).then_some((i, m, p.as_slice()))
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
/// Check the component table against SPEC §5 / §7.1 ([`ManifestV2::check_component_roles`]).
|
||
/// A sink offers the envelope's refinements to staged-delivery §4 only when it passes.
|
||
pub fn check_component_roles(&self) -> Result<(), RoleError> {
|
||
self.manifest.check_component_roles()
|
||
}
|
||
|
||
/// All companions (HDR dynamic metadata, enhancement layers) carried in
|
||
/// this envelope — applied *with* the chosen rendition, not instead of it.
|
||
pub fn companions(&self) -> Vec<(&RenditionMeta, &[u8])> {
|
||
self.manifest.renditions.iter().zip(self.renditions.iter())
|
||
.filter(|(m, _)| m.is_companion())
|
||
.map(|(m, p)| (m, p.as_slice()))
|
||
.collect()
|
||
}
|
||
|
||
/// The companions bound to the entry at `index` — what a sink applies with the
|
||
/// rendition [`Self::pick`] chose: each companion whose `companion_of_id` names
|
||
/// that entry, or, with no `companion_of_id`, whose `companion_of` is that
|
||
/// entry's capability. A companion whose payload does not match its `sha256` is
|
||
/// left out (it is not applied). As with [`Self::companions_of`], a sink applies
|
||
/// one only if the envelope passes [`Self::check_component_roles`] and it
|
||
/// recognises the companion's capability.
|
||
pub fn companions_of_index(&self, index: usize) -> Vec<(usize, &RenditionMeta, &[u8])> {
|
||
self.with_matching_payloads(companion_indices(&self.manifest.renditions, index))
|
||
}
|
||
|
||
/// The companions that name `capability` in `companion_of`, whatever entry
|
||
/// they bind to by id. Binding by capability alone; a sink that picked one
|
||
/// entry uses [`Self::companions_of_index`].
|
||
pub fn companions_of(&self, capability: &str) -> Vec<(&RenditionMeta, &[u8])> {
|
||
self.companions().into_iter()
|
||
.filter(|(m, _)| m.companion_of.as_deref() == Some(capability))
|
||
.collect()
|
||
}
|
||
|
||
pub fn media(&self) -> &str { &self.manifest.media }
|
||
pub fn intent(&self) -> &str { &self.manifest.intent }
|
||
}
|
||
|
||
/// SPEC §7.1 selection over a `WAI2` manifest's entries alone, with no payload in
|
||
/// hand: the walk [`WaiMulti::pick`] makes, in `order` (a deployer policy, SPEC §7.1
|
||
/// step 2: a permutation of a subset of the entry indices; an index not in it is not
|
||
/// considered, and an index out of range is ignored). Each entry considered is
|
||
/// skipped, in this order, when it is not selectable ([`Skip::NotSelectable`]), when
|
||
/// `eligible` refuses it (its reason), when `digest_ok` is false
|
||
/// ([`Skip::ComponentDigest`]), or when its pin is refused ([`Skip::Pin`]); the first
|
||
/// that passes is picked, with what `verify` returned and the entries skipped before
|
||
/// it. An encrypted envelope (SPEC §6.4 step 2) is inert before any entry is
|
||
/// considered: every entry is [`Skip::Encrypted`].
|
||
///
|
||
/// `digest_ok` is the payload test: `WaiMulti::pick` passes
|
||
/// [`RenditionMeta::payload_digest_matches`] over the entry's payload, and a client
|
||
/// that holds only the head passes `|_, _| true`, deferring the test to the bytes it
|
||
/// fetches (`extensions/http-delivery.md` §7 step 4). Both are this one function, so
|
||
/// the two paths pick alike on bytes that verify (http-delivery §7.2).
|
||
pub fn select_entries<V>(
|
||
manifest: &ManifestV2,
|
||
order: &[usize],
|
||
eligible: impl Fn(usize, &RenditionMeta) -> Result<(), Skip>,
|
||
digest_ok: impl Fn(usize, &RenditionMeta) -> bool,
|
||
mut verify: impl FnMut(&str, &Prior) -> Result<V, PinRefusal>,
|
||
) -> Result<(usize, Option<V>, Vec<(usize, Skip)>), Vec<(usize, Skip)>> {
|
||
let entries = &manifest.renditions;
|
||
// SPEC §6.4 step 2, before selection: an encrypted envelope is inert at a sink
|
||
// that opens no scheme, and every entry is reported so.
|
||
if !manifest_v2_plaintext(manifest) {
|
||
return Err((0..entries.len()).map(|i| (i, Skip::Encrypted)).collect());
|
||
}
|
||
let mut skipped = Vec::new();
|
||
for &index in order {
|
||
let Some(meta) = entries.get(index) else { continue };
|
||
if !meta.is_selectable() {
|
||
skipped.push((index, Skip::NotSelectable));
|
||
continue;
|
||
}
|
||
if let Err(why) = eligible(index, meta) {
|
||
skipped.push((index, why));
|
||
continue;
|
||
}
|
||
if !digest_ok(index, meta) {
|
||
skipped.push((index, Skip::ComponentDigest));
|
||
continue;
|
||
}
|
||
let verified = match meta.prior.as_ref() {
|
||
Some(p) if pin_applies_to(&meta.capability) => match verify(&meta.capability, p) {
|
||
Ok(v) => Some(v),
|
||
Err(r) => {
|
||
skipped.push((index, Skip::Pin(r)));
|
||
continue;
|
||
}
|
||
},
|
||
_ => None,
|
||
};
|
||
return Ok((index, verified, skipped));
|
||
}
|
||
Err(skipped)
|
||
}
|
||
|
||
/// The companions SPEC §7.1 binds to the entry at `index`, by entry alone: each
|
||
/// companion whose `companion_of_id` names that entry's `id`, or, with no
|
||
/// `companion_of_id`, whose `companion_of` is that entry's capability.
|
||
/// [`WaiMulti::companions_of_index`] is this with the payload digest checked; a client
|
||
/// that holds only the head checks it on the bytes it fetches.
|
||
pub fn companion_indices(entries: &[RenditionMeta], index: usize) -> Vec<usize> {
|
||
let Some(base) = entries.get(index) else { return Vec::new() };
|
||
entries.iter().enumerate()
|
||
.filter(|(_, m)| {
|
||
m.is_companion()
|
||
&& match m.companion_of_id.as_deref() {
|
||
Some(id) => base.id.as_deref() == Some(id),
|
||
None => m.companion_of.as_deref() == Some(base.capability.as_str()),
|
||
}
|
||
})
|
||
.map(|(i, _)| i)
|
||
.collect()
|
||
}
|
||
|
||
/// The refinement components whose `refines.base` names the entry at `index`, in
|
||
/// table order, by entry alone: [`WaiMulti::refinements_of`] without the payload
|
||
/// digest, which a client that holds only the head checks on the bytes it fetches.
|
||
pub fn refinement_indices(entries: &[RenditionMeta], index: usize) -> Vec<usize> {
|
||
let Some(base_id) = entries.get(index).and_then(|b| b.id.as_deref()) else { return Vec::new() };
|
||
entries.iter().enumerate()
|
||
.filter(|(_, m)| {
|
||
m.is_refinement()
|
||
&& m.refines.as_ref()
|
||
.and_then(|v| Refines::from_value(v, RefForm::ComponentId).ok())
|
||
.is_some_and(|r| r.base == base_id)
|
||
})
|
||
.map(|(i, _)| i)
|
||
.collect()
|
||
}
|
||
|
||
/// The refinement components whose `refines.base`, read on its own, is the `id` of the
|
||
/// entry at `index`, in table order: [`refinement_indices`] without the rest of
|
||
/// `refines` read. Where every component conforms (SPEC §7.1) the two agree, since each
|
||
/// refinement's `refines` is then well formed; where one does not, these are the
|
||
/// refinements a client planning from the head drops and reports
|
||
/// (`components_nonconforming`, http-delivery §7 step 4), a malformed `refines` that
|
||
/// names the pick among them.
|
||
pub fn refinements_naming(entries: &[RenditionMeta], index: usize) -> Vec<usize> {
|
||
let Some(base_id) = entries.get(index).and_then(|b| b.id.as_deref()) else { return Vec::new() };
|
||
entries.iter().enumerate()
|
||
.filter(|(_, m)| m.is_refinement() && m.refines.as_ref().and_then(|v| v.get("base")).and_then(Value::as_str) == Some(base_id))
|
||
.map(|(i, _)| i)
|
||
.collect()
|
||
}
|
||
|
||
/// Sniff the magic and return which envelope version is in this byte
|
||
/// blob. Lets callers branch on `Wai::from_bytes` vs `WaiMulti::from_bytes`
|
||
/// without parsing twice.
|
||
pub fn detect_version(bytes: &[u8]) -> std::io::Result<u8> {
|
||
if bytes.len() < 4 {
|
||
return Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
"too short for magic"));
|
||
}
|
||
match &bytes[..4] {
|
||
b"WAI1" => Ok(1),
|
||
b"WAI2" => Ok(2),
|
||
_ => Err(std::io::Error::new(std::io::ErrorKind::InvalidData,
|
||
"unknown magic (not WAI1/WAI2)")),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// A pin verifier that accepts every pin: for tests about selection order.
|
||
fn accept(_: &str, _: &Prior) -> Result<(), PinRefusal> {
|
||
Ok(())
|
||
}
|
||
|
||
/// SPEC §4 dispatch with no parameter set held: the capability, or `None`.
|
||
fn dispatched<'a>(w: &'a Wai, advertises: impl Fn(&str) -> bool) -> Option<&'a str> {
|
||
w.dispatch_verified(advertises, no_parameter_sets()).ok().map(|d| d.capability)
|
||
}
|
||
|
||
/// A rendition table whose lengths wrap a 32-bit sum (0xFFFF_FFF0 + 0x20 =
|
||
/// 0x10 mod 2^32) over a 16-byte payload block is truncated, on every target:
|
||
/// the parse returns an error rather than slicing out of range.
|
||
#[test]
|
||
fn wai2_lengths_that_wrap_32_bits_are_truncation() {
|
||
let mut bytes = WaiMulti::new("image", "improve", Value::Null, vec![
|
||
Rendition::media("wai.image.png", "png", vec![0; 8]),
|
||
Rendition::media("wai.image.avif", "avif", vec![0; 8]),
|
||
]).to_bytes().unwrap();
|
||
let table = bytes.len() - 16 - 16;
|
||
bytes[table + 4..table + 8].copy_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
|
||
bytes[table + 8..table + 12].copy_from_slice(&0xFFFF_FFF0u32.to_le_bytes());
|
||
bytes[table + 12..table + 16].copy_from_slice(&0x20u32.to_le_bytes());
|
||
let e = WaiMulti::from_bytes(&bytes).err().expect("a wrapped length sum must not parse");
|
||
assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
|
||
assert!(e.to_string().contains("payload block"), "{e}");
|
||
}
|
||
|
||
#[test]
|
||
fn round_trip_envelope() {
|
||
let m = Manifest::new(
|
||
"image",
|
||
"replicate",
|
||
ModelRequirement::new("wai.zeroth.dct").with_fallback("zeroth"),
|
||
Conditioning::new("image_zeroth"),
|
||
)
|
||
.with_target(serde_json::json!({"w": 512, "h": 512}));
|
||
let w = Wai::new(m, b"hello world".to_vec());
|
||
let bytes = w.to_bytes().unwrap();
|
||
let w2 = Wai::from_bytes(&bytes).unwrap();
|
||
assert_eq!(w2.capability(), "wai.zeroth.dct");
|
||
assert_eq!(w2.payload, b"hello world");
|
||
assert_eq!(w2.media(), "image");
|
||
// Byte-compat: a floor envelope (no prior) MUST NOT emit a `prior`
|
||
// key, so existing files round-trip byte-for-byte after the field
|
||
// was added.
|
||
assert!(w2.prior().is_none());
|
||
assert!(!bytes.windows(7).any(|win| win == b"\"prior\""),
|
||
"floor envelope must not serialize a prior key");
|
||
}
|
||
|
||
#[test]
|
||
fn prior_round_trips() {
|
||
let m = Manifest::new(
|
||
"image",
|
||
"replicate",
|
||
ModelRequirement::new("wai.neural.bmshj2018").with_fallback("wai.image.png"),
|
||
Conditioning::new("bmshj2018").with_prior(Prior::new(
|
||
"bmshj2018-factorized/q3",
|
||
"ab".repeat(32),
|
||
"int-rans/p16+onnx-cpu-f32/1thread",
|
||
)),
|
||
)
|
||
.with_target(serde_json::json!({"w": 256, "h": 256}));
|
||
let bytes = Wai::new(m.clone(), b"latents".to_vec()).to_bytes().unwrap();
|
||
let back = Wai::from_bytes(&bytes).unwrap();
|
||
let p = back.prior().expect("prior must survive the wire");
|
||
assert_eq!(p.id, "bmshj2018-factorized/q3");
|
||
assert_eq!(p.sha256, "ab".repeat(32));
|
||
assert_eq!(p.determinism, "int-rans/p16+onnx-cpu-f32/1thread");
|
||
// The neural envelope DOES carry the key.
|
||
assert!(bytes.windows(7).any(|win| win == b"\"prior\""));
|
||
}
|
||
|
||
#[test]
|
||
fn determinism_class_and_replicate_soundness() {
|
||
let mk = |det: &str| Prior::new("m", "ab".repeat(32), det);
|
||
// Integer rANS + FLOAT synthesis → only "exact here", not everywhere.
|
||
let float_synth = mk("int-rans/p16+onnx-cpu-f32/1thread");
|
||
assert_eq!(float_synth.determinism_class(), DeterminismClass::FloatReproducible);
|
||
assert!(!float_synth.is_cross_hardware_exact());
|
||
|
||
// Fully integerized inference → byte-identical across hardware.
|
||
let integerized = mk("int-rans/p16+onnx-int8-integerized");
|
||
assert_eq!(integerized.determinism_class(), DeterminismClass::IntegerExact);
|
||
assert!(integerized.is_cross_hardware_exact());
|
||
|
||
// Unrecognized contract → not reproducible.
|
||
assert_eq!(mk("magic").determinism_class(), DeterminismClass::Unknown);
|
||
|
||
// The honesty rule: `replicate` is sound only with an integer-exact prior.
|
||
assert!(!replicate_is_sound("replicate", None));
|
||
assert!(!replicate_is_sound("replicate", Some(&float_synth)));
|
||
assert!(replicate_is_sound("replicate", Some(&integerized)));
|
||
// Non-replicate intents aren't constrained by it.
|
||
assert!(replicate_is_sound("improve", Some(&float_synth)));
|
||
assert!(replicate_is_sound("create", None));
|
||
}
|
||
|
||
/// The envelope-level check applies both halves of SPEC §7: the capability's
|
||
/// registry class first, then the prior. An integer-declared prior over JPEG AI
|
||
/// (entropy-consistency only) or over a float codec does not make `replicate`
|
||
/// sound; the same prior over an integer-exact capability does.
|
||
#[test]
|
||
fn envelope_replicate_soundness_consults_the_registry() {
|
||
use crate::codecs::{ReplicateClass, ReplicateRefusal};
|
||
let env = |cap: &str, intent: &str, det: Option<&str>| {
|
||
let mut c = Conditioning::new("k");
|
||
c.prior = det.map(|d| Prior::new("m", "ab".repeat(32), d));
|
||
Wai::new(
|
||
Manifest::new("image", intent, ModelRequirement::new(cap), c)
|
||
.with_target(serde_json::json!({})),
|
||
vec![],
|
||
)
|
||
};
|
||
let int = Some("int-rans/p16+int-synth/i64");
|
||
|
||
let jpegai = env("wai.image.jpegai", "replicate", int);
|
||
assert!(!jpegai.replicate_is_sound());
|
||
assert_eq!(jpegai.replicate_refusal(),
|
||
Some(ReplicateRefusal::Class(ReplicateClass::NeuralEntropyExact)));
|
||
assert!(!env("wai.neural.bmshj2018", "replicate", int).replicate_is_sound());
|
||
|
||
assert!(env("wai.neural.int_synth", "replicate", int).replicate_is_sound());
|
||
assert!(!env("wai.neural.int_synth", "replicate", Some("int-rans+onnx-f32")).replicate_is_sound());
|
||
assert!(!env("wai.neural.int_synth", "replicate", None).replicate_is_sound());
|
||
|
||
// A floor codec carries no prior and is not refused by the neural rule.
|
||
assert!(env("wai.image.png", "replicate", None).replicate_is_sound());
|
||
// Nor is a codec whose standard conforms decoders to a tolerance: over it
|
||
// `replicate` promises the standard's conformance, not the same bytes.
|
||
assert!(env("wai.audio.opus", "replicate", None).replicate_is_sound());
|
||
// Non-replicate intents are unconstrained.
|
||
assert!(env("wai.image.jpegai", "improve", None).replicate_is_sound());
|
||
}
|
||
|
||
const NNC: &str = crate::codecs::CAP_PRIOR_NNC;
|
||
|
||
fn with_form(form: Option<&str>) -> Prior {
|
||
let p = Prior::new("int_hyper/q4", "ef".repeat(32), "int-rans/p16+int-synth");
|
||
match form {
|
||
Some(f) => p.with_form(f),
|
||
None => p,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn prior_without_a_form_round_trips_byte_for_byte() {
|
||
// A manifest written before `form` existed parses, and re-serializes to
|
||
// the identical bytes: the field is absent, not `null`.
|
||
let legacy = r#"{"wai":"1.0","media":"image","intent":"replicate","model_requirement":{"capability":"wai.neural.int_hyper"},"conditioning":{"kind":"int_hyper","prior":{"id":"int_hyper/q4","sha256":"efef","determinism":"int-rans/p16+int-synth"}},"target":null}"#;
|
||
let m: Manifest = serde_json::from_str(legacy).unwrap();
|
||
assert_eq!(m.conditioning.prior.as_ref().unwrap().form(), None);
|
||
assert_eq!(serde_json::to_string(&m).unwrap(), legacy);
|
||
}
|
||
|
||
#[test]
|
||
fn constructor_builds_the_prior_a_literal_would() {
|
||
// `Prior::new` leaves the optional form absent, so it serializes exactly
|
||
// as a prior written before `form` existed; `with_form` declares one.
|
||
let p = Prior::new("int_hyper/q4", "ef".repeat(32), "int-rans/p16+int-synth");
|
||
assert_eq!(p, with_form(None));
|
||
assert_eq!(
|
||
serde_json::to_string(&p).unwrap(),
|
||
format!(r#"{{"id":"int_hyper/q4","sha256":"{}","determinism":"int-rans/p16+int-synth"}}"#,
|
||
"ef".repeat(32))
|
||
);
|
||
assert_eq!(p.clone().with_form(NNC), with_form(Some(NNC)));
|
||
assert_eq!(p.with_form(NNC).form(), Some(NNC));
|
||
}
|
||
|
||
#[test]
|
||
fn declared_form_round_trips_through_the_envelope() {
|
||
let mut w = Wai::new(Manifest::new(
|
||
"image", "replicate",
|
||
ModelRequirement::new("wai.neural.int_hyper"),
|
||
Conditioning::new("int_hyper"),
|
||
), vec![1, 2, 3]);
|
||
w.manifest.conditioning.prior = Some(with_form(Some(NNC)));
|
||
let back = Wai::from_bytes(&w.to_bytes().unwrap()).unwrap();
|
||
assert_eq!(back.prior(), Some(&with_form(Some(NNC))));
|
||
assert_eq!(back.prior().unwrap().form(), Some(NNC));
|
||
}
|
||
|
||
#[test]
|
||
fn a_declared_form_must_be_registered_and_readable() {
|
||
// Undeclared: the digest alone identifies the artifact.
|
||
assert_eq!(with_form(None).check_form(&[]), Ok(()));
|
||
// Registered, but this sink has no reader for it: refused, never
|
||
// resolved against an artifact in some other form.
|
||
assert_eq!(with_form(Some(NNC)).check_form(&[]), Err(PriorFormError::Unreadable(NNC.into())));
|
||
assert_eq!(with_form(Some(NNC)).check_form(&[NNC]), Ok(()));
|
||
// Not a prior form at all: a payload codec, or an unknown string.
|
||
for bad in ["wai.image.png", "wai.tensor.nnc", "wai.prior.nope"] {
|
||
assert_eq!(
|
||
with_form(Some(bad)).check_form(&[bad]),
|
||
Err(PriorFormError::NotAPriorForm(bad.into())),
|
||
"{bad}"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn replicate_cannot_rest_on_a_form_not_established_exact() {
|
||
// The same integer-exact network: sound when the artifact is the bytes
|
||
// the digest covers; not sound once it must be decoded out of a form
|
||
// whose reconstruction is not established exact at every reader.
|
||
let plain = with_form(None);
|
||
let nnc = with_form(Some(NNC));
|
||
// One contract, integer-exact for both …
|
||
assert_eq!(plain.determinism_class(), DeterminismClass::IntegerExact);
|
||
assert_eq!(nnc.determinism_class(), DeterminismClass::IntegerExact);
|
||
assert!(plain.form_preserves_exactness());
|
||
assert!(!nnc.form_preserves_exactness());
|
||
// … but only the prior read as the bytes its digest covers is exact across
|
||
// hardware. The predicate every `replicate` check reads carries the form rule.
|
||
assert!(plain.is_cross_hardware_exact());
|
||
assert!(!nnc.is_cross_hardware_exact());
|
||
assert!(replicate_is_sound("replicate", Some(&plain)));
|
||
assert!(!replicate_is_sound("replicate", Some(&nnc)));
|
||
// Only `replicate` is gated.
|
||
assert!(replicate_is_sound("improve", Some(&nnc)));
|
||
}
|
||
|
||
#[test]
|
||
fn replicate_refusal_names_its_reason() {
|
||
// The reason a strict decoder reports. The form refusal must not fall back
|
||
// to the float wording: that network's contract IS integer-exact.
|
||
let mk = |det: &str, form: Option<&str>| {
|
||
let p = Prior::new("m", "ab".repeat(32), det);
|
||
match form {
|
||
Some(f) => p.with_form(f),
|
||
None => p,
|
||
}
|
||
};
|
||
let integer = mk("int-rans/p16+int-synth", None);
|
||
let floaty = mk("int-rans/p16+onnx-cpu-f32/1thread", None);
|
||
let integer_nnc = mk("int-rans/p16+int-synth", Some(NNC));
|
||
|
||
assert_eq!(replicate_prior_refusal("replicate", Some(&integer)), None);
|
||
assert_eq!(replicate_prior_refusal("improve", None), None);
|
||
assert_eq!(replicate_prior_refusal("improve", Some(&integer_nnc)), None);
|
||
assert_eq!(replicate_prior_refusal("replicate", None), Some(PriorRefusal::Absent));
|
||
assert_eq!(
|
||
replicate_prior_refusal("replicate", Some(&floaty)),
|
||
Some(PriorRefusal::NotIntegerExact(DeterminismClass::FloatReproducible))
|
||
);
|
||
// A float contract is refused as float even when it also declares a form.
|
||
assert_eq!(
|
||
replicate_prior_refusal("replicate", Some(&mk("onnx-cpu-f32", Some(NNC)))),
|
||
Some(PriorRefusal::NotIntegerExact(DeterminismClass::FloatReproducible))
|
||
);
|
||
let why = replicate_prior_refusal("replicate", Some(&integer_nnc));
|
||
assert_eq!(why, Some(PriorRefusal::FormNotExact(NNC.into())));
|
||
let text = why.unwrap().to_string();
|
||
assert!(text.contains(NNC) && text.contains("declared form"), "{text}");
|
||
assert!(text.contains("not established"), "{text}");
|
||
assert!(!text.contains("not integer-exact"), "{text}");
|
||
// A form outside the registry is refused as such, not as a form whose
|
||
// reconstruction is merely unestablished.
|
||
let nope = replicate_prior_refusal("replicate", Some(&mk("int-rans/p16+int-synth",
|
||
Some("wai.prior.nope"))));
|
||
assert_eq!(nope, Some(PriorRefusal::FormNotExact("wai.prior.nope".into())));
|
||
let text = nope.unwrap().to_string();
|
||
assert!(text.contains("not a registered prior form"), "{text}");
|
||
assert!(PriorRefusal::NotIntegerExact(DeterminismClass::Unknown)
|
||
.to_string().contains("not integer-exact"));
|
||
assert!(PriorRefusal::Absent.to_string().contains("no prior is pinned"));
|
||
}
|
||
|
||
// ---- v1.1: multi-rendition WAI2 ----
|
||
|
||
fn sample_multi() -> WaiMulti {
|
||
WaiMulti::new("audio", "replicate",
|
||
serde_json::json!({"sr": 48000, "dur": 5.0}),
|
||
vec![
|
||
Rendition::media("wai.neural.encodec32", "encodec_tokens",
|
||
b"NEURAL-TOKENS-2317-BYTES".to_vec())
|
||
.with_prior(Prior::new("encodec32/v1", "cd".repeat(32), "int-tokens")),
|
||
Rendition::media("wai.audio.opus", "opus", b"OPUS-PAYLOAD-4411-BYTES".to_vec()),
|
||
])
|
||
}
|
||
|
||
#[test]
|
||
fn multi_rendition_round_trip() {
|
||
let m = sample_multi();
|
||
let bytes = m.to_bytes().unwrap();
|
||
assert_eq!(detect_version(&bytes).unwrap(), 2);
|
||
let dec = WaiMulti::from_bytes(&bytes).unwrap();
|
||
assert_eq!(dec.manifest.wai, "1.1");
|
||
assert_eq!(dec.renditions.len(), 2);
|
||
assert_eq!(dec.renditions[0], b"NEURAL-TOKENS-2317-BYTES");
|
||
assert_eq!(dec.renditions[1], b"OPUS-PAYLOAD-4411-BYTES");
|
||
assert_eq!(dec.manifest.renditions[0].capability, "wai.neural.encodec32");
|
||
assert_eq!(dec.manifest.renditions[1].capability, "wai.audio.opus");
|
||
// Per-rendition prior survives the wire on the neural rendition, and
|
||
// the floor rendition carries none.
|
||
assert_eq!(dec.manifest.renditions[0].prior.as_ref().map(|p| p.id.as_str()),
|
||
Some("encodec32/v1"));
|
||
assert!(dec.manifest.renditions[1].prior.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn companion_descriptors_bind_to_a_primary() {
|
||
// one HDR video payload (two codec renditions of it) + two HDR
|
||
// dynamic-metadata companions (HDR10+ and ST 2094-10) bound to the AV1
|
||
// rendition. One envelope, intrinsic binding.
|
||
let m = WaiMulti::new(
|
||
"video",
|
||
"replicate",
|
||
serde_json::Value::Null,
|
||
vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1-HDR-VIDEO".to_vec()),
|
||
Rendition::media("wai.image.jp2", "j2k", b"J2K-MEZZANINE".to_vec()),
|
||
Rendition::companion("wai.meta.hdr10plus", b"ST2094-40-DYNAMIC-METADATA".to_vec(), "wai.video.av1"),
|
||
Rendition::companion("wai.meta.dovi", b"ST2094-10-RPU".to_vec(), "wai.video.av1"),
|
||
],
|
||
);
|
||
let dec = WaiMulti::from_bytes(&m.to_bytes().unwrap()).unwrap();
|
||
|
||
// pick / primary only see the selectable video renditions, never the companions.
|
||
assert_eq!(dec.primary(accept).unwrap().meta.capability, "wai.video.av1");
|
||
assert!(dec.pick(|c| c == "wai.meta.hdr10plus", accept).is_err(), "a companion is not selectable as a rendition");
|
||
assert_eq!(dec.pick(|c| c == "wai.image.jp2", accept).unwrap().payload, b"J2K-MEZZANINE");
|
||
|
||
// the companions are carried, byte-exact, and bound to the AV1 rendition.
|
||
let comps = dec.companions();
|
||
assert_eq!(comps.len(), 2);
|
||
let hdr = dec.companions_of("wai.video.av1");
|
||
assert_eq!(hdr.len(), 2);
|
||
assert!(hdr.iter().any(|(m, p)| m.capability == "wai.meta.hdr10plus" && *p == b"ST2094-40-DYNAMIC-METADATA"));
|
||
assert!(dec.companions_of("wai.image.jp2").is_empty());
|
||
// a v1.1 rendition (role absent) is not a companion — back-compat.
|
||
assert!(!dec.manifest.renditions[0].is_companion());
|
||
// and the envelope satisfies the registry-aware role rules.
|
||
assert_eq!(dec.check_component_roles(), Ok(()));
|
||
}
|
||
|
||
#[test]
|
||
fn lcevc_enhancement_binds_to_exactly_one_base() {
|
||
// The separate (dual-track-shaped) LCEVC carriage: an HD VVC base that
|
||
// any VVC sink presents on its own, plus the enhancement companion that
|
||
// an LCEVC-capable sink adds on top. An AV1 rendition of the same
|
||
// content rides alongside as a royalty-free alternative.
|
||
let m = WaiMulti::new(
|
||
"video",
|
||
"replicate",
|
||
serde_json::Value::Null,
|
||
vec![
|
||
Rendition::media("wai.video.vvc", "vvc", b"VVC-HD-BASE".to_vec()),
|
||
Rendition::media("wai.video.av1", "av1", b"AV1-UHD".to_vec()),
|
||
Rendition::companion("wai.video.lcevc", b"LCEVC-ENHANCEMENT".to_vec(), "wai.video.vvc"),
|
||
],
|
||
);
|
||
let dec = WaiMulti::from_bytes(&m.to_bytes().unwrap()).unwrap();
|
||
assert_eq!(dec.check_component_roles(), Ok(()));
|
||
// The enhancement is never selectable; the base is, and the enhancement
|
||
// is bound to it byte-exact.
|
||
assert!(dec.pick(|c| c == "wai.video.lcevc", accept).is_err());
|
||
assert_eq!(dec.primary(accept).unwrap().meta.capability, "wai.video.vvc");
|
||
let enh = dec.companions_of("wai.video.vvc");
|
||
assert_eq!(enh.len(), 1);
|
||
assert_eq!(enh[0].0.capability, "wai.video.lcevc");
|
||
assert_eq!(enh[0].1, b"LCEVC-ENHANCEMENT");
|
||
assert!(dec.companions_of("wai.video.av1").is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn lcevc_binding_rejects_ambiguous_missing_and_mislabelled_bases() {
|
||
let lcevc = || Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), "wai.video.hevc");
|
||
// Two HEVC renditions: the enhancement was encoded against ONE of them,
|
||
// and companion_of (a capability) cannot say which.
|
||
let two = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-A".to_vec()),
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-B".to_vec()),
|
||
lcevc(),
|
||
]);
|
||
assert_eq!(two.check_component_roles(),
|
||
Err(RoleError::EnhancementBaseNotUnique { index: 2, capability: "wai.video.lcevc".into(), matches: 2 }));
|
||
// No HEVC base at all.
|
||
let none = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
lcevc(),
|
||
]);
|
||
assert_eq!(none.check_component_roles(),
|
||
Err(RoleError::EnhancementBaseNotUnique { index: 1, capability: "wai.video.lcevc".into(), matches: 0 }));
|
||
// A non-video base (an enhancement of an audio payload is meaningless):
|
||
// present and unique, but not one of LCEVC's registered base codecs.
|
||
let audio = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.audio.aac", "aac", b"AAC".to_vec()),
|
||
Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), "wai.audio.aac"),
|
||
]);
|
||
assert_eq!(audio.check_component_roles(),
|
||
Err(RoleError::IncompatibleBase { index: 1, capability: "wai.video.lcevc".into(),
|
||
base: "wai.audio.aac".into() }));
|
||
// The enhancement mislabelled as a rendition: reported, and never picked
|
||
// even by a sink that advertises an LCEVC decoder.
|
||
let mis = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.lcevc", "lcevc", b"ENH".to_vec()),
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()),
|
||
]);
|
||
assert_eq!(mis.check_component_roles(),
|
||
Err(RoleError::CompanionOnlyAsRendition { index: 0, capability: "wai.video.lcevc".into() }));
|
||
assert_eq!(mis.primary(accept).unwrap().meta.capability, "wai.video.hevc");
|
||
assert!(mis.pick(|c| c == "wai.video.lcevc", accept).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn a_base_takes_at_most_one_enhancement_layer() {
|
||
// Two LCEVC enhancements bound to one HEVC base: the base resolves
|
||
// uniquely, but a sink applies every companion of the rendition it
|
||
// picked, so it would stack two sets of residuals on one picture.
|
||
let enh = |p: &[u8], of: &str| Rendition::companion("wai.video.lcevc", p.to_vec(), of);
|
||
let stacked = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()),
|
||
enh(b"ENH-1", "wai.video.hevc"),
|
||
enh(b"ENH-2", "wai.video.hevc"),
|
||
]);
|
||
assert_eq!(stacked.companions_of("wai.video.hevc").len(), 2, "both would be applied");
|
||
assert_eq!(stacked.check_component_roles(),
|
||
Err(RoleError::DuplicateEnhancementLayer {
|
||
index: 2, capability: "wai.video.lcevc".into(),
|
||
base: "wai.video.hevc".into(), first: 1 }));
|
||
// One enhancement per base across two different bases conforms, and a
|
||
// descriptor companion on an enhanced base does not count against it.
|
||
let one_each = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()),
|
||
Rendition::media("wai.video.avc", "avc", b"AVC".to_vec()),
|
||
enh(b"ENH-HEVC", "wai.video.hevc"),
|
||
enh(b"ENH-AVC", "wai.video.avc"),
|
||
Rendition::companion("wai.meta.hdr10plus", b"ST2094-40".to_vec(), "wai.video.hevc"),
|
||
]);
|
||
assert_eq!(one_each.check_component_roles(), Ok(()));
|
||
}
|
||
|
||
#[test]
|
||
fn v1_envelope_rejects_companion_only_capability_and_fallback() {
|
||
let v1 = |cap: &str, fallback: Option<&str>| Wai::new(
|
||
Manifest::new("video", "replicate", {
|
||
let r = ModelRequirement::new(cap);
|
||
match fallback { Some(f) => r.with_fallback(f), None => r }
|
||
}, Conditioning::new("bytes")),
|
||
b"BYTES".to_vec(),
|
||
);
|
||
// A WAI1 has one payload, so a companion-only capability has no base.
|
||
for cap in ["wai.video.lcevc", "wai.meta.hdr10plus", "wai.meta.dovi"] {
|
||
assert_eq!(v1(cap, None).check_component_roles(),
|
||
Err(RoleError::CompanionOnlyAsCapability { capability: cap.into() }));
|
||
// ...and it survives the wire, because parsing stays format-agnostic;
|
||
// the check is what reports it.
|
||
let back = Wai::from_bytes(&v1(cap, None).to_bytes().unwrap()).unwrap();
|
||
assert!(back.check_component_roles().is_err());
|
||
}
|
||
// Named as a fallback, dispatch step 5 would decode it standalone.
|
||
assert_eq!(v1("wai.video.hevc", Some("wai.video.lcevc")).check_component_roles(),
|
||
Err(RoleError::CompanionOnlyAsFallback { capability: "wai.video.lcevc".into() }));
|
||
// Ordinary WAI1 envelopes conform, with or without a fallback.
|
||
assert_eq!(v1("wai.video.hevc", None).check_component_roles(), Ok(()));
|
||
assert_eq!(v1("wai.video.evc.baseline", Some("wai.video.evc")).check_component_roles(), Ok(()));
|
||
}
|
||
|
||
/// SPEC §4 with the §5 companion-only rule. A companion-only fallback makes
|
||
/// the envelope non-conforming, but step 3 still decodes via a primary the
|
||
/// sink advertises; only step 5 is closed to it. A companion-only primary is
|
||
/// never dispatched, even with a usable fallback.
|
||
#[test]
|
||
fn dispatch_skips_a_companion_only_fallback_but_keeps_step_3() {
|
||
let v1 = |cap: &str, fallback: Option<&str>| Wai::new(
|
||
Manifest::new("image", "replicate", {
|
||
let r = ModelRequirement::new(cap);
|
||
match fallback { Some(f) => r.with_fallback(f), None => r }
|
||
}, Conditioning::new("bytes")),
|
||
b"BYTES".to_vec(),
|
||
);
|
||
// A sink that advertises everything, including the companion-only
|
||
// capabilities: advertising one never makes it standalone-dispatchable.
|
||
let all = |_: &str| true;
|
||
let only = |set: &'static [&'static str]| move |c: &str| set.iter().any(|s| *s == c);
|
||
|
||
// Primary advertised: step 3, whatever the fallback names.
|
||
let png_lcevc = v1("wai.image.png", Some("wai.video.lcevc"));
|
||
assert!(png_lcevc.check_component_roles().is_err(), "non-conforming envelope");
|
||
assert_eq!(dispatched(&png_lcevc, all), Some("wai.image.png"));
|
||
assert_eq!(dispatched(&png_lcevc, only(&["wai.image.png"])), Some("wai.image.png"));
|
||
// Primary not advertised: step 5 would dispatch the fallback, but a
|
||
// companion-only one is never dispatched, so the envelope is inert.
|
||
assert_eq!(dispatched(&png_lcevc, only(&["wai.video.lcevc"])), None);
|
||
// A companion-only primary is inert even with a usable fallback.
|
||
assert_eq!(dispatched(&v1("wai.video.lcevc", Some("wai.video.hevc")), all), None);
|
||
// Ordinary envelopes: step 3, then step 5, then inert.
|
||
let evc = v1("wai.video.evc.baseline", Some("wai.video.evc"));
|
||
assert_eq!(dispatched(&evc, only(&["wai.video.evc.baseline", "wai.video.evc"])),
|
||
Some("wai.video.evc.baseline"));
|
||
assert_eq!(dispatched(&evc, only(&["wai.video.evc"])), Some("wai.video.evc"));
|
||
assert_eq!(dispatched(&evc, only(&["wai.image.png"])), None);
|
||
assert_eq!(dispatched(&v1("wai.image.png", None), only(&["wai.audio.flac"])), None);
|
||
}
|
||
|
||
#[test]
|
||
fn descriptor_companions_bind_but_need_not_be_unique() {
|
||
// HDR10+ metadata describes the content, so it may apply to either of
|
||
// two renditions of one capability — unlike an enhancement layer.
|
||
let m = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-HI".to_vec()),
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-LO".to_vec()),
|
||
Rendition::companion("wai.meta.hdr10plus", b"ST2094-40".to_vec(), "wai.video.hevc"),
|
||
]);
|
||
assert_eq!(m.check_component_roles(), Ok(()));
|
||
// ...but it must bind to something present.
|
||
let dangling = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::companion("wai.meta.hdr10plus", b"ST2094-40".to_vec(), "wai.video.hevc"),
|
||
]);
|
||
assert_eq!(dangling.check_component_roles(),
|
||
Err(RoleError::UnboundCompanion { index: 1, capability: "wai.meta.hdr10plus".into() }));
|
||
// A companion with no companion_of at all binds to nothing.
|
||
let mut no_target = Rendition::companion("wai.meta.dovi", b"RPU".to_vec(), "x");
|
||
no_target.meta.companion_of = None;
|
||
let unbound = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()),
|
||
no_target,
|
||
]);
|
||
assert!(matches!(unbound.check_component_roles(), Err(RoleError::UnboundCompanion { index: 1, .. })));
|
||
}
|
||
|
||
/// SPEC §7.1, rule 1: a registered capability that is not companion-only is
|
||
/// standalone media — its payload decodes on its own — and is carried as a
|
||
/// rendition, never marked companion.
|
||
#[test]
|
||
fn a_standalone_codec_is_never_a_companion() {
|
||
for (cap, of) in [
|
||
// IAMF carries its own audio substreams; it is not mix metadata.
|
||
("wai.audio.iamf", "wai.video.av1"),
|
||
// A second video codec, bound to a video base of the same content.
|
||
("wai.video.hevc", "wai.video.av1"),
|
||
// An audio track bound to its video.
|
||
("wai.audio.opus", "wai.video.av1"),
|
||
// A standalone codec bound to a base of its own kind.
|
||
("wai.image.png", "wai.image.jp2"),
|
||
] {
|
||
let m = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::media("wai.image.jp2", "j2k", b"J2K".to_vec()),
|
||
Rendition::companion(cap, b"PAYLOAD".to_vec(), of),
|
||
]);
|
||
assert_eq!(m.check_component_roles(),
|
||
Err(RoleError::StandaloneAsCompanion { index: 2, capability: cap.into() }), "{cap}");
|
||
// Marked companion, it is never selectable either, so the check is
|
||
// the only thing that says the envelope is wrong.
|
||
assert!(m.pick(|c| c == cap, accept).is_err(), "{cap}");
|
||
}
|
||
// Reported even with no companion_of: rule 1 comes first.
|
||
let mut unbound = Rendition::companion("wai.audio.iamf", b"IAMF".to_vec(), "x");
|
||
unbound.meta.companion_of = None;
|
||
let m = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
unbound,
|
||
]);
|
||
assert_eq!(m.check_component_roles(),
|
||
Err(RoleError::StandaloneAsCompanion { index: 1, capability: "wai.audio.iamf".into() }));
|
||
// The same IAMF stream as a rendition beside the video conforms.
|
||
let ok = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::media("wai.audio.iamf", "iamf", b"IAMF".to_vec()),
|
||
]);
|
||
assert_eq!(ok.check_component_roles(), Ok(()));
|
||
}
|
||
|
||
/// SPEC §7.1, rule 1 and 3 for a capability this revision does not register:
|
||
/// its role and bases are unknown here, so it is held only to a present
|
||
/// base — a later revision may register it as a companion.
|
||
#[test]
|
||
fn an_unregistered_companion_needs_only_a_present_base() {
|
||
let m = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::companion("wai.meta.future", b"DESCRIPTOR".to_vec(), "wai.video.av1"),
|
||
]);
|
||
assert_eq!(m.check_component_roles(), Ok(()));
|
||
let dangling = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::companion("wai.meta.future", b"DESCRIPTOR".to_vec(), "wai.video.hevc"),
|
||
]);
|
||
assert_eq!(dangling.check_component_roles(),
|
||
Err(RoleError::UnboundCompanion { index: 1, capability: "wai.meta.future".into() }));
|
||
}
|
||
|
||
/// SPEC §5 / §7.1, rule 2 for HDR dynamic metadata: it was registered as a
|
||
/// companion to a video payload, so among registered capabilities it binds
|
||
/// to one in the `video` media class — `wai.video.*`, the neural video
|
||
/// codecs, the films — and to nothing else. An unregistered base is held to
|
||
/// rule 3 only.
|
||
#[test]
|
||
fn hdr_descriptors_bind_only_to_video() {
|
||
let env = |hdr: &str, base: &str, kind: &str| WaiMulti::new(
|
||
"video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media(base, kind, b"BASE".to_vec()),
|
||
Rendition::companion(hdr, b"ST2094".to_vec(), base),
|
||
]);
|
||
for hdr in ["wai.meta.hdr10plus", "wai.meta.dovi"] {
|
||
// Not video: audio, images (a neural image codec among them), a
|
||
// volumetric and a world payload.
|
||
for base in ["wai.audio.opus", "wai.audio.aac", "wai.audio.iamf", "wai.image.avif",
|
||
"wai.image.jp2", "wai.neural.glc", "wai.splat.4d", "wai.world.replay"] {
|
||
assert_eq!(env(hdr, base, "x").check_component_roles(),
|
||
Err(RoleError::IncompatibleBase { index: 1, capability: hdr.into(), base: base.into() }),
|
||
"{hdr} on {base}");
|
||
}
|
||
// Video: wai.video.* (including the codecs LCEVC is not registered
|
||
// over), the neural video codecs outside that namespace, the films.
|
||
for base in ["wai.video.av1", "wai.video.hevc", "wai.video.j2k", "wai.video.av2",
|
||
"wai.video.int_motion", "wai.neural.video_bmshj2018", "wai.neural.dcvc_rt",
|
||
"wai.neural.dcvc_fm", "wai.film.linear", "wai.film.reel"] {
|
||
assert_eq!(env(hdr, base, "x").check_component_roles(), Ok(()), "{hdr} on {base}");
|
||
}
|
||
// A base this revision does not register is held only to rule 3: present
|
||
// it binds, absent it is unbound.
|
||
assert_eq!(env(hdr, "wai.neural.video_future", "x").check_component_roles(), Ok(()));
|
||
let absent_unknown = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::companion(hdr, b"ST2094".to_vec(), "wai.neural.video_future"),
|
||
]);
|
||
assert_eq!(absent_unknown.check_component_roles(),
|
||
Err(RoleError::UnboundCompanion { index: 1, capability: hdr.into() }));
|
||
}
|
||
// The refusal says what the descriptor does apply to.
|
||
let msg = env("wai.meta.hdr10plus", "wai.audio.opus", "opus").check_component_roles()
|
||
.unwrap_err().to_string();
|
||
assert!(msg.contains("video payload") && msg.contains("neural video"), "{msg}");
|
||
// Never to a companion: HDR metadata bound to an enhancement layer.
|
||
let on_enh = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()),
|
||
Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), "wai.video.hevc"),
|
||
Rendition::companion("wai.meta.hdr10plus", b"ST2094".to_vec(), "wai.video.lcevc"),
|
||
]);
|
||
assert_eq!(on_enh.check_component_roles(),
|
||
Err(RoleError::IncompatibleBase { index: 2, capability: "wai.meta.hdr10plus".into(),
|
||
base: "wai.video.lcevc".into() }));
|
||
// The mismatch is caught even when the named base is absent: the name alone
|
||
// says it cannot apply.
|
||
let absent = WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.av1", "av1", b"AV1".to_vec()),
|
||
Rendition::companion("wai.meta.dovi", b"RPU".to_vec(), "wai.audio.opus"),
|
||
]);
|
||
assert!(matches!(absent.check_component_roles(), Err(RoleError::IncompatibleBase { index: 1, .. })));
|
||
}
|
||
|
||
/// SPEC §5 / §7.1, rule 2 for LCEVC: it binds only to a base codec its
|
||
/// registration names (AVC, HEVC, VVC, EVC, VP9, AV1), not to any
|
||
/// `wai.video.*`.
|
||
#[test]
|
||
fn lcevc_binds_only_to_its_base_codecs() {
|
||
let env = |base: &str| WaiMulti::new("video", "replicate", serde_json::Value::Null, vec![
|
||
Rendition::media(base, "x", b"BASE".to_vec()),
|
||
Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), base),
|
||
]);
|
||
for &base in crate::codecs::enhancement_bases("wai.video.lcevc") {
|
||
assert_eq!(env(base).check_component_roles(), Ok(()), "LCEVC on {base}");
|
||
}
|
||
for base in ["wai.video.j2k", "wai.video.av2", "wai.video.avs3", "wai.video.int_motion",
|
||
"wai.video.int_hyper", "wai.video.future", "wai.image.jp2"] {
|
||
assert_eq!(env(base).check_component_roles(),
|
||
Err(RoleError::IncompatibleBase { index: 1, capability: "wai.video.lcevc".into(),
|
||
base: base.into() }),
|
||
"LCEVC on {base}");
|
||
}
|
||
// The error names the bases it would accept.
|
||
let msg = env("wai.video.j2k").check_component_roles().unwrap_err().to_string();
|
||
assert!(msg.contains("wai.video.hevc") && msg.contains("wai.video.av1"), "{msg}");
|
||
}
|
||
|
||
#[test]
|
||
fn multi_pick_by_capability() {
|
||
let m = sample_multi();
|
||
let picked = m.pick(|c| c == "wai.audio.opus", accept).unwrap();
|
||
assert_eq!(picked.meta.capability, "wai.audio.opus");
|
||
assert_eq!(picked.payload, b"OPUS-PAYLOAD-4411-BYTES");
|
||
// primary = first rendition (deployer-preferred order)
|
||
let pmeta = m.primary(accept).unwrap().meta;
|
||
assert_eq!(pmeta.capability, "wai.neural.encodec32");
|
||
}
|
||
|
||
/// SPEC §5, *Candidate capabilities*: a candidate is never dispatched, picked
|
||
/// or treated as a primary, and the candidate check reports every place one
|
||
/// can be named; such an envelope still parses.
|
||
#[test]
|
||
fn a_candidate_is_never_dispatched_picked_or_emitted() {
|
||
use crate::codecs::{is_candidate, known_capabilities};
|
||
let v1 = |cap: &str, fb: Option<&str>| {
|
||
let req = match fb {
|
||
Some(fb) => ModelRequirement::new(cap).with_fallback(fb),
|
||
None => ModelRequirement::new(cap),
|
||
};
|
||
Wai::new(Manifest::new("binary", "improve", req, Conditioning::new("bytes")), b"payload".to_vec())
|
||
};
|
||
let all = |_: &str| true;
|
||
let candidates: Vec<&str> = known_capabilities().iter().copied().filter(|c| is_candidate(c)).collect();
|
||
assert!(!candidates.is_empty());
|
||
for &cand in &candidates {
|
||
// A candidate primary is unsupported: step 5 takes a usable fallback …
|
||
assert_eq!(dispatched(&v1(cand, Some("wai.image.png")), all), Some("wai.image.png"), "{cand}");
|
||
// … and without one the envelope is inert.
|
||
assert_eq!(dispatched(&v1(cand, None), all), None, "{cand}");
|
||
assert_eq!(v1(cand, None).dispatch_verified(all, no_parameter_sets()).unwrap_err(),
|
||
Inert::MissingCapability, "{cand}");
|
||
// A candidate fallback is never taken, even advertised alone.
|
||
assert_eq!(dispatched(&v1("wai.image.png", Some(cand)), |c| c == cand), None, "{cand}");
|
||
// The check names each place, and every message says why.
|
||
let as_cap = v1(cand, None).check_candidates().unwrap_err();
|
||
assert_eq!(as_cap, CandidateError::AsCapability { capability: cand.into() });
|
||
let as_fb = v1("wai.image.png", Some(cand)).check_candidates().unwrap_err();
|
||
assert_eq!(as_fb, CandidateError::AsFallback { capability: cand.into() });
|
||
for e in [&as_cap, &as_fb] {
|
||
assert!(e.to_string().contains("MUST NOT emit"), "{e}");
|
||
}
|
||
// Such an envelope still parses, byte for byte.
|
||
let env = v1(cand, Some("wai.image.png"));
|
||
let back = Wai::from_bytes(&env.to_bytes().unwrap()).unwrap();
|
||
assert_eq!(back.capability(), cand);
|
||
assert!(back.check_candidates().is_err());
|
||
// WAI2: a candidate at index 0 is skipped by primary and pick, and reported.
|
||
let m = WaiMulti::new("binary", "improve", serde_json::Value::Null, vec![
|
||
Rendition::media(cand, "bytes", b"c".to_vec()),
|
||
Rendition::media("wai.image.png", "png", b"p".to_vec()),
|
||
]);
|
||
let back = WaiMulti::from_bytes(&m.to_bytes().unwrap()).unwrap();
|
||
assert_eq!(back.primary(no_parameter_sets()).unwrap().meta.capability, "wai.image.png", "{cand}");
|
||
let p = back.pick(all, no_parameter_sets()).unwrap();
|
||
assert_eq!(p.meta.capability, "wai.image.png", "{cand}");
|
||
assert_eq!(p.skipped, [(0, Skip::NotSelectable)], "{cand}");
|
||
let e = back.pick(|c| c == cand, no_parameter_sets()).unwrap_err();
|
||
assert_eq!(e, [(0, Skip::NotSelectable), (1, Skip::NotEligible)], "{cand}");
|
||
assert_eq!(back.check_candidates(),
|
||
Err(CandidateError::AsComponent { index: 0, capability: cand.into() }));
|
||
}
|
||
// A registered, pinned capability is unaffected.
|
||
assert_eq!(dispatched(&v1("wai.pc.gpcc", None), all), Some("wai.pc.gpcc"));
|
||
assert_eq!(v1("wai.pc.gpcc", None).check_candidates(), Ok(()));
|
||
// A string this revision does not register is the sink's to advertise
|
||
// (SPEC §4, §8): taken when advertised, the fallback otherwise.
|
||
assert_eq!(dispatched(&v1("wai.image.nope", Some("wai.image.png")), all), Some("wai.image.nope"));
|
||
assert_eq!(dispatched(&v1("wai.image.nope", Some("wai.image.png")), |c| c == "wai.image.png"),
|
||
Some("wai.image.png"));
|
||
// A record or derivation names no payload: never taken, whatever is advertised.
|
||
assert_eq!(dispatched(&v1("wai.video.manifest", Some("wai.image.png")), all), Some("wai.image.png"));
|
||
assert_eq!(dispatched(&v1("wai.derive.crop", None), all), None);
|
||
let m = WaiMulti::new("binary", "improve", serde_json::Value::Null, vec![
|
||
Rendition::media("wai.video.manifest", "bytes", b"r".to_vec()),
|
||
Rendition::media("wai.image.nope", "bytes", b"n".to_vec()),
|
||
]);
|
||
assert!(!m.manifest.renditions[0].is_selectable());
|
||
assert_eq!(m.primary(no_parameter_sets()).unwrap().meta.capability, "wai.image.nope");
|
||
assert_eq!(m.pick(|c| c == "wai.image.nope", no_parameter_sets()).unwrap().meta.capability, "wai.image.nope");
|
||
}
|
||
|
||
/// SPEC §5, *Payloads of the WAI-native integer codecs*: `to_emit_bytes` (the C
|
||
/// packers' emit path) parses an integer payload as a sink does and writes nothing
|
||
/// when it is not its capability's pinned format, as the capability, the fallback
|
||
/// or a `WAI2` component: a keyframe-0 `WIV1` under `wai.video.int_hyper`, a `WIV1`
|
||
/// cut inside its tables, a tool's JSON form. The committed payloads are written.
|
||
#[test]
|
||
fn the_emit_path_refuses_an_integer_payload_that_is_not_its_format() {
|
||
let read = |rel: &str| std::fs::read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors").join(rel)).unwrap();
|
||
let (k0, k1) = (read("int_video/int_video_bitstream.wiv1"), read("int_video/video_nk_bitstream.wiv1"));
|
||
let (mot, hyp) = (crate::codecs::CAP_VIDEO_INT_MOTION, crate::codecs::CAP_VIDEO_INT_HYPER);
|
||
let v1 = |cap: &str, fb: Option<&str>, p: &[u8]| {
|
||
let mut req = ModelRequirement::new(cap);
|
||
if let Some(f) = fb {
|
||
req = req.with_fallback(f);
|
||
}
|
||
Wai::new(Manifest::new("video", "improve", req, Conditioning::new("bytes")), p.to_vec()).to_emit_bytes()
|
||
};
|
||
let code = |r: Result<Vec<u8>, EmitError>| match r {
|
||
Err(EmitError::IntPayload { component, error }) => Some((component, error.code)),
|
||
Err(e) => panic!("{e}"),
|
||
Ok(_) => None,
|
||
};
|
||
assert_eq!(code(v1(hyp, None, &k0)), Some((None, "keyframe_capability_mismatch")));
|
||
assert_eq!(code(v1(mot, None, &k0[..40])), Some((None, "stream_length")));
|
||
assert_eq!(code(v1(mot, None, &read("int_video/int_video_bitstream.json"))), Some((None, "bad_magic")));
|
||
assert_eq!(code(v1(crate::codecs::CAP_VIDEO_AV1, Some(mot), &k1)), Some((None, "keyframe_capability_mismatch")));
|
||
assert_eq!(code(v1(mot, None, &k0)), None);
|
||
assert_eq!(code(v1(hyp, None, &k1)), None);
|
||
let v2 = |a: &[u8], b: &[u8]| WaiMulti::new("video", "improve", serde_json::Value::Null, vec![
|
||
Rendition::media(mot, "bytes", a.to_vec()), Rendition::media(hyp, "bytes", b.to_vec()),
|
||
]).to_emit_bytes();
|
||
assert_eq!(code(v2(&k0, &k0)), Some((Some(1), "keyframe_capability_mismatch")));
|
||
assert_eq!(code(v2(&k0, &k1)), None);
|
||
}
|
||
|
||
/// An encrypted envelope's payload is ciphertext (SPEC §6): the emit paths do not
|
||
/// check it against the integer format, since a sink decrypts it first (§6.4) and
|
||
/// checks the plaintext when it decodes it. A `WAI1` and a `WAI2` that declare an
|
||
/// encryption are written; the same bytes in plaintext are refused.
|
||
#[test]
|
||
fn an_encrypted_integer_payload_is_emitted() {
|
||
let read = |rel: &str| std::fs::read(std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/vectors").join(rel)).unwrap();
|
||
let plain = read("int_video/int_video_bitstream.wiv1");
|
||
let cipher: Vec<u8> = plain.iter().enumerate().map(|(i, b)| b ^ (i as u8).wrapping_mul(151).wrapping_add(7)).collect();
|
||
let mot = crate::codecs::CAP_VIDEO_INT_MOTION;
|
||
let v1 = |security: Option<Security>| {
|
||
let mut m = Manifest::new("video", "improve", ModelRequirement::new(mot), Conditioning::new("bytes"));
|
||
m.security = security;
|
||
Wai::new(m, cipher.clone()).to_emit_bytes()
|
||
};
|
||
assert!(v1(Some(Security::new("aes-256-gcm", 7))).is_ok(), "an encrypted WAI1 is written");
|
||
assert!(matches!(v1(None), Err(EmitError::IntPayload { .. })), "the ciphertext in plaintext is refused");
|
||
assert!(matches!(v1(Some(Security::new("none", 0))), Err(EmitError::IntPayload { .. })), "`none` is plaintext");
|
||
let mut multi = WaiMulti::new("video", "improve", serde_json::Value::Null, vec![Rendition::media(mot, "bytes", cipher.clone())]);
|
||
assert!(matches!(multi.to_emit_bytes(), Err(EmitError::IntPayload { .. })));
|
||
multi.manifest.extra.insert("security".into(), serde_json::json!({"encryption": "mls-sframe", "kid": 0}));
|
||
assert!(multi.to_emit_bytes().is_ok(), "an encrypted WAI2 is written");
|
||
}
|
||
|
||
/// SPEC §6.4 step 2 at the reference dispatchers, which open no scheme: a manifest
|
||
/// that declares an encryption other than `"none"` is inert before any capability
|
||
/// is considered (`unsupported_encryption`), so a payload sent in the clear under a
|
||
/// false claim of encryption is not decoded as plaintext; `"none"`, `null` and an
|
||
/// absent member are plaintext. A `WAI2` `security` member that is not a security
|
||
/// object is inert too, and is never written.
|
||
#[test]
|
||
fn an_encrypted_envelope_is_inert_at_the_reference_dispatchers() {
|
||
let png = "wai.image.png";
|
||
let all = |_: &str| true;
|
||
let v1 = |security: Option<Security>| {
|
||
let mut m = Manifest::new("image", "improve", ModelRequirement::new(png), Conditioning::new("bytes"));
|
||
m.security = security;
|
||
Wai::new(m, b"bytes".to_vec())
|
||
};
|
||
assert_eq!(v1(None).dispatch_verified(all, no_parameter_sets()).unwrap().capability, png);
|
||
assert_eq!(v1(Some(Security::new("none", 0))).dispatch_verified(all, no_parameter_sets()).unwrap().capability, png);
|
||
let e = v1(Some(Security::new("aes-256-gcm", 7))).dispatch_verified(all, no_parameter_sets()).unwrap_err();
|
||
assert_eq!(e.code(), "unsupported_encryption");
|
||
assert!(e.to_string().contains("\"aes-256-gcm\""), "{e}");
|
||
|
||
let multi = |security: Option<Value>| {
|
||
let mut m = WaiMulti::new("image", "improve", Value::Null,
|
||
vec![Rendition::media(png, "bytes", b"a".to_vec()), Rendition::media(png, "bytes", b"b".to_vec())]);
|
||
if let Some(v) = security {
|
||
m.manifest.extra.insert("security".into(), v);
|
||
}
|
||
m
|
||
};
|
||
for plain in [None, Some(Value::Null), Some(serde_json::json!({"encryption": "none"})), Some(serde_json::json!({}))] {
|
||
let m = multi(plain.clone());
|
||
assert_eq!(m.pick(all, no_parameter_sets()).unwrap().index, 0, "{plain:?}");
|
||
assert!(m.to_emit_bytes().is_ok(), "{plain:?}");
|
||
}
|
||
let encrypted = multi(Some(serde_json::json!({"encryption": "mls-sframe", "kid": 0})));
|
||
assert!(encrypted.to_emit_bytes().is_ok(), "an encrypted WAI2 is written");
|
||
let skips = encrypted.pick(all, no_parameter_sets()).unwrap_err();
|
||
assert_eq!(skips, vec![(0, Skip::Encrypted), (1, Skip::Encrypted)]);
|
||
assert_eq!(skips[0].1.code(), "unsupported_encryption");
|
||
for bad in [serde_json::json!(true), serde_json::json!("aes"), serde_json::json!([]), serde_json::json!({"encryption": 5})] {
|
||
let m = multi(Some(bad.clone()));
|
||
assert!(matches!(m.to_emit_bytes(), Err(EmitError::Security(ref why)) if why.contains("is not a security object")), "{bad}");
|
||
let back = WaiMulti::from_bytes(&m.to_bytes().unwrap()).unwrap();
|
||
assert_eq!(back.pick(all, no_parameter_sets()).unwrap_err()[0].1, Skip::Encrypted, "{bad}: read, it is inert");
|
||
}
|
||
}
|
||
|
||
/// The emit path of the C packers (`wai_envelope_pack`,
|
||
/// `wai_envelope_pack_multi`), driven exactly as they drive it: the manifest
|
||
/// JSON parsed into the manifest type, the envelope built, and the bytes taken
|
||
/// from `to_emit_bytes`, which refuses a candidate in every place one can be
|
||
/// named and writes nothing. No codec feature is needed.
|
||
#[test]
|
||
fn the_emit_path_refuses_a_candidate() {
|
||
let v1 = |cap: &str, fb: &str| {
|
||
let json = format!(r#"{{"wai":"1.0","media":"binary","intent":"improve","model_requirement":{{"capability":"{cap}","fallback":{fb}}},"conditioning":{{"kind":"bytes"}},"target":null}}"#);
|
||
let manifest: Manifest = serde_json::from_str(&json).unwrap();
|
||
Wai::new(manifest, b"bytes".to_vec()).to_emit_bytes()
|
||
};
|
||
let refused = v1("wai.splat.sog", "null").unwrap_err();
|
||
assert!(matches!(refused, EmitError::Candidate(CandidateError::AsCapability { .. })), "{refused}");
|
||
let refused = v1("wai.image.png", "\"wai.tensor.nnc\"").unwrap_err();
|
||
assert!(matches!(refused, EmitError::Candidate(CandidateError::AsFallback { .. })), "{refused}");
|
||
assert!(refused.to_string().contains("MUST NOT emit"), "{refused}");
|
||
let ok = v1("wai.image.png", "null").unwrap();
|
||
assert_eq!(Wai::from_bytes(&ok).unwrap().capability(), "wai.image.png");
|
||
|
||
let v2 = |caps: &[(&str, Option<&str>)]| {
|
||
let comps: Vec<String> = caps.iter().map(|(c, of)| match of {
|
||
Some(of) => format!(r#"{{"capability":"{c}","kind":"bytes","role":"companion","companion_of":"{of}"}}"#),
|
||
None => format!(r#"{{"capability":"{c}","kind":"bytes"}}"#),
|
||
}).collect();
|
||
let json = format!(r#"{{"wai":"1.1","media":"binary","intent":"improve","renditions":[{}],"target":null}}"#, comps.join(","));
|
||
let manifest: ManifestV2 = serde_json::from_str(&json).unwrap();
|
||
let payloads = vec![b"p".to_vec(); manifest.renditions.len()];
|
||
WaiMulti::from_parts(manifest, payloads).unwrap().to_emit_bytes()
|
||
};
|
||
// A candidate as a rendition, and as a companion.
|
||
let refused = v2(&[("wai.splat.sog", None)]).unwrap_err();
|
||
assert!(matches!(refused, EmitError::Candidate(CandidateError::AsComponent { index: 0, .. })), "{refused}");
|
||
let refused = v2(&[("wai.video.av1", None), ("wai.tensor.nnc", Some("wai.video.av1"))]).unwrap_err();
|
||
assert!(matches!(refused, EmitError::Candidate(CandidateError::AsComponent { index: 1, .. })), "{refused}");
|
||
assert!(v2(&[("wai.video.av1", None), ("wai.meta.hdr10plus", Some("wai.video.av1"))]).is_ok());
|
||
// The free functions the wasm writers use agree with the methods.
|
||
assert_eq!(check_candidates_v1("wai.splat.sog", None),
|
||
Err(CandidateError::AsCapability { capability: "wai.splat.sog".into() }));
|
||
assert_eq!(check_component_candidates(["wai.image.png", "wai.splat.sog"]),
|
||
Err(CandidateError::AsComponent { index: 1, capability: "wai.splat.sog".into() }));
|
||
assert_eq!(check_component_candidates(["wai.image.png"]), Ok(()));
|
||
}
|
||
|
||
#[test]
|
||
fn version_detection_and_cross_rejection() {
|
||
let v1 = Wai::new(
|
||
Manifest::new("image", "replicate", ModelRequirement::new("wai.image.png"),
|
||
Conditioning::new("png")),
|
||
b"x".to_vec(),
|
||
).to_bytes().unwrap();
|
||
let v2 = sample_multi().to_bytes().unwrap();
|
||
assert_eq!(detect_version(&v1).unwrap(), 1);
|
||
assert_eq!(detect_version(&v2).unwrap(), 2);
|
||
// a v1 reader on a v2 envelope MUST refuse cleanly (no silent
|
||
// fallback — that would hide a major-version mismatch)
|
||
assert!(Wai::from_bytes(&v2).is_err());
|
||
// and vice versa
|
||
assert!(WaiMulti::from_bytes(&v1).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn multi_rejects_truncation() {
|
||
let bytes = sample_multi().to_bytes().unwrap();
|
||
for cut in [3, 7, 30, bytes.len() - 5] {
|
||
assert!(WaiMulti::from_bytes(&bytes[..cut]).is_err(),
|
||
"truncation at {cut} should be rejected");
|
||
}
|
||
}
|
||
|
||
// ---- the parameter-set pin (SPEC §3.1) ----
|
||
|
||
use crate::codecs::{DeclaredTier, ReplicateRefusal, CAP_PRIOR_SET};
|
||
|
||
const MLIC: &str = "wai.neural.int_mlicpp";
|
||
const STREAM: &[u8] = include_bytes!("../tests/vectors/int_mlicpp/stream.wai");
|
||
const STREAM_SET: &[u8] = include_bytes!("../tests/vectors/int_mlicpp/stream_set.wai");
|
||
const MODEL_SHA: &str = "7ef44f3b1fc4a2df6c3e3b68460f595cae3d31cb7f9018699e29e738340272a4";
|
||
const LISTING_SHA: &str = "85a05b0dac15566c5a657ddeee253eaa9f54ce852ae04924f5b04151901d9d1b";
|
||
|
||
#[test]
|
||
fn pin_members_round_trip_and_stay_absent() {
|
||
let a = "a1".repeat(32);
|
||
let p = Prior::new("m", "ab".repeat(32), "int")
|
||
.with_tier(DeclaredTier::DecodeEquivalence)
|
||
.with_supersedes(Supersedes::new(a.clone()));
|
||
assert_eq!(
|
||
serde_json::to_string(&p).unwrap(),
|
||
format!(r#"{{"id":"m","sha256":"{}","determinism":"int","tier":"decode-equivalence","supersedes":{{"sha256":"{a}"}}}}"#,
|
||
"ab".repeat(32))
|
||
);
|
||
let back: Prior = serde_json::from_str(&serde_json::to_string(&p).unwrap()).unwrap();
|
||
assert_eq!(back, p);
|
||
// `with_tier(None)` writes the registered token "none".
|
||
assert_eq!(Prior::new("m", "ab".repeat(32), "int").with_tier(DeclaredTier::None).tier(), Some("none"));
|
||
// `Prior::new` writes none of the optional members.
|
||
assert_eq!(serde_json::to_string(&Prior::new("m", "ab", "d")).unwrap(),
|
||
r#"{"id":"m","sha256":"ab","determinism":"d"}"#);
|
||
// A member this revision does not define is kept.
|
||
let future = r#"{"id":"m","sha256":"ab","determinism":"d","later":{"x":1}}"#;
|
||
let f: Prior = serde_json::from_str(future).unwrap();
|
||
assert_eq!(serde_json::to_string(&f).unwrap(), future);
|
||
}
|
||
|
||
#[test]
|
||
fn null_member_equals_absent() {
|
||
// SPEC §3.1: `null` in an optional pin member is the same as absent.
|
||
let with_nulls = r#"{"id":"m","sha256":"ab","determinism":"d","form":null,"tier":null,"supersedes":null}"#;
|
||
let p: Prior = serde_json::from_str(with_nulls).unwrap();
|
||
assert_eq!(p, Prior::new("m", "ab", "d"));
|
||
assert_eq!(serde_json::to_string(&p).unwrap(), r#"{"id":"m","sha256":"ab","determinism":"d"}"#);
|
||
// A member of the wrong JSON type is a malformed manifest.
|
||
assert!(serde_json::from_str::<Prior>(r#"{"id":"m","sha256":"ab","determinism":"d","tier":3}"#).is_err());
|
||
assert!(serde_json::from_str::<Prior>(r#"{"id":"m","sha256":"ab","determinism":"d","supersedes":"x"}"#).is_err());
|
||
// The same holds for a rendition entry's identity members (SPEC §7.1).
|
||
let entry = |extra: &str| serde_json::from_str::<RenditionMeta>(
|
||
&format!(r#"{{"capability":"wai.image.png","kind":"png"{extra}}}"#));
|
||
assert!(entry(r#","id":null,"sha256":null,"companion_of_id":null"#).is_ok());
|
||
for bad in [r#","id":7"#, r#","sha256":true"#, r#","companion_of_id":["a"]"#] {
|
||
assert!(entry(bad).is_err(), "{bad}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn committed_int_mlicpp_envelope_round_trips_byte_for_byte() {
|
||
for (bytes, len) in [(STREAM, 859usize), (STREAM_SET, 910)] {
|
||
assert_eq!(bytes.len(), len);
|
||
let w = Wai::from_bytes(bytes).unwrap();
|
||
assert_eq!(w.to_bytes().unwrap(), bytes, "re-serialization must keep every byte");
|
||
let p = w.prior().unwrap();
|
||
assert_eq!(p.check_syntax(), Ok(()));
|
||
assert_eq!(p.declared_tier(), Some(DeterminismTier::DecodeEquivalence));
|
||
assert_eq!(w.replicate_refusal(), None);
|
||
assert_eq!(w.check_pin(), Ok(()));
|
||
}
|
||
let legacy = Wai::from_bytes(STREAM).unwrap();
|
||
assert_eq!(legacy.prior().unwrap().sha256, MODEL_SHA);
|
||
assert_eq!((legacy.prior().unwrap().form(), legacy.prior().unwrap().tier()), (None, None));
|
||
let set = Wai::from_bytes(STREAM_SET).unwrap();
|
||
assert_eq!(set.prior().unwrap().sha256, LISTING_SHA);
|
||
assert_eq!(set.prior().unwrap().form(), Some(CAP_PRIOR_SET));
|
||
// The two envelopes differ only in their pins.
|
||
assert_eq!(set.payload, legacy.payload);
|
||
let strip = |w: &Wai| { let mut m = w.manifest.clone(); m.conditioning.prior = None; serde_json::to_string(&m).unwrap() };
|
||
assert_eq!(strip(&set), strip(&legacy));
|
||
}
|
||
|
||
#[test]
|
||
fn pin_syntax_is_checked() {
|
||
let good = Prior::new("m", MODEL_SHA, "int");
|
||
assert_eq!(good.check_syntax(), Ok(()));
|
||
for bad in [MODEL_SHA.to_ascii_uppercase(), MODEL_SHA[..63].to_string(),
|
||
format!("{}g", &MODEL_SHA[..63]), String::new()] {
|
||
assert_eq!(Prior::new("m", bad.clone(), "int").check_syntax().unwrap_err().code(), "pin_malformed", "{bad}");
|
||
}
|
||
// `supersedes` is informative: a bad one is a static finding, not a refusal.
|
||
let selfref = good.clone().with_supersedes(Supersedes::new(MODEL_SHA));
|
||
assert_eq!(selfref.check_syntax(), Ok(()));
|
||
assert!(selfref.supersedes_finding().unwrap().contains("equals"));
|
||
assert!(selfref.usable_supersedes().is_none());
|
||
let badform = good.clone().with_supersedes(Supersedes::new(LISTING_SHA).with_form("wai.image.png"));
|
||
assert!(badform.supersedes_finding().unwrap().contains("not a registered prior form"));
|
||
let ok = good.clone().with_supersedes(Supersedes::new(LISTING_SHA).with_form(CAP_PRIOR_SET));
|
||
assert_eq!(ok.supersedes_finding(), None);
|
||
// An unregistered tier token is not malformed; it declares no tier.
|
||
let mut t = good.clone();
|
||
t.tier = Some("decode_equivalence".into());
|
||
assert_eq!(t.check_syntax(), Ok(()));
|
||
assert!(!t.tier_is_registered());
|
||
assert_eq!(t.declared_tier(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn replicate_reads_the_declared_tier() {
|
||
let synth = "wai.neural.int_synth";
|
||
let env = |prior: Option<Prior>| {
|
||
let mut c = Conditioning::new("int_synth");
|
||
c.prior = prior;
|
||
Wai::new(Manifest::new("image", "replicate", ModelRequirement::new(synth), c), vec![])
|
||
};
|
||
let code = |w: &Wai| w.replicate_refusal().map(|r| r.code());
|
||
let int = Prior::new("m", "ab".repeat(32), "int-rans/p16+int-synth");
|
||
assert_eq!(code(&env(None)), Some("replicate_unpinned"));
|
||
assert_eq!(code(&env(Some(int.clone().with_tier(DeclaredTier::None)))), Some("replicate_tier"));
|
||
assert_eq!(code(&env(Some(int.clone().with_tier(DeclaredTier::EntropyConsistency)))), Some("replicate_tier"));
|
||
assert_eq!(code(&env(Some(Prior::new("m", "ab".repeat(32), "onnx-f32")))), Some("replicate_tier"));
|
||
assert_eq!(code(&env(Some(int.clone().with_form(NNC)))), Some("replicate_form_not_exact"));
|
||
// A set-form pin declaring decode-equivalence is sound.
|
||
assert_eq!(code(&env(Some(int.clone().with_form(CAP_PRIOR_SET).with_tier(DeclaredTier::DecodeEquivalence)))), None);
|
||
// An explicit tier wins over the words, which are informative.
|
||
let words_float = Prior::new("m", "ab".repeat(32), "onnx-cpu-f32").with_tier(DeclaredTier::DecodeEquivalence);
|
||
assert_eq!(code(&env(Some(words_float))), None);
|
||
// An unregistered tier token reads as none.
|
||
let mut odd = int.clone();
|
||
odd.tier = Some("decode_equivalence".into());
|
||
assert_eq!(code(&env(Some(odd))), Some("replicate_tier"));
|
||
// The legacy reading is unchanged for tier-less pins.
|
||
assert_eq!(code(&env(Some(int))), None);
|
||
assert_eq!(ReplicateRefusal::Unregistered.code(), "replicate_unregistered");
|
||
assert_eq!(ReplicateRefusal::Class(crate::codecs::ReplicateClass::NeuralFloat).code(), "replicate_class");
|
||
}
|
||
|
||
fn mlic_env(prior: Option<Prior>, fallback: Option<&str>) -> Wai {
|
||
let mut req = ModelRequirement::new(MLIC);
|
||
req.fallback = fallback.map(String::from);
|
||
let mut c = Conditioning::new("int_mlicpp");
|
||
c.prior = prior;
|
||
Wai::new(Manifest::new("image", "replicate", req, c), b"payload".to_vec())
|
||
}
|
||
|
||
#[test]
|
||
fn wai1_dispatch_treats_a_refused_pin_as_unsupported() {
|
||
let all = |_: &str| true;
|
||
let pin = Prior::new("m", MODEL_SHA, "int");
|
||
let refuse = |c: &str, _: &Prior| -> Result<(), PinRefusal> { Err(PinRefusal::NotHeld { capability: c.into() }) };
|
||
// A refused pin falls back to png.
|
||
let w = mlic_env(Some(pin.clone()), Some("wai.image.png"));
|
||
let d = w.dispatch_verified(all, refuse).unwrap();
|
||
assert_eq!((d.capability, d.verified), ("wai.image.png", None));
|
||
// With no fallback the envelope is inert and names the reason.
|
||
let e = mlic_env(Some(pin.clone()), None).dispatch_verified(all, refuse).unwrap_err();
|
||
assert_eq!(e.code(), "prior_not_held");
|
||
assert!(matches!(e, Inert::PinRefused { ref capability, .. } if capability == MLIC));
|
||
// A verified pin dispatches the primary, carrying what the verifier returned.
|
||
let d = w.dispatch_verified(all, |_, p: &Prior| Ok::<_, PinRefusal>(p.sha256.clone())).unwrap();
|
||
assert_eq!((d.capability, d.verified.as_deref()), (MLIC, Some(MODEL_SHA)));
|
||
// A pin on a capability that decodes against no parameter set never calls the verifier.
|
||
let mut calls = 0;
|
||
let png = Wai::new(Manifest::new("image", "replicate", ModelRequirement::new("wai.image.png"),
|
||
Conditioning::new("png").with_prior(pin.clone())), vec![]);
|
||
let d = png.dispatch_verified(all, |_, _| { calls += 1; Ok::<(), PinRefusal>(()) }).unwrap();
|
||
assert_eq!((d.capability, calls), ("wai.image.png", 0));
|
||
// One pin applies to whichever capability decodes: the fallback is verified too.
|
||
let mut seen = Vec::new();
|
||
let e = mlic_env(Some(pin.clone()), Some("wai.neural.int_synth"))
|
||
.dispatch_verified(all, |c, _| { seen.push(c.to_string()); Err::<(), _>(PinRefusal::NotHeld { capability: c.into() }) })
|
||
.unwrap_err();
|
||
assert_eq!(seen, [MLIC, "wai.neural.int_synth"]);
|
||
assert_eq!(e, Inert::PinRefused { capability: MLIC.into(), refusal: PinRefusal::NotHeld { capability: MLIC.into() } });
|
||
// An unpinned envelope dispatches without verification, and an unadvertised one is inert.
|
||
assert_eq!(mlic_env(None, None).dispatch_verified(all, refuse).unwrap().capability, MLIC);
|
||
assert_eq!(mlic_env(None, None).dispatch_verified(|_| false, refuse).unwrap_err(), Inert::MissingCapability);
|
||
// A candidate is never dispatched, whatever its pin and whatever the sink
|
||
// advertises (SPEC §4, §5): step 5 takes the fallback, and the candidate's
|
||
// pin is never verified.
|
||
let mut seen = Vec::new();
|
||
let cand = Wai::new(Manifest::new("binary", "improve",
|
||
ModelRequirement::new(crate::codecs::CAP_SPLAT_SOG).with_fallback("wai.image.png"),
|
||
Conditioning::new("bytes").with_prior(pin.clone())), vec![]);
|
||
let d = cand.dispatch_verified(all, |c, _| { seen.push(c.to_string()); Ok::<(), PinRefusal>(()) }).unwrap();
|
||
assert_eq!(d.capability, "wai.image.png");
|
||
assert!(seen.is_empty(), "{seen:?}");
|
||
// An unregistered capability with a pin is verified (fail closed).
|
||
let mut calls = 0;
|
||
let future = Wai::new(Manifest::new("image", "replicate", ModelRequirement::new("wai.neural.future"),
|
||
Conditioning::new("x").with_prior(pin)), vec![]);
|
||
let _ = future.dispatch_verified(all, |_, _| { calls += 1; Ok::<(), PinRefusal>(()) });
|
||
assert_eq!(calls, 1);
|
||
}
|
||
|
||
/// A verifier over held digests, as a sink holding one-file sets would run it.
|
||
fn holding(held: &'static [&'static str]) -> impl FnMut(&str, &Prior) -> Result<(), PinRefusal> {
|
||
move |cap: &str, p: &Prior| {
|
||
if held.contains(&p.sha256.as_str()) {
|
||
Ok(())
|
||
} else if p.usable_supersedes().is_some_and(|s| held.contains(&s.sha256.as_str())) {
|
||
Err(PinRefusal::Superseded { capability: cap.into(), predecessor: p.supersedes().unwrap().sha256.clone() })
|
||
} else {
|
||
Err(PinRefusal::NotHeld { capability: cap.into() })
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn wai2_pick_picks_the_rendition_whose_pin_resolves() {
|
||
const A: &str = "aa00000000000000000000000000000000000000000000000000000000000000";
|
||
const B: &str = "bb00000000000000000000000000000000000000000000000000000000000000";
|
||
const C: &str = "cc00000000000000000000000000000000000000000000000000000000000000";
|
||
let m = WaiMulti::new("image", "replicate", Value::Null, vec![
|
||
Rendition::media(MLIC, "int_mlicpp", b"B".to_vec())
|
||
.with_prior(Prior::new("b", B, "int").with_tier(DeclaredTier::DecodeEquivalence)
|
||
.with_supersedes(Supersedes::new(A))),
|
||
Rendition::media(MLIC, "int_mlicpp", b"A".to_vec())
|
||
.with_prior(Prior::new("a", A, "int").with_tier(DeclaredTier::DecodeEquivalence)),
|
||
Rendition::media("wai.image.avif", "avif", b"AVIF".to_vec()),
|
||
]);
|
||
let all = |_: &str| true;
|
||
let codes = |s: &[(usize, Skip)]| s.iter().map(|(i, k)| (*i, k.code())).collect::<Vec<_>>();
|
||
let p = m.pick(all, holding(&[A])).unwrap();
|
||
assert_eq!((p.index, p.payload), (1, &b"A"[..]));
|
||
assert_eq!(codes(&p.skipped), [(0, "digest_superseded")]);
|
||
assert_eq!(m.pick(all, holding(&[B])).unwrap().index, 0);
|
||
let p = m.pick(all, holding(&[C])).unwrap();
|
||
assert_eq!(p.index, 2);
|
||
assert_eq!(codes(&p.skipped), [(0, "prior_not_held"), (1, "prior_not_held")]);
|
||
// Nothing left: inert, with a reason for every entry.
|
||
let e = m.pick(|c| c != "wai.image.avif", holding(&[C])).unwrap_err();
|
||
assert_eq!(codes(&e), [(0, "prior_not_held"), (1, "prior_not_held"), (2, "missing_capability")]);
|
||
// The pin decides, not the capability.
|
||
assert_eq!(m.pick(|c| c == MLIC, holding(&[A])).unwrap().index, 1);
|
||
assert_eq!(m.primary(holding(&[B])).unwrap().index, 0);
|
||
// A candidate is never picked, whatever its pin and whatever the sink
|
||
// advertises: it is skipped as not selectable before its pin is verified
|
||
// (SPEC §4, §7.1).
|
||
let mut calls = 0;
|
||
let c = WaiMulti::new("binary", "improve", Value::Null, vec![
|
||
Rendition::media(crate::codecs::CAP_TENSOR_NNC, "bytes", b"T".to_vec())
|
||
.with_prior(Prior::new("t", A, "int")),
|
||
Rendition::media("wai.image.avif", "avif", b"AVIF".to_vec()),
|
||
]);
|
||
let p = c.pick(all, |_, _| { calls += 1; Ok::<(), PinRefusal>(()) }).unwrap();
|
||
assert_eq!((p.index, calls), (1, 0));
|
||
assert_eq!(codes(&p.skipped), [(0, "not_selectable")]);
|
||
assert_eq!(codes(&c.pick(|cap| cap == crate::codecs::CAP_TENSOR_NNC, holding(&[A])).unwrap_err()),
|
||
[(0, "not_selectable"), (1, "missing_capability")]);
|
||
}
|
||
|
||
#[test]
|
||
fn manifest_bytes_are_the_original() {
|
||
let v1_manifest = br#"{"wai":"1.0", "media":"image","intent":"replicate","model_requirement":{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"security":{"encryption":"none","kid":0},"target":null}"#;
|
||
let mut env = b"WAI1".to_vec();
|
||
env.extend_from_slice(&(v1_manifest.len() as u32).to_le_bytes());
|
||
env.extend_from_slice(v1_manifest);
|
||
env.extend_from_slice(&3u32.to_le_bytes());
|
||
env.extend_from_slice(b"abc");
|
||
assert_eq!(manifest_bytes(&env).unwrap(), v1_manifest);
|
||
// The Rust types now keep `security` too, re-serialized without the whitespace.
|
||
let w = Wai::from_bytes(&env).unwrap();
|
||
assert_eq!(w.manifest.security, Some(Security::new("none", 0)));
|
||
assert!(serde_json::to_string(&w.manifest).unwrap().contains(r#""security":{"encryption":"none","kid":0}"#));
|
||
let v2 = WaiMulti::from_parts(
|
||
serde_json::from_str(r#"{"wai":"1.1","media":"audio","intent":"replicate","renditions":[{"capability":"wai.audio.opus","kind":"opus","later":true}],"target":null}"#).unwrap(),
|
||
vec![b"OPUS".to_vec()]).unwrap().to_bytes().unwrap();
|
||
let m = manifest_bytes(&v2).unwrap();
|
||
assert!(std::str::from_utf8(m).unwrap().contains(r#""later":true"#));
|
||
assert!(manifest_bytes(&env[..10]).is_err());
|
||
assert!(manifest_bytes(b"WAI3\0\0\0\0").is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn static_pin_findings() {
|
||
let code = |w: &Wai| w.check_pin().err().map(|f| f.code);
|
||
let png = |p: Prior| Wai::new(Manifest::new("image", "replicate", ModelRequirement::new("wai.image.png"),
|
||
Conditioning::new("png").with_prior(p)), vec![]);
|
||
let on = |cap: &str, intent: &str, p: Option<Prior>| {
|
||
let mut c = Conditioning::new("k");
|
||
c.prior = p;
|
||
Wai::new(Manifest::new("image", intent, ModelRequirement::new(cap), c), vec![])
|
||
};
|
||
let pin = Prior::new("m", MODEL_SHA, "int");
|
||
assert_eq!(code(&png(pin.clone())), Some("prior_not_applicable"));
|
||
assert_eq!(code(&on("wai.neural.bmshj2018", "improve", Some(pin.clone()))), Some("form_required"));
|
||
assert_eq!(code(&on("wai.neural.future", "improve", Some(pin.clone()))), Some("form_required"));
|
||
assert_eq!(code(&on("wai.image.jpegai", "improve",
|
||
Some(pin.clone().with_form(CAP_PRIOR_SET).with_tier(DeclaredTier::DecodeEquivalence)))),
|
||
Some("tier_exceeds_class"));
|
||
assert_eq!(code(&on("wai.image.jpegai", "improve",
|
||
Some(pin.clone().with_form(CAP_PRIOR_SET).with_tier(DeclaredTier::EntropyConsistency)))),
|
||
None);
|
||
assert_eq!(code(&on("wai.neural.int_synth", "replicate", Some(pin.clone().with_tier(DeclaredTier::None)))),
|
||
Some("replicate_tier"));
|
||
assert_eq!(code(&on("wai.neural.int_synth", "replicate", None)), Some("replicate_unpinned"));
|
||
assert_eq!(code(&on(MLIC, "replicate", Some(Prior::new("m", MODEL_SHA.to_ascii_uppercase(), "int")))),
|
||
Some("pin_malformed"));
|
||
let mut odd = pin.clone();
|
||
odd.tier = Some("exact".into());
|
||
assert_eq!(code(&on(MLIC, "improve", Some(odd))), Some("tier_unregistered"));
|
||
assert_eq!(code(&on(MLIC, "improve", Some(pin.clone().with_supersedes(Supersedes::new(MODEL_SHA))))),
|
||
Some("supersedes_malformed"));
|
||
assert_eq!(code(&on(MLIC, "improve", Some(pin.clone().with_form("wai.image.png")))),
|
||
Some("form_not_registered"));
|
||
assert_eq!(code(&Wai::from_bytes(STREAM).unwrap()), None);
|
||
// Order: prior_not_applicable before pin_malformed.
|
||
assert_eq!(code(&png(Prior::new("m", "AB", "int"))), Some("prior_not_applicable"));
|
||
for c in PIN_FINDING_CODES {
|
||
assert!(c.chars().all(|ch| ch.is_ascii_lowercase() || ch == '_'), "{c} is snake_case");
|
||
}
|
||
}
|
||
|
||
// ---- component identity (SPEC §7.1) ----
|
||
|
||
#[test]
|
||
fn component_ids_and_digests_round_trip_and_stay_absent() {
|
||
let m = WaiMulti::new("video", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC".to_vec()).with_id("base-hd").with_payload_digest(),
|
||
Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), "wai.video.hevc").with_companion_of_id("base-hd"),
|
||
]);
|
||
let bytes = m.to_bytes().unwrap();
|
||
let back = WaiMulti::from_bytes(&bytes).unwrap();
|
||
assert_eq!(back.to_bytes().unwrap(), bytes);
|
||
let e = &back.manifest.renditions;
|
||
assert_eq!(e[0].id.as_deref(), Some("base-hd"));
|
||
assert_eq!(e[0].sha256.as_deref(), Some(crate::pin::sha256_hex(b"HEVC").as_str()));
|
||
assert_eq!(e[1].companion_of_id.as_deref(), Some("base-hd"));
|
||
// Absent members are not written.
|
||
let plain = WaiMulti::new("audio", "replicate", Value::Null, vec![Rendition::media("wai.audio.opus", "opus", vec![1])]);
|
||
let j = serde_json::to_string(&plain.manifest).unwrap();
|
||
assert_eq!(j, r#"{"wai":"1.1","media":"audio","intent":"replicate","renditions":[{"capability":"wai.audio.opus","kind":"opus"}],"target":null}"#);
|
||
}
|
||
|
||
#[test]
|
||
fn component_digest_mismatch_skips_the_entry() {
|
||
let mut m = WaiMulti::new("image", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.image.png", "png", b"PNG-A".to_vec()).with_payload_digest(),
|
||
Rendition::media("wai.image.png", "png", b"PNG-B".to_vec()).with_payload_digest(),
|
||
Rendition::companion("wai.meta.hdr10plus", b"META".to_vec(), "wai.image.png").with_payload_digest(),
|
||
]);
|
||
assert_eq!(m.pick(|_| true, accept).unwrap().index, 0);
|
||
m.renditions[0][0] ^= 1;
|
||
let p = m.pick(|_| true, accept).unwrap();
|
||
assert_eq!(p.index, 1);
|
||
assert_eq!(p.skipped, [(0, Skip::ComponentDigest)]);
|
||
assert_eq!(Skip::ComponentDigest.code(), "component_digest_mismatch");
|
||
let class = crate::codecs::replicate_refusal("wai.image.jpegai", "replicate", None).unwrap();
|
||
assert_eq!(Skip::Replicate(class).code(), "replicate_class");
|
||
// A companion whose payload does not match is not applied.
|
||
assert_eq!(m.companions_of_index(1).len(), 1);
|
||
m.renditions[2][0] ^= 1;
|
||
assert!(m.companions_of_index(1).is_empty());
|
||
// A malformed digest matches nothing.
|
||
let mut bad = RenditionMeta::new("wai.image.png", "png");
|
||
bad.sha256 = Some("AB".repeat(32));
|
||
assert!(!bad.payload_digest_matches(b""));
|
||
}
|
||
|
||
#[test]
|
||
fn companion_of_id_binds_one_of_two_same_capability_renditions() {
|
||
let two = |bind: bool| {
|
||
let enh = Rendition::companion("wai.video.lcevc", b"ENH".to_vec(), "wai.video.hevc");
|
||
WaiMulti::new("video", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-A".to_vec()).with_id("a"),
|
||
Rendition::media("wai.video.hevc", "hevc", b"HEVC-B".to_vec()).with_id("b"),
|
||
if bind { enh.with_companion_of_id("b") } else { enh },
|
||
])
|
||
};
|
||
assert_eq!(two(true).check_component_roles(), Ok(()));
|
||
// Without the id the binding is ambiguous, as an earlier sink reads it.
|
||
assert_eq!(two(false).check_component_roles(),
|
||
Err(RoleError::EnhancementBaseNotUnique { index: 2, capability: "wai.video.lcevc".into(), matches: 2 }));
|
||
// The layer applies to entry 1 only.
|
||
let m = two(true);
|
||
assert!(m.companions_of_index(0).is_empty());
|
||
assert_eq!(m.companions_of_index(1).iter().map(|c| c.0).collect::<Vec<_>>(), [2]);
|
||
// Two layers on one base by id are stacked: refused by index.
|
||
let stacked = WaiMulti::new("video", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"A".to_vec()).with_id("a"),
|
||
Rendition::media("wai.video.hevc", "hevc", b"B".to_vec()).with_id("b"),
|
||
Rendition::companion("wai.video.lcevc", b"E1".to_vec(), "wai.video.hevc").with_companion_of_id("a"),
|
||
Rendition::companion("wai.video.lcevc", b"E2".to_vec(), "wai.video.hevc").with_companion_of_id("a"),
|
||
]);
|
||
assert!(matches!(stacked.check_component_roles(), Err(RoleError::DuplicateEnhancementLayer { index: 3, first: 2, .. })));
|
||
// One layer per base, by id, conforms.
|
||
let each = WaiMulti::new("video", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"A".to_vec()).with_id("a"),
|
||
Rendition::media("wai.video.hevc", "hevc", b"B".to_vec()).with_id("b"),
|
||
Rendition::companion("wai.video.lcevc", b"E1".to_vec(), "wai.video.hevc").with_companion_of_id("a"),
|
||
Rendition::companion("wai.video.lcevc", b"E2".to_vec(), "wai.video.hevc").with_companion_of_id("b"),
|
||
]);
|
||
assert_eq!(each.check_component_roles(), Ok(()));
|
||
}
|
||
|
||
#[test]
|
||
fn component_id_grammar_and_uniqueness() {
|
||
for ok in ["a", "base-hd", "r_1", &"x".repeat(64)] {
|
||
assert!(is_component_id(ok), "{ok}");
|
||
}
|
||
for bad in ["", "Base", "a.b", "wai.video.hevc", "a b", &"x".repeat(65)] {
|
||
assert!(!is_component_id(bad), "{bad}");
|
||
}
|
||
let with = |ids: [&str; 2]| WaiMulti::new("image", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.image.png", "png", vec![1]).with_id(ids[0]),
|
||
Rendition::media("wai.image.avif", "avif", vec![2]).with_id(ids[1]),
|
||
]);
|
||
assert_eq!(with(["a", "a"]).check_component_roles(), Err(RoleError::DuplicateComponentId { index: 1, first: 0 }));
|
||
assert_eq!(with(["a", "B"]).check_component_roles(), Err(RoleError::ComponentIdMalformed { index: 1 }));
|
||
// companion_of_id must resolve, and name an entry of capability companion_of.
|
||
let m = |id: &str| WaiMulti::new("video", "replicate", Value::Null, vec![
|
||
Rendition::media("wai.video.hevc", "hevc", b"H".to_vec()).with_id("h"),
|
||
Rendition::media("wai.video.av1", "av1", b"A".to_vec()).with_id("v"),
|
||
Rendition::companion("wai.meta.hdr10plus", b"M".to_vec(), "wai.video.hevc").with_companion_of_id(id),
|
||
]);
|
||
assert_eq!(m("h").check_component_roles(), Ok(()));
|
||
assert_eq!(m("zz").check_component_roles(), Err(RoleError::CompanionIdUnresolved { index: 2, id: "zz".into() }));
|
||
assert_eq!(m("v").check_component_roles(),
|
||
Err(RoleError::CompanionIdCapabilityMismatch { index: 2, id: "v".into(), capability: "wai.video.av1".into() }));
|
||
}
|
||
|
||
/// vocab/v1 maps the pin's members: the scoped JSON-LD term `prior` names
|
||
/// exactly the members a pin serializes, and the SHACL shape's tier list is
|
||
/// the registered tokens.
|
||
#[test]
|
||
fn vocab_pin_terms_match_prior_members() {
|
||
// v1 carries the pin terms as additive terms; v2 is generated from the
|
||
// registry (`wai registry export`) and carries the same terms and shapes.
|
||
for (name, context, shapes) in [
|
||
("v1", include_str!("../../vocab/v1.jsonld"), include_str!("../../vocab/v1.shapes.ttl")),
|
||
("v2", include_str!("../../vocab/v2.jsonld"), include_str!("../../vocab/v2.shapes.ttl")),
|
||
] {
|
||
let ctx: Value = serde_json::from_str(context).unwrap();
|
||
let mut terms: Vec<&str> = ctx["@context"]["prior"]["@context"].as_object()
|
||
.unwrap_or_else(|| panic!("{name}: a scoped `prior` term"))
|
||
.keys().map(String::as_str).filter(|k| !k.starts_with('@')).collect();
|
||
terms.sort_unstable();
|
||
let full = Prior::new("m", "ab".repeat(32), "int").with_form(CAP_PRIOR_SET)
|
||
.with_tier(DeclaredTier::DecodeEquivalence).with_supersedes(Supersedes::new("cd".repeat(32)));
|
||
let v = serde_json::to_value(&full).unwrap();
|
||
let mut members: Vec<&str> = v.as_object().unwrap().keys().map(String::as_str).collect();
|
||
members.sort_unstable();
|
||
assert_eq!(terms, members, "{name}");
|
||
assert!(ctx["@context"]["ParameterSetPin"].as_str().is_some_and(|t| t.ends_with("ParameterSetPin")), "{name}");
|
||
let tiers: Vec<String> = crate::codecs::TIER_TOKENS.iter().map(|(t, _)| format!("\"{t}\"")).collect();
|
||
assert!(shapes.contains(&format!("sh:in ( {} )", tiers.join(" "))), "{name}: the shape's tier list");
|
||
// The context types no `prior` object, so the pin shape must target the
|
||
// objects of wai:prior (a class target would never fire), and `supersedes`
|
||
// is checked as a digest and a form, not as a whole pin.
|
||
let pin_shape = shapes.split("wai:ParameterSetPinShape a sh:NodeShape").nth(1)
|
||
.unwrap_or_else(|| panic!("{name}: a ParameterSetPinShape"))
|
||
.split("\n\n").next().unwrap();
|
||
assert!(pin_shape.contains("sh:targetObjectsOf wai:prior"), "{name}: {pin_shape}");
|
||
assert!(!pin_shape.contains("sh:targetClass"), "{name}: {pin_shape}");
|
||
assert!(pin_shape.contains("sh:path wai:pinSupersedes ; sh:node wai:SupersedesShape"), "{name}: {pin_shape}");
|
||
let sup = shapes.split("wai:SupersedesShape a sh:NodeShape").nth(1)
|
||
.unwrap_or_else(|| panic!("{name}: a SupersedesShape"))
|
||
.split("\n\n").next().unwrap();
|
||
assert!(sup.contains("sh:path wai:pinSha256") && sup.contains("sh:path wai:pinForm"), "{name}: {sup}");
|
||
assert!(!sup.contains("wai:pinId") && !sup.contains("wai:pinDeterminism"), "{name}: {sup}");
|
||
}
|
||
// v2 defines each pin term once, the tier property among them.
|
||
let v2 = include_str!("../../vocab/v2.ttl");
|
||
for term in ["ParameterSetPin", "prior", "pinId", "pinSha256", "pinDeterminism", "pinForm", "pinTier", "pinSupersedes"] {
|
||
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: "[email protected]".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\":\"[email protected]\",\"settings\":\"{}\"}},\"target\"", "ab".repeat(32), "cd".repeat(32))), "{text}");
|
||
let back = Wai::from_bytes(&with).unwrap();
|
||
assert_eq!(DerivedFrom::from_value(back.manifest.derived_from.as_ref().unwrap()).unwrap(), d);
|
||
assert_eq!(back.to_bytes().unwrap(), with, "the member round-trips byte for byte");
|
||
// A malformed member parses; the typed reading names what is wrong.
|
||
let bad = String::from_utf8(with[8..8 + u32::from_le_bytes(with[4..8].try_into().unwrap()) as usize].to_vec()).unwrap().replace(&"ab".repeat(32), &"AB".repeat(32));
|
||
let m2: Manifest = serde_json::from_str(&bad).unwrap();
|
||
assert_eq!(DerivedFrom::from_value(m2.derived_from.as_ref().unwrap()).unwrap_err().0, "motif is not 64 lowercase hexadecimal digits");
|
||
for (v, why) in [
|
||
(serde_json::json!(1), "derived_from is not an object"),
|
||
(serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "", "renderer": "r", "settings": "cd".repeat(32)}), "motif_capability is not a non-empty string"),
|
||
(serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "c", "settings": "cd".repeat(32)}), "renderer is not a non-empty string"),
|
||
(serde_json::json!({"motif": "ab".repeat(32), "motif_capability": "c", "renderer": "r", "settings": "x"}), "settings is not 64 lowercase hexadecimal digits"),
|
||
] {
|
||
assert_eq!(DerivedFrom::from_value(&v).unwrap_err().0, why);
|
||
}
|
||
// An entry of a WAI2 carries it the same way.
|
||
let e = RenditionMeta::new("wai.video.av1", "av1").with_derived_from(&d);
|
||
let j = serde_json::to_string(&e).unwrap();
|
||
assert!(j.ends_with(&format!("\"settings\":\"{}\"}}}}", "cd".repeat(32))), "{j}");
|
||
let back: RenditionMeta = serde_json::from_str(&j).unwrap();
|
||
assert_eq!(DerivedFrom::from_value(back.derived_from.as_ref().unwrap()).unwrap(), d);
|
||
}
|
||
|
||
/// A `WAI1` (or, with `v2`, a `WAI2`) envelope whose manifest is exactly `len`
|
||
/// bytes: a small manifest with one member no revision defines, `pad`, filled to
|
||
/// length (a reader ignores such a member, SPEC §8), over a 3-byte payload.
|
||
fn envelope_with_manifest_len(v2: bool, len: usize) -> Vec<u8> {
|
||
let head = if v2 {
|
||
r#"{"wai":"1.1","media":"text","intent":"replicate","renditions":[{"capability":"wai.text.zstd","kind":"zstd"}],"target":null,"pad":""#
|
||
} else {
|
||
r#"{"wai":"1.0","media":"text","intent":"replicate","model_requirement":{"capability":"wai.text.zstd"},"conditioning":{"kind":"zstd"},"target":null,"pad":""#
|
||
};
|
||
let m = format!("{head}{}\"}}", "x".repeat(len - head.len() - 2));
|
||
assert_eq!(m.len(), len);
|
||
let mut b = if v2 { MAGIC_V2.to_vec() } else { MAGIC.to_vec() };
|
||
b.extend_from_slice(&(m.len() as u32).to_le_bytes());
|
||
b.extend_from_slice(m.as_bytes());
|
||
if v2 {
|
||
b.extend_from_slice(&1u16.to_le_bytes());
|
||
b.extend_from_slice(&0u32.to_le_bytes());
|
||
}
|
||
b.extend_from_slice(&3u32.to_le_bytes());
|
||
b.extend_from_slice(b"abc");
|
||
b
|
||
}
|
||
|
||
/// SPEC §2, *Reader limits*, from the length field: eight bytes declaring a manifest
|
||
/// of exactly `MANIFEST_MAX_LEN` bytes are refused only as truncated, and one byte
|
||
/// more, or the most a u32 can declare, as too large, by both readers and by
|
||
/// `manifest_bytes`. A 2 MiB manifest, the size a staged layer's `prev` reaches over
|
||
/// some 23 000 units with alternating producers, is read and written back byte for
|
||
/// byte. Both writers stop at their limits: an envelope is written at limits equal to
|
||
/// its manifest's and its payloads' lengths, and refused at one byte less of either
|
||
/// (at the real limits the check would serialise 128 MiB, which a debug build takes
|
||
/// many seconds over, or hold a 4 GiB payload).
|
||
#[test]
|
||
fn a_manifest_past_the_reader_limit_is_refused_and_never_written() {
|
||
assert_eq!(MANIFEST_MAX_LEN, 134_217_728);
|
||
let too_large = |e: String| assert!(e.starts_with("manifest_too_large:"), "{e}");
|
||
let truncated = |e: String| assert!(e.contains("truncated"), "{e}");
|
||
for magic in [MAGIC, MAGIC_V2] {
|
||
let read = |b: &[u8]| if magic == MAGIC { Wai::from_bytes(b).err() } else { WaiMulti::from_bytes(b).err() }
|
||
.expect("refused").to_string();
|
||
let at = [&magic[..], &(MANIFEST_MAX_LEN as u32).to_le_bytes()].concat();
|
||
truncated(read(&at));
|
||
truncated(manifest_bytes(&at).unwrap_err().to_string());
|
||
for declared in [MANIFEST_MAX_LEN as u32 + 1, u32::MAX] {
|
||
let b = [&magic[..], &declared.to_le_bytes()].concat();
|
||
too_large(read(&b));
|
||
too_large(manifest_bytes(&b).unwrap_err().to_string());
|
||
}
|
||
}
|
||
|
||
let len = 2 << 20;
|
||
let one = envelope_with_manifest_len(false, len);
|
||
let w = Wai::from_bytes(&one).expect("a 2 MiB manifest is read");
|
||
assert_eq!(w.to_bytes().unwrap(), one, "and written back byte for byte");
|
||
let multi = envelope_with_manifest_len(true, len);
|
||
let w2 = WaiMulti::from_bytes(&multi).expect("a 2 MiB WAI2 manifest is read");
|
||
assert_eq!(w2.to_bytes().unwrap(), multi, "and written back byte for byte");
|
||
|
||
// Each payload is 3 bytes.
|
||
let lim = |manifest, field| WriteLimits { manifest, field };
|
||
let past_field = |e: std::io::Error, what: &str| {
|
||
let e = e.to_string();
|
||
assert!(e.contains(&format!("{what} is 3 bytes, past the u32 field")), "{e}");
|
||
};
|
||
assert_eq!(w.to_bytes_within(lim(len, 3)).unwrap(), one, "written at its own lengths");
|
||
too_large(w.to_bytes_within(lim(len - 1, 3)).unwrap_err().to_string());
|
||
past_field(w.to_bytes_within(lim(len, 2)).unwrap_err(), "the payload");
|
||
assert_eq!(w2.to_bytes_within(lim(len, 3)).unwrap(), multi, "written at its own lengths");
|
||
too_large(w2.to_bytes_within(lim(len - 1, 3)).unwrap_err().to_string());
|
||
past_field(w2.to_bytes_within(lim(len, 2)).unwrap_err(), "rendition 0's payload");
|
||
// A second rendition of 3 bytes: each fits a field of 5, their total does not.
|
||
let mut w3 = WaiMulti::from_bytes(&multi).unwrap();
|
||
w3.manifest.renditions.push(w3.manifest.renditions[0].clone());
|
||
w3.renditions.push(b"def".to_vec());
|
||
assert!(w3.to_bytes_within(lim(MANIFEST_MAX_LEN, 6)).is_ok());
|
||
let e = w3.to_bytes_within(lim(MANIFEST_MAX_LEN, 5)).unwrap_err().to_string();
|
||
assert!(e.contains("payload sum overflows u32"), "{e}");
|
||
}
|
||
|
||
/// The manifest's nesting limit the reference readers apply (SPEC §2, *Reader
|
||
/// limits*): 127 nested arrays and objects are read, 128 are refused, in `WAI1` and
|
||
/// `WAI2` manifests alike.
|
||
#[test]
|
||
fn a_manifest_nested_past_127_is_refused() {
|
||
for v2 in [false, true] {
|
||
let base = envelope_with_manifest_len(v2, 300);
|
||
let ml = u32::from_le_bytes(base[4..8].try_into().unwrap()) as usize;
|
||
let text = std::str::from_utf8(&base[8..8 + ml]).unwrap();
|
||
let rest = &base[8 + ml..];
|
||
// The manifest object is level 1, so `pad` holding n nested arrays is n + 1.
|
||
let pad = text.find("\"pad\":").unwrap();
|
||
let nested = |levels: usize| {
|
||
let n = levels - 1;
|
||
let m = format!("{}\"pad\":{}{}}}", &text[..pad], "[".repeat(n), "]".repeat(n));
|
||
[&base[..4], &(m.len() as u32).to_le_bytes(), m.as_bytes(), rest].concat()
|
||
};
|
||
let read = |b: &[u8]| if v2 { WaiMulti::from_bytes(b).map(|_| ()) } else { Wai::from_bytes(b).map(|_| ()) };
|
||
assert!(read(&nested(127)).is_ok(), "v2={v2}: 127 levels are read");
|
||
assert!(read(&nested(128)).is_err(), "v2={v2}: 128 levels are refused");
|
||
}
|
||
}
|
||
|
||
/// A length a container writes into a u32 field must fit it: the writers refuse a
|
||
/// payload past 2^32 − 1 bytes instead of writing its length modulo 2^32.
|
||
#[test]
|
||
#[cfg(target_pointer_width = "64")]
|
||
fn a_length_past_a_u32_field_is_refused() {
|
||
assert_eq!(u32_field(u32::MAX as usize, u32::MAX, "x").unwrap(), u32::MAX);
|
||
let e = u32_field(u32::MAX as usize + 1, u32::MAX, "the payload").unwrap_err().to_string();
|
||
assert!(e.contains("the payload is 4294967296 bytes, past the u32 field"), "{e}");
|
||
}
|
||
}
|