//! A deterministic, corpus-seeded fuzz of the `WVG1` and `WOS1` parsers (§2.7): no //! input panics, every input accepted writes back to its own bytes, an accepted //! payload of small cost renders its first frame, and every accepted payload's text //! layer holds (§5.4). Seeds: every vector of the corpus and the files under //! `fuzz/seeds/`. //! //! `WAI_VECTOR_FUZZ_ITERS` sets the mutations per seed (default 40). use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; use wai_vector::json::{self, Value}; use wai_vector::text::TextLayer; use wai_vector::vectors::{all_vectors, corpus_store}; use wai_vector::{clock, wos1, wvg1}; struct Rng(u64); impl Rng { fn next(&mut self) -> u64 { self.0 ^= self.0 << 13; self.0 ^= self.0 >> 7; self.0 ^= self.0 << 17; self.0 } fn below(&mut self, n: usize) -> usize { (self.next() % n.max(1) as u64) as usize } } const INTERESTING: [u32; 14] = [0, 1, 2, 3, 15, 16, 255, 0xFFFF, 0x1_0000, 0x7FFF_FFFF, 0x8000_0000, 0xFFFF_FFFF, 1 << 20, (1 << 22) + 1]; fn mutate(seed: &[u8], r: &mut Rng) -> Vec { let mut b = seed.to_vec(); for _ in 0..1 + r.below(4) { let n = b.len(); match r.below(7) { 0 if n > 0 => { let i = r.below(n); b[i] ^= 1 << r.below(8); } 1 if n > 0 => { let i = r.below(n); b[i] = r.next() as u8; } 2 if n > 0 => b.truncate(r.below(n)), 3 => b.push(r.next() as u8), 4 if n > 0 => { let i = r.below(n); b.insert(i, r.next() as u8); } 5 if n >= 4 => { // A field-sized value at a 4-aligned offset, most often in a header. let i = if r.below(2) == 0 { 4 * r.below(20.min(n / 4)) } else { 4 * r.below(n / 4) }; let v = INTERESTING[r.below(INTERESTING.len())]; b[i..i + 4].copy_from_slice(&v.to_le_bytes()); } 6 if n >= 2 => { let i = r.below(n - 1); let v = INTERESTING[r.below(INTERESTING.len())] as u16; b[i..i + 2].copy_from_slice(&v.to_le_bytes()); } _ => {} } } b } /// The blocks drawn at tick `tau`, by the frame procedure's own rule (§3.10): an item /// draws when it is active, and an inactive group begin skips everything up to its /// matching end. This never forms a window. fn drawn_at(d: &wvg1::Doc, tau: u64) -> Vec { let active = |it: &wvg1::Item| u64::from(it.t_on) <= tau && tau < u64::from(it.t_off); let mut drawn = vec![false; d.texts.len()]; let mut i = 0; while i < d.items.len() { let it = &d.items[i]; match it.kind { wvg1::KIND_BEGIN if !active(it) => { let mut depth = 1; while depth > 0 { i += 1; match d.items[i].kind { wvg1::KIND_BEGIN => depth += 1, wvg1::KIND_END => depth -= 1, _ => {} } } } wvg1::KIND_RUN | wvg1::KIND_PATH if active(it) && it.text != 0xFFFF => drawn[it.text as usize] = true, _ => {} } i += 1; } drawn } /// An accepted payload's text layer (§5.4). Each active set is sorted, its intervals /// neither overlap nor touch, and all lie in the document's window. The transcript is /// JSON, holds every block that is not decorative, and gives back each block's tag and /// string. At the first and the last frame of every listed rate, the projection is /// JSON and lists exactly the blocks the frame procedure draws at that tick. fn text_layer_holds(d: &wvg1::Doc) { let layer = TextLayer::new(d); let (t0, end) = (u64::from(d.header.t0), u64::from(d.header.t0) + u64::from(d.header.dur)); for set in &layer.active { let mut last: Option = None; for &(on, off) in set { assert!(t0 <= u64::from(on) && on < off && u64::from(off) <= end, "[{on}, {off}) is not an interval of the document's window"); assert!(last.is_none_or(|l| l < on), "intervals ascend, and neither overlap nor touch"); last = Some(off); } } let exposed: Vec = (0..d.texts.len()).filter(|&t| d.texts[t].purpose != 13).collect(); let index = |n: &Value| n.get("index").and_then(Value::as_int).expect("a node has an index") as usize; let transcript = json::parse(&layer.transcript()).expect("the transcript is JSON"); let nodes = transcript.as_arr().expect("the transcript is an array"); assert_eq!(nodes.iter().map(index).collect::>(), exposed, "the transcript holds every block that is not decorative"); for n in nodes { let t = index(n); assert_eq!(n.get("text").and_then(Value::as_str).map(str::as_bytes), Some(d.string(t)), "a string is given back as carried"); assert_eq!(n.get("lang").and_then(Value::as_str).map(str::as_bytes), Some(d.lang(t))); assert_eq!(n.get("active").and_then(Value::as_arr).map(<[Value]>::len), Some(layer.active[t].len())); } for (r, rate) in d.rates.iter().enumerate() { let p = rate.ticks_per_frame(d.header.tb); let (first, last) = clock::frame_range(d.header.t0, d.header.dur, p); if first > last { continue; } for g in [first as u64, last as u64] { let projection = json::parse(&layer.projection(r, g).expect("a frame in range")).expect("a projection is JSON"); let drawn = drawn_at(d, g * p); let want: Vec = exposed.iter().copied().filter(|&t| drawn[t]).collect(); assert_eq!(projection.as_arr().expect("an array").iter().map(index).collect::>(), want, "frame {g} at {}/{}", rate.num, rate.den); } } } #[test] fn fuzz_smoke() { let iters: usize = std::env::var("WAI_VECTOR_FUZZ_ITERS").ok().and_then(|s| s.parse().ok()).unwrap_or(40); let mut seeds: Vec<(bool, Vec)> = all_vectors() .into_iter() .filter(|v| v.ext != "json" && v.bytes.len() <= 1 << 16) .map(|v| (v.ext == "wos1", v.bytes)) .collect(); let dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fuzz/seeds"); let mut names: Vec = std::fs::read_dir(&dir).expect("fuzz/seeds").flatten().map(|e| e.path()).collect(); names.sort(); assert!(!names.is_empty()); for p in names { seeds.push((p.extension().is_some_and(|x| x == "wos1"), std::fs::read(&p).unwrap())); } let mut store = corpus_store(); // The text layer of every seed that loads, before any is mutated. let mut layers = 0usize; for (is_wos1, seed) in &seeds { if !*is_wos1 && let Ok(l) = wvg1::load(seed, &mut store) { text_layer_holds(&l.doc); layers += 1; } } assert!(layers > 60, "{layers} seeds load"); let mut r = Rng(0x5EED_0FD7_0002); let (mut runs, mut accepted, mut rendered) = (0usize, 0usize, 0usize); for (is_wos1, seed) in &seeds { for _ in 0..iters { let input = mutate(seed, &mut r); let ok = catch_unwind(AssertUnwindSafe(|| { if *is_wos1 { wos1::parse(&input).ok().map(|w| w.write()) } else { wvg1::load(&input, &mut store).ok().map(|l| { // An accepted payload renders, as a sink with a budget would: the // first frame at the first size, when that size's cost is small. let d = &l.doc; let p = d.rates[0].ticks_per_frame(d.header.tb); if let Some(g) = wai_vector::clock::frames(d.header.t0, d.header.dur, p).next() && l.costs[0] <= 1 << 22 { let px = wai_vector::frame::render(&l, 0, 0, g).expect("a frame in range renders"); assert_eq!(px.len(), 4 * usize::from(d.sizes[0].w) * usize::from(d.sizes[0].h)); rendered += 1; } text_layer_holds(d); l.doc.write() }) } })); let out = ok.unwrap_or_else(|_| panic!("the parser, the renderer or the text layer panicked on input {}", json::hex(&input))); if let Some(written) = out { assert_eq!(written, input, "an accepted input writes back to its own bytes"); accepted += 1; } runs += 1; } } eprintln!("fuzz smoke: {runs} inputs from {} seeds, {accepted} accepted, {rendered} rendered; the text layer of {layers} seeds and of every accepted input", seeds.len()); assert!(runs >= seeds.len() * iters); }