open-standards/wai/wai-rs/tests/format_probe.rs
Transaction Science f5a242a9a2
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 23s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 21s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 22s
wai-app and Create / wai-app · does this change touch it (push) Successful in 31s
WAI operation catalogue / does this change touch it (push) Successful in 20s
wai-audit collector and player integration / does this change touch it (push) Successful in 30s
wai-sink-swift native sink package / does this change touch it (push) Successful in 18s
wai-quantum studio and desktop / does this change touch it (push) Successful in 14s
wai-ledger service / does this change touch it (push) Successful in 45s
wai-fleet service / does this change touch it (push) Successful in 1m1s
wai-sink-android native sink library / does this change touch it (push) Successful in 19s
wai-quantum for JavaScript / does this change touch it (push) Successful in 51s
WAI Studio / does this change touch it (push) Successful in 52s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m14s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m12s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 5m30s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m13s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 27s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 41s
wai-audit collector and player integration / collector · linux (push) Successful in 2m30s
wai-ledger service / service · linux (push) Successful in 1m53s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m5s
wai-quantum studio and desktop / studio · browser (push) Successful in 4m38s
wai-quantum for JavaScript / javascript · linux (push) Successful in 3m51s
wai-fleet service / service · linux (push) Successful in 4m2s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 8m54s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 11m31s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m53s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 9m58s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m1s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m17s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m46s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 5m29s
wai-ledger service / service · macos (push) Successful in 1m5s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 17m5s
wai-fleet service / service · macos (push) Successful in 44s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 8m34s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 18m3s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 12m15s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m46s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m10s
wai: header probes for AV2, AC-4, IAMF and IVAS, their codec strings, and why EVC has none
format-probes §14 to §17 add probes of basis `standard`, each read from its published text:

- AV2 (`wai.video.av2`, AV2 v1.0.0): the length-delimited bitstream of Annex B, the OBU
  headers, the temporal delimiter a payload begins with, and the first sequence header OBU
  through its cropping window: profile, level, tier, chroma format, bit depth, maximum and
  cropped size, layer counts, with Annex A's reserved profiles and levels and each profile's
  chroma formats refused. No codec string: AOMedia's ISO BMFF binding is a working draft.
- AC-4 (`wai.audio.ac4`, ETSI TS 103 190-1 V1.4.1 and -2 V1.3.1): sync frames with their CRC
  checked and the first table of contents to the first presentation's md_compat; the codec
  string of TS 103 190-2 E.13.
- IAMF (`wai.audio.iamf`, IAMF v2.0.0): a standalone IA Sequence, its profiles, the order of
  its Descriptors, the Opus, FLAC and LPCM configurations as §3.13 constrains them, the start of
  each audio element; the codec string of §6.4.
- IVAS (`wai.audio.ivas`, 3GPP TS 26.253 V19.3.0 Annex A): one IVAS payload's E and ToC bytes,
  frame sizes, processing information, padding and the first IVAS frame's format; `sivs`
  (TS 26.244 A.2.2).

§18 says why EVC has no probe: its framing, NAL unit types and SPS are in ISO/IEC 23094-1,
which is not openly readable, and RFC 9584 gives only the NAL unit header. §2.6 and
platform-decode §2.5: the probes' `codec`, which browser-sink §4.2's gate refuses, so no user
agent is asked; a native sink reads the string from the payload. SPEC §5 pins the payload forms
of AC-4, IAMF and IVAS to what their probes read. Nothing is added to the floor or the
recommended set.

format-probe-conformance: hand-built vectors with known field values under gen/av2, gen/ac4,
gen/iamf and gen/ivas, the texts' codec-string examples among them, and negative controls;
format_probe_verify.py reads them with its own readers and requires the new planted faults
(stated profiles, levels, versions, bit rates and codec strings, header bytes under restated
digests) to fail, as tests/format_probe.rs requires of corpus::verify.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 15:29:02 -05:00

136 lines
6.5 KiB
Rust

//! Header probes (extensions/format-probes.md): the committed corpus `format-probe-conformance/`
//! regenerates byte for byte from its external vectors and holds no other generated file,
//! every vector, binding and fact gets its stated outcome, the extension's §2.4 and §12 tables
//! are the reference's probes and codes, and projection.md §4 lists only codes of §12.
#![cfg(feature = "format_probe")]
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use wai::format_probe::{corpus, Refusal, PROBES};
fn wai_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
}
fn walk(root: &Path, dir: &Path, out: &mut BTreeMap<String, Vec<u8>>) {
for e in std::fs::read_dir(dir).expect("a corpus directory").flatten() {
let p = e.path();
if p.is_dir() {
walk(root, &p, out);
} else {
let rel = p.strip_prefix(root).unwrap().to_string_lossy().replace('\\', "/");
out.insert(rel, std::fs::read(&p).unwrap());
}
}
}
fn committed() -> BTreeMap<String, Vec<u8>> {
let root = wai_dir().join("format-probe-conformance");
let mut out = BTreeMap::new();
for sub in ["ext", "gen"] {
walk(&root, &root.join(sub), &mut out);
}
out.insert("vectors.json".into(), std::fs::read(root.join("vectors.json")).expect("vectors.json"));
out
}
#[test]
fn the_corpus_regenerates_and_every_outcome_holds() {
let files = committed();
assert!(files.keys().filter(|k| k.starts_with("ext/")).count() >= 9, "the external vectors");
let fails = corpus::verify(&files);
assert!(fails.is_empty(), "{}: run `wai_format_probe gen`", fails.join("\n"));
}
fn section(text: &str, heading: &str) -> String {
let at = text.find(&format!("\n{heading}")).unwrap_or_else(|| panic!("{heading}"));
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
text[at..end].to_string()
}
/// §2.4 lists exactly the probes and bases of `PROBES`, in order; §12 exactly the codes of
/// `Refusal::ALL`, in order.
#[test]
fn the_extension_lists_the_probes_and_the_codes() {
let text = std::fs::read_to_string(wai_dir().join("extensions/format-probes.md")).expect("the extension");
let rows: Vec<(String, String)> = section(&text, "## 2. Common rules")
.lines()
.filter(|l| l.starts_with("| `wai."))
.map(|l| {
let cells: Vec<&str> = l.split('|').map(str::trim).collect();
(cells[1].trim_matches('`').to_string(), cells[2].to_string())
})
.collect();
let want: Vec<(String, String)> = PROBES.iter().map(|(c, b)| (c.to_string(), b.label().to_string())).collect();
assert_eq!(rows, want, "§2.4's table and PROBES differ");
let codes: Vec<String> = section(&text, "## 12. Refusals")
.lines()
.filter(|l| l.starts_with("| `"))
.filter_map(|l| l.split('`').nth(1).map(String::from))
.collect();
let want: Vec<String> = Refusal::ALL.iter().map(|r| r.code().to_string()).collect();
assert_eq!(codes, want, "§12's table and Refusal::ALL differ");
// projection.md states the codes its reader and its binding check give.
let proj = std::fs::read_to_string(wai_dir().join("extensions/projection.md")).expect("projection.md");
let checks = section(&proj, "## 4. Checks");
for r in [Refusal::NoSignature, Refusal::LengthMismatch, Refusal::ReservedValue, Refusal::DescriptorInvalid] {
assert!(checks.contains(&format!("`{}`", r.code())), "projection.md §4 does not state {}", r.code());
}
assert!(section(&proj, "## 5. Binding to the rendition").contains("`layer_absent`"));
}
fn sha256_hex(b: &[u8]) -> String {
use sha2::{Digest, Sha256};
Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect()
}
/// Planted faults in the AV2, AC-4, IAMF and IVAS vectors (format-probes §14 to §17): a
/// stated profile, level, version, bit rate or codec string changed, or a header byte changed
/// with the file's digest restated so that only the outcome can catch it. `corpus::verify`
/// must report each.
#[test]
fn planted_faults_in_the_av2_ac4_iamf_and_ivas_vectors_are_caught() {
use serde_json::{json, Value};
let files = committed();
let doc: Value = serde_json::from_slice(&files["vectors.json"]).unwrap();
let find = |doc: &Value, name: &str| doc["vectors"].as_array().unwrap().iter().position(|v| v["name"] == name).unwrap_or_else(|| panic!("{name}"));
let stated = [
("main420_10_1080p", "/outcome/accept/profile", json!(1)),
("main420_10_1080p", "/outcome/accept/level", json!("4.1")),
("main420_10_1080p", "/outcome/accept/codec", json!("av02.0.08M.10")),
("v2_crc_24fps", "/outcome/accept/codec", json!("ac-4.02.01.04")),
("v2_crc_24fps", "/outcome/accept/presentation/version", json!(2)),
("opus_simple", "/outcome/accept/codec", json!("iamf.000.000.opus")),
("opus_simple", "/outcome/accept/primary_profile", json!(1)),
("stereo_32k_two_frames_cmr", "/outcome/accept/codec", json!("sevs")),
("stereo_32k_two_frames_cmr", "/outcome/accept/frames/0/bitrate", json!(24400)),
];
let mut caught = 0;
for (name, ptr, value) in stated {
let mut d = doc.clone();
let i = find(&d, name);
*d["vectors"][i].pointer_mut(ptr).unwrap_or_else(|| panic!("{name}{ptr}")) = value;
let mut f = files.clone();
f.insert("vectors.json".into(), serde_json::to_vec_pretty(&d).unwrap());
assert!(!corpus::verify(&f).is_empty(), "{name}{ptr} restated, and nothing fails");
caught += 1;
}
// Header bytes: the AV2 profile's last bit, the AC-4 frame rate index, the IAMF primary
// profile, the IVAS ToC byte's bit rate.
let bytes = [("main420_10_1080p", 4, 0x04u8, false), ("v0_single_44k", 5, 0x01, false), ("opus_simple", 6, 0x01, true), ("stereo_32k_two_frames_cmr", 1, 0x07, false)];
for (name, at, x, set) in bytes {
let mut d = doc.clone();
let i = find(&d, name);
let file = d["vectors"][i]["file"].as_str().unwrap().to_string();
let mut f = files.clone();
let mut b = f[&file].clone();
b[at] = if set { x } else { b[at] ^ x };
d["vectors"][i]["sha256"] = json!(sha256_hex(&b));
f.insert(file, b);
f.insert("vectors.json".into(), serde_json::to_vec_pretty(&d).unwrap());
let fails = corpus::verify(&f);
assert!(fails.iter().any(|m| m.starts_with(&format!("{name}: stated"))), "{name} byte {at}: {fails:?}");
caught += 1;
}
assert_eq!(caught, 13);
}