197 lines
5.0 KiB
Rust
197 lines
5.0 KiB
Rust
//! 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<char> = 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");
|
||
}
|
||
}
|