mod common;
use std::io::Cursor;
use std::panic::{catch_unwind, AssertUnwindSafe};
use common::{fixture_bytes, open, vector, vectors};
use wav_codec::{Error, SampleFormat, WavReader, WavWriter};
fn finish<W: std::io::Write + std::io::Seek>(w: WavWriter<W>) {
match w.finalize() {
Ok(()) | Err(Error::UnfinishedFrame) => {}
Err(e) => panic!("finalize failed: {}", e),
}
}
fn exercise(data: &[u8]) -> usize {
let mut reader = match WavReader::new(Cursor::new(data)) {
Ok(r) => r,
Err(_) => return 0,
};
let spec_ex = reader.spec_ex();
let width = usize::from(spec_ex.bytes_per_sample);
assert!(width >= 1);
let n = match spec_ex.spec.sample_format {
SampleFormat::Int => {
let mut got = Vec::new();
for s in reader.samples::<i32>() {
match s {
Ok(v) => got.push(v),
Err(_) => break,
}
}
if let Ok(mut w) = WavWriter::new_with_spec_ex(Cursor::new(Vec::new()), spec_ex) {
w.write_samples(&got).unwrap();
finish(w);
}
got.len()
}
SampleFormat::Float => {
let mut got = Vec::new();
for s in reader.samples::<f64>() {
match s {
Ok(v) => got.push(v),
Err(_) => break,
}
}
if let Ok(mut w) = WavWriter::new_with_spec_ex(Cursor::new(Vec::new()), spec_ex) {
if spec_ex.spec.bits_per_sample == 64 {
w.write_samples(&got).unwrap();
} else {
let narrow: Vec<f32> = got.iter().map(|v| *v as f32).collect();
w.write_samples(&narrow).unwrap();
}
finish(w);
}
got.len()
}
};
assert!(n * width <= data.len(), "more samples than bytes");
if let Ok(mut reader) = WavReader::new(Cursor::new(data)) {
let mut bulk = 0usize;
let mut buf = [0i32; 7];
let mut fbuf = [0f64; 7];
loop {
let r = match reader.spec().sample_format {
SampleFormat::Int => reader.read_samples(&mut buf),
SampleFormat::Float => reader.read_samples(&mut fbuf),
};
match r {
Ok(0) => break,
Ok(k) => bulk += k,
Err(_) => break,
}
}
assert!(bulk <= n);
assert!(n - bulk < 7);
}
n
}
fn must_not_panic(label: &str, data: &[u8]) -> usize {
catch_unwind(AssertUnwindSafe(|| exercise(data)))
.unwrap_or_else(|_| panic!("{} panicked", label))
}
fn seeds() -> Vec<(String, Vec<u8>)> {
let mut out = Vec::new();
for v in vectors() {
if v.field("kind").as_str() == "perf" {
continue;
}
out.push((v.field("id").as_str().to_string(), fixture_bytes(&v)));
}
for name in ["ref-ffmpeg-f64.wav", "ref-ffmpeg-s24.wav"] {
let bytes = std::fs::read(common::fixture_path(&format!("fixtures/{}", name))).unwrap();
out.push((name.to_string(), bytes));
}
out
}
#[test]
fn fuzz_corpus_smoke() {
let mut runs = 0usize;
for (id, bytes) in seeds() {
must_not_panic(&id, &bytes);
runs += 1;
for cut in 0..bytes.len() {
must_not_panic(&format!("{} cut at {}", id, cut), &bytes[..cut]);
runs += 1;
}
let mut flipped = bytes.clone();
for i in 0..bytes.len() {
flipped[i] ^= 0xFF;
must_not_panic(&format!("{} flip at {}", id, i), &flipped);
flipped[i] = bytes[i];
runs += 1;
for b in [0x00u8, 0x01, 0x7F, 0x80] {
flipped[i] = b;
must_not_panic(&format!("{} set {} at {}", id, b, i), &flipped);
flipped[i] = bytes[i];
runs += 1;
}
}
}
assert!(runs > 10_000, "{} runs", runs);
}
#[test]
fn hostile_header_values_do_not_panic() {
let base = fixture_bytes(&vectors()[1]); let mut cases: Vec<(&str, Vec<u8>)> = Vec::new();
let mut b = base.clone();
b[16..20].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
cases.push(("fmt size u32::MAX", b));
let mut b = base.clone();
b[16..20].copy_from_slice(&17u32.to_le_bytes());
cases.push(("fmt size 17", b));
let mut b = base.clone();
b[22..24].copy_from_slice(&0xFFFFu16.to_le_bytes());
b[32..34].copy_from_slice(&0xFFFFu16.to_le_bytes());
cases.push(("65535 channels, block align 65535", b));
let mut b = base.clone();
b[32..34].copy_from_slice(&0xFFFFu16.to_le_bytes());
cases.push(("block align 65535 with 2 channels", b));
let mut b = base.clone();
b[34..36].copy_from_slice(&0xFFFFu16.to_le_bytes());
cases.push(("bits 65535", b));
let mut b = base.clone();
b[34..36].copy_from_slice(&0u16.to_le_bytes());
cases.push(("bits 0", b));
let mut b = base.clone();
b[32..34].copy_from_slice(&2u16.to_le_bytes());
b[34..36].copy_from_slice(&1u16.to_le_bytes());
cases.push(("1 valid bit in a 1-byte container", b));
let mut b = base.clone();
b[40..44].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
cases.push(("data size u32::MAX (odd)", b));
let mut b = base.clone();
b[40..44].copy_from_slice(&0xFFFF_FFFCu32.to_le_bytes());
cases.push(("data size u32::MAX - 3", b));
let mut b = base.clone();
b[36..40].copy_from_slice(b"JUNK");
b[40..44].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes());
cases.push(("unknown chunk size u32::MAX", b));
let mut b = base.clone();
b[36..40].copy_from_slice(b"fmt ");
cases.push(("duplicate fmt", b));
let mut b = base.clone();
b[36..40].copy_from_slice(b"\x00\x01\x02\x03");
cases.push(("unprintable chunk id", b));
let mut b = base.clone();
b[24..28].copy_from_slice(&0u32.to_le_bytes());
cases.push(("zero sample rate", b));
let mut b = base.clone();
b[20..22].copy_from_slice(&0xFFFEu16.to_le_bytes());
cases.push(("extensible tag in a 16-byte fmt", b));
let mut b = base.clone();
b[20..22].copy_from_slice(&0x0002u16.to_le_bytes());
cases.push(("ADPCM tag", b));
for (label, bytes) in cases {
let n = must_not_panic(label, &bytes);
assert!(n <= 8, "{}: {} samples", label, n);
}
for len in 0..13 {
must_not_panic("short RIFF", &base[..len]);
}
}
#[test]
fn declared_size_does_not_drive_allocation() {
let x08 = fixture_bytes(
&vectors()
.into_iter()
.find(|v| v.field("id").as_str() == "X08")
.unwrap(),
);
let start = std::time::Instant::now();
let n = must_not_panic("X08", &x08);
assert_eq!(n, 2);
assert!(
start.elapsed().as_secs() < 5,
"the drain must not walk the declared size"
);
}
#[test]
fn wide_block_align_does_not_overflow() {
let mut bytes = fixture_bytes(&vector("V01"));
bytes[32..34].copy_from_slice(&0xFF01u16.to_le_bytes());
let outcome = catch_unwind(AssertUnwindSafe(|| open(&bytes).map(|_| ())));
let outcome = outcome.expect("parsing must not panic");
assert!(matches!(outcome, Err(Error::Unsupported(_))));
bytes[22..24].copy_from_slice(&8u16.to_le_bytes());
bytes[32..34].copy_from_slice(&0xFF00u16.to_le_bytes());
let outcome = catch_unwind(AssertUnwindSafe(|| open(&bytes).map(|_| ())));
assert!(matches!(
outcome.expect("no panic"),
Err(Error::Unsupported(_))
));
}