//! Fuzz: 20 000 adversarial/mutated inputs; parse + layout must never panic, never //! loop forever, and only produce finite, non-negative sizes. Deterministic (xorshift). use notescraft_math::{Style, layout, parse, testutil::FakeMetrics}; /// Deterministic xorshift64*; seeds are fixed so failures reproduce. struct Rng(u64); impl Rng { fn next(&mut self) -> u64 { let mut x = self.0; x ^= x >> 12; x ^= x << 25; x ^= x >> 27; self.0 = x; x.wrapping_mul(0x2545_F491_4F6C_DD1D) } fn below(&mut self, hi: usize) -> usize { let r = self.next() >> 32; // top bits: no modulo bias for the small ranges used let t = usize::try_from(r).unwrap_or(usize::MAX); t % hi } } const TOKENS: &[&str] = &[ "x", "y", "1", "2", "+", "-", "=", "(", ")", "[", "]", "{", "}", "^", "_", "&", "\\", ",", ".", "!", "~", " ", "'", "é", "α", "𝔸", "\\frac", "\\sqrt", "\\sum", "\\lim", "\\max", "\\int", "\\iota", "_i", "^n", "\\left", "\\right", "\\begin{matrix}", "\\end{matrix}", "\\begin{aligned}", "\\end{aligned}", "\\begin{cases}", "\\end{cases}", "\\text{a b}", "\\alpha", "\\hat", "\\mathbb{R}", "\\mathrm{x}", "\\,", "\\quad", "\\frac{a}{b}", "\\sqrt[3]{x}", "\\sum_i", "\\lim_{x}", "\\cdot", "\\infty", "\\to", "{a}", "\\end", "\\begin", "\\right)", "&", "x^^\\", "\\frac{a}", "\\left(a", "%", "#", "\\\\", "\\end{x}", ]; const VALID: &[&str] = &[ "\\frac{a}{b}+\\sqrt[3]{x^2}", "\\begin{pmatrix}a&b\\\\c&d\\end{pmatrix}_{i\\,j}^{n}", "\\sum_{k=1}^{n}\\frac{k!}{2^k} = \\lim_{n\\to\\infty}\\left(\\sum...\\right)", "\\left|\\frac{\\alpha}{\\beta}\\right|^2 + \\hat x \\vec v", "\\begin{cases} x^2 & x \\geq 0 \\\\ -x & \\text{else} \\end{cases}", "f(x) = \\int_{-\\infty}^{\\infty} \\mathrm{e}^{-x^2} dx", ]; fn mutate(rng: &mut Rng, src: &str) -> String { let chars: Vec = src.chars().collect(); let pos = rng.below(chars.len().max(1)); let mut out = String::new(); let pick = rng.below(3); for (i, ch) in chars.iter().enumerate() { if i == pos && pick == 0 { // Insert a random token. let tok = TOKENS[rng.below(TOKENS.len())]; out.push_str(tok); } if i == pos && pick == 1 { continue; // Delete the character. } out.push(*ch); if i == pos && pick == 2 { out.push(*ch); // Duplicate it. } } out } fn random_token_soup(rng: &mut Rng, out: &mut String) { let n = 1 + rng.below(12); for _ in 0..n { let tok = TOKENS[rng.below(TOKENS.len())]; out.push_str(tok); if rng.below(4) == 0 { out.push(' '); } } } fn check(src: &str, tag: &str) { match parse(src) { Err(_) => { /* Malformed input is always an Err, never a panic. */ } Ok(node) => { for display in [false, true] { let b = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display }); assert!(b.width.is_finite(), "{tag}: width"); assert!(b.height.is_finite(), "{tag}: height"); assert!(b.depth.is_finite(), "{tag}: depth"); assert!(b.width >= -0.01, "{tag}: {}", b.width); assert!(b.height >= -0.01, "{tag}: {}", b.height); assert!(b.depth >= -0.01, "{tag}: {}", b.depth); for item in &b.items { check_item(item, tag); } } } } } fn check_item(item: ¬escraft_math::Item, tag: &str) { match item { notescraft_math::Item::Glyphs { x, baseline_y, size, text, .. } => { let (x, by, size) = (*x, *baseline_y, *size); assert!(x.is_finite() && by.is_finite() && size.is_finite(), "{tag}"); assert!(size > 0.0, "{tag}"); assert!(!text.is_empty(), "{tag}: empty run"); } notescraft_math::Item::Rule { w, h, .. } => { let (w, h) = (*w, *h); assert!(w >= 0.0 && h > 0.0 && w.is_finite() && h.is_finite(), "{tag}"); } notescraft_math::Item::Path { points, stroke } => { assert!(*stroke > 0.0 && stroke.is_finite(), "{tag}"); for [x, y] in points { assert!((*x).is_finite() && (*y).is_finite(), "{tag}"); } } } } #[test] fn fuzz_parse_and_layout_never_panics() { let mut rng = Rng(0x1234_5678_9ABC_DEF0); for n in 0..10_000u64 { let mut soup = String::new(); random_token_soup(&mut rng, &mut soup); check(&soup, "soup"); if n % 1000 == 0 { check(&soup.repeat(400), "repeated soup"); } } for _n in 0..10_000u64 { let tmpl = VALID[rng.below(VALID.len())]; check(&mutate(&mut rng, tmpl), "mutate"); } }