The test checked §7.9.7's claims against the generated sets' tallies alone, so a claim the photographs' column refutes passed it (screen-b, falsified); it now reads the row's three columns, as the second implementation does. Co-Authored-By: Claude Opus 5.5 <[email protected]>
220 lines
12 KiB
Rust
220 lines
12 KiB
Rust
//! The registered watermark scheme of extensions/synthesis.md §7.5 and its corpus
|
||
//! `live-conformance/synthesis-mark/`: every file regenerates byte for byte from its fixed seeds
|
||
//! (the measurement among them) and no other file is there; every embedding, detection and case
|
||
//! gets what it states; the extension's definition, generator polynomial and shuffle are the
|
||
//! reference's; and §7.5.8 claims only attacks the measurement shows survived, with §7.5.6's
|
||
//! figures the measurement's. Run it in release: the measurement embeds and attacks every
|
||
//! picture of the measurement sets.
|
||
#![cfg(feature = "synthesis_mark")]
|
||
|
||
use serde_json::Value;
|
||
use std::collections::BTreeMap;
|
||
use std::path::PathBuf;
|
||
use wai::synthesis::mark;
|
||
use wai::synthesis_mark::corpus;
|
||
|
||
fn wai_dir() -> PathBuf {
|
||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..")
|
||
}
|
||
|
||
fn committed() -> BTreeMap<String, Vec<u8>> {
|
||
let root = wai_dir().join("live-conformance/synthesis-mark");
|
||
let mut out = BTreeMap::new();
|
||
let mut stack = vec![root.clone()];
|
||
while let Some(d) = stack.pop() {
|
||
for e in std::fs::read_dir(&d).expect("the committed corpus").flatten() {
|
||
let p = e.path();
|
||
if p.is_dir() {
|
||
stack.push(p);
|
||
continue;
|
||
}
|
||
let rel = p.strip_prefix(&root).expect("under the corpus").to_string_lossy().replace('\\', "/");
|
||
if rel != "README.md" {
|
||
out.insert(rel, std::fs::read(&p).expect("a file"));
|
||
}
|
||
}
|
||
}
|
||
out
|
||
}
|
||
|
||
fn extension() -> String {
|
||
std::fs::read_to_string(wai_dir().join("extensions/synthesis.md")).expect("the extension")
|
||
}
|
||
|
||
/// The text of the section that starts at `head`, up to the next heading of its level or above.
|
||
fn section(text: &str, head: &str) -> String {
|
||
let at = text.find(&format!("\n{head}")).unwrap_or_else(|| panic!("a section {head}"));
|
||
let level = head.chars().take_while(|c| *c == '#').count();
|
||
let end = text[at + 1..]
|
||
.match_indices("\n#")
|
||
.find(|(i, _)| text[at + 1 + i + 1..].chars().take_while(|c| *c == '#').count() <= level)
|
||
.map_or(text.len(), |(i, _)| at + 1 + i);
|
||
text[at..end].to_string()
|
||
}
|
||
|
||
fn fold(s: &str) -> String {
|
||
s.split_whitespace().collect::<Vec<_>>().join(" ")
|
||
}
|
||
|
||
#[test]
|
||
fn the_committed_corpus_regenerates_from_its_seeds() {
|
||
let files = committed();
|
||
let generated: BTreeMap<String, Vec<u8>> = corpus::generate().into_iter().collect();
|
||
let extra: Vec<&String> = files.keys().filter(|k| !generated.contains_key(*k)).collect();
|
||
let differ: Vec<&String> = generated.keys().filter(|k| files.get(*k) != generated.get(*k)).collect();
|
||
assert!(extra.is_empty() && differ.is_empty(), "extra {extra:?}, differ or missing {differ:?}: run `wai_synthesis_mark_vectors gen`");
|
||
}
|
||
|
||
#[test]
|
||
fn every_vector_and_case_gets_what_it_states() {
|
||
let fails = corpus::verify(&committed());
|
||
assert!(fails.is_empty(), "{}", fails.join("\n"));
|
||
}
|
||
|
||
/// §7.4's definition is the reference's, with its digest; §7.5.3's generator polynomial and
|
||
/// the shuffle's first entries are the reference's.
|
||
#[test]
|
||
fn the_extension_pins_the_scheme() {
|
||
let text = extension();
|
||
let s74 = section(&text, "### 7.4 ");
|
||
let block = s74.split("```").nth(1).expect("§7.4's definition");
|
||
let def: String = block.lines().map(str::trim).collect();
|
||
assert_eq!(def, mark::definition_text(), "§7.4's definition");
|
||
let digest: String = mark::digest().iter().map(|b| format!("{b:02x}")).collect();
|
||
assert!(fold(&s74).contains(&format!("its digest is `{digest}`")), "§7.4's digest");
|
||
let s753 = fold(§ion(&text, "#### 7.5.3 "));
|
||
assert!(s753.contains(&format!("in hexadecimal, are `{}`", mark::GENERATOR_HEX)), "§7.5.3's generator");
|
||
let p = &mark::layout().perm;
|
||
assert!(s753.contains(&format!("π begins {}, {}, {}, {},", p[0], p[1], p[2], p[3])), "§7.5.3's shuffle");
|
||
}
|
||
|
||
/// §7.5.8 claims only attacks every set survived with no unmarked reading; §7.5.6's figures are
|
||
/// battery.json's.
|
||
#[test]
|
||
fn the_extension_claims_only_what_the_measurement_shows() {
|
||
let text = extension();
|
||
let battery: Value = serde_json::from_slice(&committed()["battery.json"]).expect("battery.json");
|
||
let s758 = fold(§ion(&text, "#### 7.5.8 "));
|
||
let at = s758.find("For pictures, these attacks, each with the parameter measured: ").expect("§7.5.8's claims");
|
||
let rest = &s758[at..];
|
||
let clips_at = rest.find(" For clips: ").expect("the clips' claims");
|
||
let names = |s: &str| -> Vec<String> { s.split('`').skip(1).step_by(2).map(String::from).collect() };
|
||
let pictures = names(&rest[..clips_at]);
|
||
let clips = names(&rest[clips_at..rest[clips_at..].find(". ").map_or(rest.len(), |e| clips_at + e)]);
|
||
let has = |kind: &str, a: &String| battery["claims"][kind].as_array().expect("claims").iter().any(|x| x == a);
|
||
assert!(!pictures.is_empty() && pictures.iter().all(|a| has("pictures", a)), "§7.5.8 claims {pictures:?}");
|
||
assert!(!clips.is_empty() && clips.iter().all(|a| has("clips", a)), "§7.5.8 claims {clips:?}");
|
||
let w = &battery["words"];
|
||
let s756 = fold(§ion(&text, "#### 7.5.6 "));
|
||
let fp = battery["false_positive_log2"].as_str().expect("a bound").trim_start_matches('-').to_string();
|
||
assert!(s756.contains(&format!("2^−{fp}, about")), "§7.5.6's bound");
|
||
let pairs = w["pairs"].as_u64().expect("pairs");
|
||
let words = format!(
|
||
"have a mean of {} ones with a standard deviation of {}, and the {},{:03} pairs of those words from different pictures differ in a mean of {} bits with a standard deviation of {} and in no fewer than {};",
|
||
w["ones_mean"].as_str().unwrap(),
|
||
w["ones_sd"].as_str().unwrap(),
|
||
pairs / 1000,
|
||
pairs % 1000,
|
||
w["distance_mean"].as_str().unwrap(),
|
||
w["distance_sd"].as_str().unwrap(),
|
||
w["distance_min"]
|
||
);
|
||
assert!(s756.contains(&words), "§7.5.6's figures: {words}");
|
||
}
|
||
|
||
#[test]
|
||
fn the_corpus_holds_only_what_its_vectors_name() {
|
||
let files = committed();
|
||
let doc: Value = serde_json::from_slice(&files["vectors.json"]).expect("vectors.json");
|
||
let text = String::from_utf8_lossy(&files["vectors.json"]).to_string() + &String::from_utf8_lossy(&files["vectors-tent64s.json"]);
|
||
for k in files.keys().filter(|k| k.starts_with("frames/")) {
|
||
let h = &k["frames/".len()..k.len() - ".rgb".len()];
|
||
assert!(text.contains(h), "{k} is named by no vector");
|
||
assert_eq!(&mark::sha256(&files[k]).iter().map(|b| format!("{b:02x}")).collect::<String>(), h, "{k} is not named by its SHA-256");
|
||
}
|
||
assert!(doc["cases"].as_array().is_some_and(|c| !c.is_empty()));
|
||
}
|
||
|
||
/// §7.9.1's definition is the reference's, with its digest; §7.9.2's shuffle begins as the
|
||
/// reference's; §7.9.5's bound is the corpus's.
|
||
#[test]
|
||
fn the_extension_pins_the_tiled_scheme() {
|
||
use wai::synthesis::mark::sync;
|
||
let text = extension();
|
||
let s = section(&text, "#### 7.9.1 ");
|
||
let block = s.split("```").nth(1).expect("§7.9.1's definition");
|
||
let def: String = block.lines().map(str::trim).collect();
|
||
assert_eq!(def, sync::definition_text(), "§7.9.1's definition");
|
||
let digest: String = sync::digest().iter().map(|b| format!("{b:02x}")).collect();
|
||
assert!(fold(&s).contains(&format!("its digest is `{digest}`")), "§7.9.1's digest");
|
||
let p = &sync::tile_layout().perm;
|
||
assert!(fold(§ion(&text, "#### 7.9.2 ")).contains(&format!("π begins {}, {}, {}, {},", p[0], p[1], p[2], p[3])), "§7.9.2's shuffle");
|
||
let files = committed();
|
||
let doc: Value = serde_json::from_slice(&files["vectors-tent64s.json"]).expect("vectors-tent64s.json");
|
||
let fp = doc["scheme"]["false_positive_log2"].as_str().expect("a bound").trim_start_matches('-').to_string();
|
||
assert!(fold(§ion(&text, "#### 7.9.5 ")).contains(&format!("2^−{fp}, about")), "§7.9.5's bound");
|
||
}
|
||
|
||
/// §7.9.7 claims only attacks every generated set survived with no unmarked reading, and its
|
||
/// table's generated columns are battery-tent64s.json's; §7.9.5's figures are its words'.
|
||
#[test]
|
||
fn the_extension_claims_only_what_the_tiled_measurement_shows() {
|
||
let text = extension();
|
||
let battery: Value = serde_json::from_slice(&committed()["battery-tent64s.json"]).expect("battery-tent64s.json");
|
||
let s = section(&text, "#### 7.9.7 ");
|
||
let mut rows: BTreeMap<(String, String), String> = BTreeMap::new();
|
||
for r in battery["table"].as_array().expect("a table") {
|
||
let (d, t, e) = (r["detected"].as_u64().unwrap(), r["trials"].as_u64().unwrap(), r["errors_max"].as_u64().unwrap());
|
||
let cell = if d > 0 { format!("{d} / {t} ({e})") } else { format!("{d} / {t}") };
|
||
rows.insert((r["attack"].as_str().unwrap().to_string(), r["set"].as_str().unwrap().to_string()), cell);
|
||
}
|
||
let mut seen = 0;
|
||
// The picture rows' three columns, for the claims below.
|
||
let mut columns: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||
for line in s.lines() {
|
||
let cells: Vec<&str> = line.trim().trim_matches('|').split('|').map(str::trim).collect();
|
||
if !cells[0].starts_with('`') || cells.len() < 2 {
|
||
continue;
|
||
}
|
||
let a = cells[0].trim_matches('`').to_string();
|
||
if cells.len() == 4 {
|
||
columns.insert(a.clone(), cells[1..].iter().map(|c| c.to_string()).collect());
|
||
assert_eq!(cells[1], rows[&(a.clone(), "spectral".into())], "§7.9.7 {a} spectral");
|
||
assert_eq!(cells[2], rows[&(a.clone(), "leaves".into())], "§7.9.7 {a} leaves");
|
||
seen += 1;
|
||
} else if cells.len() == 2 {
|
||
assert_eq!(cells[1], rows[&(a.clone(), "clips".into())], "§7.9.7 {a} clips");
|
||
seen += 1;
|
||
}
|
||
}
|
||
assert_eq!(seen, battery["image_attacks"].as_array().unwrap().len() + battery["clip_attacks"].as_array().unwrap().len(), "§7.9.7's rows");
|
||
let f = fold(&s);
|
||
let at = f.find("For pictures: ").expect("§7.9.7's claims");
|
||
let rest = &f[at..];
|
||
let clips_at = rest.find(" For clips: ").expect("the clips' claims");
|
||
let names = |x: &str| -> Vec<String> { x.split('`').skip(1).step_by(2).map(String::from).collect() };
|
||
let pictures = names(&rest[..clips_at]);
|
||
let clips = names(&rest[clips_at..rest[clips_at..].find(". ").map_or(rest.len(), |e| clips_at + e)]);
|
||
let has = |kind: &str, a: &String| battery["claims"][kind].as_array().expect("claims").iter().any(|x| x == a);
|
||
assert!(!pictures.is_empty() && pictures.iter().all(|a| has("pictures", a)), "§7.9.7 claims {pictures:?}");
|
||
assert!(!clips.is_empty() && clips.iter().all(|a| has("clips", a)), "§7.9.7 claims {clips:?}");
|
||
// A claim is an attack every column survived, the photographs' among them: "n / n (e)".
|
||
let whole = |c: &String| c.split_once(" / ").is_some_and(|(d, rest)| rest.split_once(" (").is_some_and(|(t, _)| d == t));
|
||
for a in &pictures {
|
||
assert!(columns.get(a).is_some_and(|cs| cs.len() == 3 && cs.iter().all(whole)), "§7.9.7 claims {a}, which a column of its table does not show survived");
|
||
}
|
||
let w = &battery["words"];
|
||
let pairs = w["pairs"].as_u64().expect("pairs");
|
||
let words = format!(
|
||
"{} words have a mean of {} ones with a standard deviation of {}, and the {},{:03} pairs of those words from different pictures differ in a mean of {} bits with a standard deviation of {} and in no fewer than {};",
|
||
w["words"],
|
||
w["ones_mean"].as_str().unwrap(),
|
||
w["ones_sd"].as_str().unwrap(),
|
||
pairs / 1000,
|
||
pairs % 1000,
|
||
w["distance_mean"].as_str().unwrap(),
|
||
w["distance_sd"].as_str().unwrap(),
|
||
w["distance_min"]
|
||
);
|
||
assert!(fold(§ion(&text, "#### 7.9.5 ")).contains(&words), "§7.9.5's figures: {words}");
|
||
}
|