//! Fuzzer for encoder/decoder self-consistency. //! //! Interprets the fuzzer's byte input as a sequence of 24-bit signed PCM //! samples, clamped to the LAC input range, encodes the frame, and //! verifies the decoded output matches the original samples byte-for-byte. //! //! Catches any encoder bug that produces a bitstream the decoder can't //! reconstruct exactly — which is the whole point of a lossless codec. //! //! This complements `decode_arbitrary`: //! - `decode_arbitrary` asserts the decoder is robust to arbitrary input. //! - `roundtrip_arbitrary` asserts encoder and decoder agree on meaning. #![no_main] use libfuzzer_sys::fuzz_target; /// LAC's documented input range (spec §1): `|sample| ≤ 2^23 − 1`. /// Fuzzer bytes are interpreted as i32 then clamped to this symmetric /// range — note the lower bound is `-(2^23 − 1)`, NOT the two's-complement /// `-2^23`; the encoder's debug-asserted input contract excludes the /// extra negative value. const SAMPLE_MIN: i32 = -((1 << 23) - 1); const SAMPLE_MAX: i32 = (1 << 23) - 1; fuzz_target!(|data: &[u8]| { // Need at least one sample (4 bytes interpreted as i32) and a // reasonable upper bound to keep each fuzz iteration fast. 16k samples // at 48 kHz is ~340 ms of audio, more than enough to exercise every // encoder path including the full MAX_PARTITION_ORDER search. if data.len() < 4 || data.len() > 65_536 { return; } // Read i32 chunks little-endian, clamp into LAC's 24-bit range. let samples: Vec = data .chunks_exact(4) .map(|c| { let raw = i32::from_le_bytes([c[0], c[1], c[2], c[3]]); raw.clamp(SAMPLE_MIN, SAMPLE_MAX) }) .collect(); if samples.is_empty() { return; } let encoded = lac::encode_frame(&samples); let decoded = lac::decode_frame(&encoded) .expect("decoder rejected its own encoder's output"); assert_eq!( decoded, samples, "round-trip mismatch: {} samples, encoded {} bytes", samples.len(), encoded.len() ); });