//! 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> { 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::>().join(" ") } #[test] fn the_committed_corpus_regenerates_from_its_seeds() { let files = committed(); let generated: BTreeMap> = 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 { 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::(), 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> = 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 { 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}"); }