wai-live-web exports the browser_sink functions (routeReport, evaluateConstraints, readBinding, makeBinding, frameTier, frameState) and, behind its feature int_video, decodeBound: a wai.video.int_motion envelope verified against its group and object receipts, decoded by the integer decoder and every frame bound; an object that does not verify is not decoded, and one decoded without receipts is unbound. joule-moq-web's verifier now returns a verified object's content hash and its group receipt's link hash, which a frame decoded from it names. Reason's list is a function, so nothing of the module reaches include/wai.h. Co-Authored-By: Claude Opus 5.5 <[email protected]>
158 lines
6.7 KiB
Rust
158 lines
6.7 KiB
Rust
//! The browser sink (extensions/browser-sink.md): the committed corpus
|
|
//! `live-conformance/browser/` regenerates byte for byte, every case in it gets the outcome
|
|
//! it states, every reason of §11 appears in it, §11 lists exactly the reference's codes, and
|
|
//! the chain's group receipts and objects, which the corpus generator signs by hand, verify
|
|
//! under `jwp_delivery` (jwp-receipts §3) with each binding naming the object's content hash
|
|
//! and its group receipt's link hash.
|
|
#![cfg(all(feature = "browser_sink", feature = "receipt_claims"))]
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::path::PathBuf;
|
|
|
|
use serde_json::Value;
|
|
use wai::browser_sink::{corpus, Reason};
|
|
use wai::jwp_delivery::{GroupReceipt, ObjectReceipt};
|
|
|
|
fn hex_of(b: &[u8]) -> String {
|
|
b.iter().map(|x| format!("{x:02x}")).collect()
|
|
}
|
|
|
|
fn unhex(s: &str) -> Vec<u8> {
|
|
(0..s.len() / 2).map(|i| u8::from_str_radix(&s[2 * i..2 * i + 2], 16).unwrap()).collect()
|
|
}
|
|
|
|
fn wai_dir() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
|
}
|
|
|
|
fn read(p: &str) -> Vec<u8> {
|
|
std::fs::read(wai_dir().join(p)).unwrap_or_else(|e| panic!("{p}: {e}"))
|
|
}
|
|
|
|
fn vectors() -> Value {
|
|
serde_json::from_slice(&read("live-conformance/browser/vectors.json")).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn every_committed_file_regenerates_and_no_other_is_there() {
|
|
let dir = wai_dir().join("live-conformance/browser");
|
|
let want: BTreeSet<String> = corpus::files().iter().map(|(n, _)| n.to_string()).chain(["README.md".to_string()]).collect();
|
|
let have: BTreeSet<String> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name().into_string().unwrap()).collect();
|
|
assert_eq!(have, want, "the corpus directory holds exactly the generated files and its README");
|
|
for (name, bytes) in corpus::files() {
|
|
assert!(read(&format!("live-conformance/browser/{name}")) == bytes, "{name} differs from `wai_browser_sink_vectors gen`");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn every_case_gets_its_stated_outcome() {
|
|
let doc = String::from_utf8(read("live-conformance/browser/vectors.json")).unwrap();
|
|
let fails = corpus::verify(&doc);
|
|
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
|
}
|
|
|
|
/// Every code of §11 is given somewhere in the corpus: as a report's, an entry's, a leg's, an
|
|
/// energy entry's, a binding's or a side table's refusal.
|
|
#[test]
|
|
fn the_corpus_gives_every_reason() {
|
|
let v = vectors();
|
|
let mut seen = BTreeSet::new();
|
|
let mut take = |x: &Value| {
|
|
if let Some(s) = x.as_str() {
|
|
seen.insert(s.to_string());
|
|
}
|
|
};
|
|
for c in v["routes"].as_array().unwrap() {
|
|
take(&c["reason"]);
|
|
for e in c["table"].as_array().into_iter().flatten() {
|
|
take(&e["reason"]);
|
|
take(&e["energy"]["reason"]);
|
|
for l in ["ua", "host", "wasm"] {
|
|
take(&e["legs"][l]);
|
|
}
|
|
}
|
|
}
|
|
for c in v["constraints"].as_array().unwrap() {
|
|
take(&c["reason"]);
|
|
}
|
|
for c in v["bindings"].as_array().unwrap() {
|
|
take(&c["reason"]);
|
|
}
|
|
for c in v["tables"].as_array().unwrap() {
|
|
for r in c["result"]["refusals"].as_array().unwrap() {
|
|
take(&r["reason"]);
|
|
}
|
|
}
|
|
// binding_unverified_reference is a page's refusal over a decode it ran (§7.2): the corpus
|
|
// states it for the chain's changed object, which a page refuses to bind.
|
|
for g in v["chain"]["groups"].as_array().unwrap() {
|
|
for o in g["objects"].as_array().unwrap() {
|
|
if o["outcome"] == "refused" {
|
|
seen.insert("binding_unverified_reference".into());
|
|
}
|
|
}
|
|
}
|
|
for r in Reason::all() {
|
|
assert!(seen.contains(r.code()), "nothing in the corpus gives {r}");
|
|
}
|
|
}
|
|
|
|
/// §11 of the extension lists exactly the codes `Reason::all()` has, each once, in order.
|
|
#[test]
|
|
fn the_extension_lists_the_reasons() {
|
|
let text = String::from_utf8(read("extensions/browser-sink.md")).unwrap();
|
|
let at = text.find("\n## 11. Reasons").expect("a §11");
|
|
let end = text[at + 1..].find("\n## ").map_or(text.len(), |e| at + 1 + e);
|
|
let listed: Vec<String> = text[at..end].lines().filter(|l| l.starts_with("| `")).filter_map(|l| l.split('`').nth(1).map(String::from)).collect();
|
|
let want: Vec<String> = Reason::all().iter().map(|r| r.code().to_string()).collect();
|
|
assert_eq!(listed, want, "§11's table and Reason::all() differ");
|
|
}
|
|
|
|
/// The chain the generator signs by hand verifies under jwp_delivery, and every binding names
|
|
/// its object's content hash and its group receipt's link hash; the changed object does not
|
|
/// verify.
|
|
#[test]
|
|
fn the_chain_verifies_under_jwp_delivery() {
|
|
let v = vectors();
|
|
let mut previous: Option<GroupReceipt> = None;
|
|
let mut objects = 0;
|
|
for g in v["chain"]["groups"].as_array().unwrap() {
|
|
let group = GroupReceipt::from_json(&g["receipt"]).expect("the group reads");
|
|
group.verify().expect("the group's signature verifies");
|
|
group.check_parent(previous.as_ref()).expect("the group links to the one before");
|
|
assert_eq!(hex_of(&group.link_hash()), g["link_hash"].as_str().unwrap());
|
|
assert_eq!(hex_of(&group.canonical_bytes()), g["canonical"].as_str().unwrap());
|
|
for o in g["objects"].as_array().unwrap() {
|
|
let receipt = ObjectReceipt::from_json(&o["receipt"]).expect("the object receipt reads");
|
|
let envelope = match &o["envelope"] {
|
|
Value::String(h) => unhex(h),
|
|
f => read(&format!("live-conformance/browser/{}", f["file"].as_str().unwrap())),
|
|
};
|
|
let accepted = receipt.accept(&group, &envelope);
|
|
if o["outcome"] == "verified" {
|
|
accepted.unwrap_or_else(|e| panic!("{}: {e:?}", o["name"]));
|
|
let b: Value = serde_json::from_str(o["binding"].as_str().unwrap()).unwrap();
|
|
assert_eq!(b["digest"], hex_of(&wai::jwp_hash::content_hash(&envelope)));
|
|
assert_eq!(b["receipt"], g["link_hash"]);
|
|
assert_eq!(b["tier"], "decode-equivalence");
|
|
objects += 1;
|
|
} else {
|
|
assert!(accepted.is_err(), "{}: a changed object verifies", o["name"]);
|
|
assert!(o["binding"].is_null());
|
|
}
|
|
}
|
|
previous = Some(group);
|
|
}
|
|
assert_eq!(objects, 3);
|
|
}
|
|
|
|
/// The envelope the browser decodes is the WIV1 payload whose frame digests `sha256.json`
|
|
/// publishes, wrapped once.
|
|
#[test]
|
|
fn the_int_motion_envelope_wraps_the_published_payload() {
|
|
let e = read("live-conformance/browser/int_motion.wai");
|
|
let wiv1 = read("wai-rs/tests/vectors/int_video/int_video_bitstream.wiv1");
|
|
assert!(e.ends_with(&wiv1));
|
|
let digests: Value = serde_json::from_slice(&read(corpus::WIV1_DIGESTS)).unwrap();
|
|
assert_eq!(digests["int_video"]["frames_sha256"].as_array().unwrap().len(), 5);
|
|
}
|