use std::panic::{AssertUnwindSafe, catch_unwind};
use std::time::{Duration, Instant};
use crate::extract::{extended, parse};
const PER_INPUT_LIMIT: Duration = Duration::from_secs(2);
const MAX_LENGTH: usize = 4096;
fn enabled() -> bool {
std::env::var("DATES_LE_FUZZ").as_deref() == Ok("1")
}
fn budget() -> Duration {
let seconds = std::env::var("DATES_LE_FUZZ_SECONDS")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(60);
Duration::from_secs(seconds)
}
fn seed() -> u64 {
std::env::var("DATES_LE_FUZZ_SEED")
.ok()
.and_then(|value| value.parse().ok())
.unwrap_or(0x2024_0115)
}
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, limit: usize) -> usize {
if limit == 0 {
return 0;
}
(self.next() % limit as u64) as usize
}
fn pick<'a, T>(&mut self, items: &'a [T]) -> &'a T {
&items[self.below(items.len())]
}
}
const ALPHABET: [char; 42] = [
'0', '1', '5', '9', '-', '+', ':', '.', ',', '/', 'T', 'Z', 'W', 'w', 'G', 'M', 'a', 'p', 'm',
'e', '(', ')', '[', ']', '<', '>', '"', '\'', '!', ' ', '\t', '\r', '\n', '\0', '\u{a0}',
'\u{feff}', 'é', '—', '🗓', '٢', '\u{130}', '\u{212a}',
];
fn seeds() -> Vec<String> {
#[derive(serde::Deserialize)]
struct Oracle {
cases: Vec<Case>,
}
#[derive(serde::Deserialize)]
struct Case {
input: String,
}
let oracle: Oracle = serde_json::from_str(include_str!("../fixtures/date-parse.json"))
.expect("the oracle is valid JSON");
let mut inputs: Vec<String> = oracle.cases.into_iter().map(|case| case.input).collect();
inputs.extend(
[
"2024-W03-1",
"2024-015",
"20240115T103045Z",
"Mon, 15 Jan 2024 10:30:45 CEST",
"Jan 15 10:30:47",
"15/Jan/2024:10:30:08 +0000",
"1705314645123456789",
"",
]
.into_iter()
.map(str::to_string),
);
inputs
}
fn mutate(rng: &mut Rng, input: &str, pool: &[String]) -> String {
let mut characters: Vec<char> = input.chars().collect();
let operations = 1 + rng.below(3);
for _ in 0..operations {
match rng.below(7) {
0 if !characters.is_empty() => {
let at = rng.below(characters.len());
characters.remove(at);
}
2 if !characters.is_empty() => {
let at = rng.below(characters.len());
characters[at] = *rng.pick(&ALPHABET);
}
3 if characters.len() > 1 => {
let (from, to) = (rng.below(characters.len()), rng.below(characters.len()));
characters.swap(from, to);
}
4 if !characters.is_empty() => {
let at = rng.below(characters.len());
let length = 1 + rng.below(characters.len() - at);
let slice: Vec<char> = characters[at..at + length].to_vec();
let times = 1 + rng.below(8);
for _ in 0..times {
characters.extend(slice.iter().copied());
}
}
5 => {
let other: Vec<char> = rng.pick(pool).chars().collect();
let at = rng.below(characters.len() + 1);
characters.splice(at..at, other);
}
_ => {
let at = rng.below(characters.len() + 1);
characters.insert(at, *rng.pick(&ALPHABET));
}
}
characters.truncate(MAX_LENGTH);
}
characters.into_iter().collect()
}
fn campaign(target: &str, body: impl Fn(&str)) {
let seed = seed();
let budget = budget();
let mut rng = Rng(seed | 1);
let pool = seeds();
let inputs: Vec<String> = pool.clone();
let started = Instant::now();
let mut iteration = 0usize;
while started.elapsed() < budget || iteration < inputs.len() {
let input = if iteration < inputs.len() {
inputs[iteration].clone()
} else {
let base = rng.pick(&pool).clone();
mutate(&mut rng, &base, &pool)
};
iteration += 1;
let call_started = Instant::now();
let outcome = catch_unwind(AssertUnwindSafe(|| body(&input)));
let elapsed = call_started.elapsed();
assert!(
outcome.is_ok(),
"{target} panicked\n seed: {seed}\n iteration: {iteration}\n input: {input:?}"
);
assert!(
elapsed < PER_INPUT_LIMIT,
"{target} took {elapsed:?} on one input, which is a hang rather than a scan\n \
seed: {seed}\n iteration: {iteration}\n input: {input:?}"
);
}
println!(
"fuzz {target}: {iteration} inputs in {:?}, seed {seed}",
started.elapsed()
);
}
#[test]
fn date_parse_never_panics_or_hangs() {
if !enabled() {
return;
}
campaign("date_parse", |input| {
let _ = parse::date_parse(input);
});
}
#[test]
fn the_layer_above_date_parse_never_panics_or_hangs() {
if !enabled() {
return;
}
campaign("extended::instant", |input| {
let _ = extended::instant(input);
let _ = extended::week_date(input);
let _ = extended::ordinal_date(input);
let _ = extended::basic_format(input);
});
}
const LANGUAGES: [&str; 6] = ["json", "xml", "log", "javascript", "html", "unknown"];
#[test]
fn the_scan_never_panics_or_hangs() {
if !enabled() {
return;
}
let next = std::cell::Cell::new(0usize);
campaign("extract", move |input| {
let language = LANGUAGES[next.get() % LANGUAGES.len()];
next.set(next.get() + 1);
let _ = crate::extract::extract(input, language, 2026);
});
}