#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::panic::{catch_unwind, AssertUnwindSafe};
use seam_core::{input::Input, json::Document, parse, validate::validate, Limits};
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, n: usize) -> usize {
if n == 0 {
0
} else {
(self.next() % n as u64) as usize
}
}
fn pick<'a, T>(&mut self, xs: &'a [T]) -> &'a T {
&xs[self.below(xs.len())]
}
}
const FRAGMENTS: &[&str] = &[
"{",
"}",
"[",
"]",
":",
",",
"\"",
"\\",
"\\u",
"\\ud800",
"\\udc00",
"\\uD83D\\uDE00",
"-",
"+",
".",
"e",
"E",
"0",
"00",
"1e999",
"-0",
"9007199254740993",
"18446744073709551616",
"true",
"false",
"null",
"NaN",
"Infinity",
" ",
"\t",
"\n",
"\r",
"\u{0}",
"\u{7f}",
"\u{85}",
"é",
"日本語",
"🙂",
"\u{feff}",
"schema",
"union",
"optional",
"enum",
"@tag",
"@format",
"@min_len",
"@range",
"u64",
"String",
"Date",
"DateTime",
"?",
"(",
")",
"..=",
"//",
];
fn seeds() -> Vec<String> {
let mut out = vec![
String::new(),
"{}".into(),
"[]".into(),
"null".into(),
"\"\"".into(),
"{\"a\":1}".into(),
"[[[[[[[[[[]]]]]]]]]]".into(),
"{\"a\":{\"b\":{\"c\":[1,2,3]}}}".into(),
"\"\\ud800\"".into(),
"1e309".into(),
"-9223372036854775809".into(),
"schema A { x: u8 }".into(),
"schema A { x: String @min_len(1) @format(email) }".into(),
"schema A { x: optional [String?]? }".into(),
"union U @tag(\"t\") { a: A }\nschema A { x: u8 }".into(),
"schema A { x: enum { a, b } }".into(),
];
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../conformance/schemas");
if let Ok(entries) = std::fs::read_dir(dir) {
for entry in entries.flatten() {
if let Ok(text) = std::fs::read_to_string(entry.path()) {
out.push(text);
}
}
}
out
}
fn mutate(rng: &mut Rng, input: &str) -> String {
let mut bytes = input.as_bytes().to_vec();
for _ in 0..1 + rng.below(4) {
match rng.below(8) {
0 => {
let at = rng.below(bytes.len() + 1);
let frag = rng.pick(FRAGMENTS).as_bytes();
bytes.splice(at..at, frag.iter().copied());
}
1 => {
let keep = rng.below(bytes.len() + 1);
bytes.truncate(keep);
}
2 => {
if !bytes.is_empty() {
let at = rng.below(bytes.len());
bytes[at] ^= 1 << rng.below(8);
}
}
3 => {
if !bytes.is_empty() {
let at = rng.below(bytes.len());
let end = (at + 1 + rng.below(8)).min(bytes.len());
bytes.drain(at..end);
}
}
4 => {
if !bytes.is_empty() && bytes.len() < 4096 {
let at = rng.below(bytes.len());
let end = (at + 1 + rng.below(32)).min(bytes.len());
let run: Vec<u8> = bytes[at..end].to_vec();
bytes.splice(at..at, run);
}
}
5 => {
let at = rng.below(bytes.len() + 1);
bytes.insert(at, rng.below(256) as u8);
}
6 => {
let n = 1 + rng.below(400);
let open = rng.below(2) == 0;
let (a, b) = if open { (b'[', b']') } else { (b'{', b'}') };
let mut deep = vec![a; n];
deep.extend(std::iter::repeat_n(b, n));
bytes = deep;
}
_ => {
let n = 1 + rng.below(2048);
let b = *rng.pick(&[b'a', b'0', b'\\', b'"', b' ', 0xff]);
let at = rng.below(bytes.len() + 1);
bytes.splice(at..at, std::iter::repeat_n(b, n));
}
}
}
String::from_utf8_lossy(&bytes).into_owned()
}
fn exercise(input: &str) -> Option<&'static str> {
let bytes = input.as_bytes();
let schema = match catch_unwind(AssertUnwindSafe(|| parse(input))) {
Err(_) => return Some("parse"),
Ok(result) => result.ok(),
};
for limits in [
Limits::DEFAULT,
Limits {
max_depth: 2,
max_items: 2,
max_string_bytes: 4,
max_object_keys: 2,
},
Limits { max_depth: usize::MAX, ..Limits::DEFAULT },
] {
let parsed = catch_unwind(AssertUnwindSafe(|| {
Document::parse(bytes, limits).map(|doc| {
fn walk(r: &seam_core::json::Ref<'_, '_>, depth: usize) {
if depth > 300 {
return;
}
let _ = r.kind();
let _ = r.as_bool();
let _ = r.as_int();
let _ = r.as_f64();
let _ = r.as_str();
let _ = r.len();
for each in r.elements() {
walk(&each, depth + 1);
}
for (_, each) in r.entries() {
walk(&each, depth + 1);
}
}
walk(&doc.root(), 0);
})
}));
if parsed.is_err() {
return Some("json");
}
}
if let Some(schema) = schema {
let names: Vec<String> = schema
.types
.keys()
.chain(schema.unions.keys())
.cloned()
.collect();
let validated = catch_unwind(AssertUnwindSafe(|| {
if let Ok(doc) = Document::parse(bytes, Limits::DEFAULT) {
let root = doc.root();
for name in &names {
let _ = validate(&schema, name, &root, Limits::DEFAULT);
}
let _ = validate(&schema, "\u{0}nope", &root, Limits::DEFAULT);
}
}));
if validated.is_err() {
return Some("validate");
}
}
let formatted = catch_unwind(AssertUnwindSafe(|| {
for f in seam_core::Format::ALL {
let _ = f.matches(input);
}
}));
if formatted.is_err() {
return Some("format");
}
None
}
fn iterations() -> usize {
std::env::var("SEAM_FUZZ_ITERATIONS")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(20_000)
}
#[test]
fn no_input_makes_the_engine_panic() {
let seeds = seeds();
let total = iterations();
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let mut failures: Vec<(u64, &'static str, String)> = Vec::new();
for i in 0..total {
let seed = 0x5EA3_0000_0000_0001 ^ i as u64;
let mut rng = Rng(seed);
let seed_input = rng.pick(&seeds).clone();
let input = mutate(&mut rng, &seed_input);
if let Some(where_) = exercise(&input) {
failures.push((seed, where_, input));
if failures.len() >= 5 {
break;
}
}
}
std::panic::set_hook(hook);
assert!(
failures.is_empty(),
"the engine panicked on {} input(s):\n{}",
failures.len(),
failures
.iter()
.map(|(seed, where_, input)| format!(
" seed {seed:#x} panicked in `{where_}` on {} bytes:\n {:?}",
input.len(),
&input[..input.len().min(200)]
))
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn known_bad_inputs_stay_fixed() {
let cases: &[&str] = &[
"",
"[",
"{",
"\"",
"\\",
"\"\\u",
"\"\\ud800\"",
"\"\\udc00\"",
"-",
"1e",
"1e+",
"0123",
"{\"a\"",
"{\"a\":",
"{\"a\":}",
"[,]",
"[1,]",
"schema",
"schema A",
"schema A {",
"schema A { x",
"schema A { x:",
"schema A { x: }",
"union",
"union U",
"union U @tag",
"union U @tag(",
"union U @tag()",
"schema A { x: String @format( }",
"schema A { x: String @min_len( }",
"schema A { x: String @range(1..=) }",
"é",
"\u{feff}schema A { x: u8 }",
];
for case in cases {
assert!(
exercise(case).is_none(),
"panicked on the regression input {case:?}"
);
}
}
#[test]
fn the_harness_would_notice_a_panic() {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let caught = catch_unwind(AssertUnwindSafe(|| panic!("deliberate"))).is_err();
std::panic::set_hook(hook);
assert!(caught);
}
#[test]
fn the_generator_reaches_past_the_first_byte() {
let seeds = seeds();
let (mut json_ok, mut schema_ok, mut nonempty) = (0usize, 0usize, 0usize);
let total = 20_000;
for i in 0..total {
let mut rng = Rng(0x9E37_79B9_7F4A_7C15 ^ i as u64);
let seed_input = rng.pick(&seeds).clone();
let input = mutate(&mut rng, &seed_input);
if !input.is_empty() {
nonempty += 1;
}
if Document::parse(input.as_bytes(), Limits::DEFAULT).is_ok() {
json_ok += 1;
}
if parse(&input).is_ok() {
schema_ok += 1;
}
}
assert!(
nonempty > total / 2,
"most inputs were empty: {nonempty}/{total}"
);
assert!(
json_ok > total / 100,
"almost nothing parsed as JSON: {json_ok}/{total} — the generator is producing noise"
);
assert!(
schema_ok > total / 1000,
"almost nothing parsed as a schema: {schema_ok}/{total} — the generator is producing noise"
);
println!("corpus: {json_ok}/{total} valid JSON, {schema_ok}/{total} valid schemas");
}