diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d59e50..6d3b797 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,8 +67,9 @@ jobs: strategy: matrix: include: - - fuzz_target: fastpfor_cpp - - fuzz_target: fastpfor_rust + - fuzz_target: cpp_roundtrip + - fuzz_target: rust_compress_oracle + - fuzz_target: rust_decompress_oracle steps: - uses: actions/checkout@v6 diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 2388a51..687e89c 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -17,13 +17,19 @@ fastpfor = { path = "..", features = ["cpp", "rust"] } members = ["."] [[bin]] -name = "fastpfor_rust" -path = "fuzz_targets/fastpfor_rust.rs" +name = "cpp_roundtrip" +path = "fuzz_targets/cpp_roundtrip.rs" test = false doc = false [[bin]] -name = "fastpfor_cpp" -path = "fuzz_targets/fastpfor_cpp.rs" +name = "rust_compress_oracle" +path = "fuzz_targets/rust_compress_oracle.rs" +test = false +doc = false + +[[bin]] +name = "rust_decompress_oracle" +path = "fuzz_targets/rust_decompress_oracle.rs" test = false doc = false diff --git a/fuzz/README.md b/fuzz/README.md index 038f4b1..4233efd 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,36 +1,17 @@ # Fuzzing FastPFOR -This directory contains a fuzz test for the FastPFOR compression codec to find bugs, panics, and data corruption issues. +This directory contains fuzz tests for the FastPFOR compression codec to find bugs, panics, and data corruption issues. ## Why Fuzz FastPFOR? -The FastPFOR codec is the core compression algorithm. Fuzzing helps catch: -- Data corruption during compress/decompress roundtrips +The FastPFOR codec is a core compression algorithm. Fuzzing helps catch: +- Implementation discrepancies between Rust and C++ +- Data corruption during compress/decompress cycles - Panics on edge case inputs - Buffer overflows or underflows - Incorrect handling of different block sizes (128 vs 256) - Issues with boundary conditions (empty data, very large values, etc.) -## Known Issues Found - -The fuzzer has already discovered the following issues: - -### Data Loss with Small Inputs (Issue #1) - -**Input:** Single element array `[0]` with block size 128 - -**Issue:** FastPFOR silently drops data that doesn't fit into complete blocks. When the input length is less than the block size (128 or 256), `greatest_multiple(input_length, block_size)` returns 0, causing the codec to: -1. Write 0 to the output header -2. Skip compression entirely -3. On decompression, return 0 elements instead of the original input - -**Expected:** Should either: -- Compress partial blocks correctly, or -- Return an error indicating input is too small, or -- Document this limitation clearly - -This is a **data corruption bug** where the codec claims success but loses data. - ## Prerequisites Install cargo-fuzz and switch to nightly Rust: @@ -40,34 +21,41 @@ cargo install cargo-fuzz rustup install nightly ``` -## Running the Fuzzer +## Running the Fuzzers + +Run the oracle-based compression fuzzer: ```bash cd fuzz -cargo +nightly fuzz run fastpfor_rust +cargo +nightly fuzz run rust_compress_oracle +# or +cargo +nightly fuzz run rust_decompress_oracle +# or +cargo +nightly fuzz run cpp_roundtrip ``` Run for a specific duration (e.g., 60 seconds): ```bash -cargo +nightly fuzz run fastpfor_rust -- -max_total_time=60 +cargo +nightly fuzz run rust_compress_oracle -- -max_total_time=60 ``` -## What It Tests +Run with specific number of iterations: -The fuzzer: -1. Generates random sequences of u32 integers -2. Randomly selects block size (128 or 256) -3. Compresses the data with FastPFOR -4. Decompresses the result -5. Verifies the output matches the original input exactly - -This ensures the codec is lossless and doesn't corrupt data under any input pattern. +```bash +cargo +nightly fuzz run rust_compress_oracle -- -runs=1000 +``` ## If a Crash Is Found -Crashes are saved to `fuzz/artifacts/fastpfor_rust/`. To reproduce: +Crashes are saved to `fuzz/artifacts//`. To reproduce: + +```bash +cargo +nightly fuzz run fuzz/artifacts//crash- +``` + +For example: ```bash -cargo +nightly fuzz run fastpfor_rust fuzz/artifacts/fastpfor_rust/crash- +cargo +nightly fuzz run rust_compress_oracle fuzz/artifacts/rust_compress_oracle/crash-abc123 ``` diff --git a/fuzz/fuzz_targets/common.rs b/fuzz/fuzz_targets/common.rs new file mode 100644 index 0000000..b097619 --- /dev/null +++ b/fuzz/fuzz_targets/common.rs @@ -0,0 +1,119 @@ +use fastpfor::cpp; +use fastpfor::rust; + +pub type BoxedCppCodec = Box; + +#[derive(arbitrary::Arbitrary)] +pub struct FuzzInput { + pub data: Vec, + pub codec: C, +} + +impl std::fmt::Debug for FuzzInput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FuzzInput") + .field("data_length", &self.data.len()) + .field("codec", &self.codec) + .finish() + } +} + +#[derive(arbitrary::Arbitrary, Clone, Copy, PartialEq, Eq, Debug)] +pub enum RustCodec { + FastPFOR256, + FastPFOR128, + VariableByte, + JustCopy, +} + +impl From for rust::Codec { + fn from(codec: RustCodec) -> Self { + use rust::*; + match codec { + RustCodec::FastPFOR256 => Codec::from(FastPFOR::new(DEFAULT_PAGE_SIZE, BLOCK_SIZE_256)), + RustCodec::FastPFOR128 => Codec::from(FastPFOR::new(DEFAULT_PAGE_SIZE, BLOCK_SIZE_128)), + RustCodec::VariableByte => Codec::from(VariableByte::new()), + RustCodec::JustCopy => Codec::from(JustCopy::new()), + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq, arbitrary::Arbitrary, Debug)] +pub enum CppCodec { + BP32, + Copy, + FastBinaryPacking8, + FastPFor128, + FastPFor256, + FastBinaryPacking16, + FastBinaryPacking32, + MaskedVByte, + NewPFor, + OptPFor, + PFor2008, + PFor, + SimdBinaryPacking, + SimdFastPFor128, + SimdFastPFor256, + SimdGroupSimple, + SimdGroupSimpleRingBuf, + SimdNewPFor, + SimdOptPFor, + SimdPFor, + SimdSimplePFor, + // Simple16, // cannot encode arbitrary bytes + // Simple8b, // cannot encode arbitrary bytes + // Simple8bRle, // cannot encode arbitrary bytes + // Simple9, // cannot encode arbitrary bytes + // Simple9Rle, // cannot encode arbitrary bytes + // SimplePFor, // cannot encode arbitrary bytes + // Snappy, // Conditional with #ifdef + StreamVByte, + VByte, + VarInt, + // VarIntG8iu, // Conditional with #ifdef + VarIntGb, + // VsEncoding, // This is leaking memory +} + +impl From for BoxedCppCodec { + fn from(codec: CppCodec) -> Self { + use cpp::*; + match codec { + CppCodec::BP32 => Box::new(BP32Codec::default()), + CppCodec::Copy => Box::new(CopyCodec::default()), + CppCodec::FastBinaryPacking8 => Box::new(FastBinaryPacking8Codec::default()), + CppCodec::FastPFor128 => Box::new(FastPFor128Codec::default()), + CppCodec::FastPFor256 => Box::new(FastPFor256Codec::default()), + CppCodec::FastBinaryPacking16 => Box::new(FastBinaryPacking16Codec::default()), + CppCodec::FastBinaryPacking32 => Box::new(FastBinaryPacking32Codec::default()), + CppCodec::MaskedVByte => Box::new(MaskedVByteCodec::default()), + CppCodec::NewPFor => Box::new(NewPForCodec::default()), + CppCodec::OptPFor => Box::new(OptPForCodec::default()), + CppCodec::PFor2008 => Box::new(PFor2008Codec::default()), + CppCodec::PFor => Box::new(PForCodec::default()), + CppCodec::SimdBinaryPacking => Box::new(SimdBinaryPackingCodec::default()), + CppCodec::SimdFastPFor128 => Box::new(SimdFastPFor128Codec::default()), + CppCodec::SimdFastPFor256 => Box::new(SimdFastPFor256Codec::default()), + CppCodec::SimdGroupSimple => Box::new(SimdGroupSimpleCodec::default()), + CppCodec::SimdGroupSimpleRingBuf => Box::new(SimdGroupSimpleRingBufCodec::default()), + CppCodec::SimdNewPFor => Box::new(SimdNewPForCodec::default()), + CppCodec::SimdOptPFor => Box::new(SimdOptPForCodec::default()), + CppCodec::SimdPFor => Box::new(SimdPForCodec::default()), + CppCodec::SimdSimplePFor => Box::new(SimdSimplePForCodec::default()), + // CppCodec::Simple16 => Box::new(Simple16Codec::default()), + // CppCodec::Simple8b => Box::new(Simple8bCodec::default()), + // CppCodec::Simple8bRle => Box::new(Simple8bRleCodec::default()), + // CppCodec::Simple9 => Box::new(Simple9Codec::default()), + // CppCodec::Simple9Rle => Box::new(Simple9RleCodec::default()), + // CppCodec::SimplePFor => Box::new(SimplePForCodec::default()), + // CppCodec::Snappy => Box::new(SnappyCodec::default()), + CppCodec::StreamVByte => Box::new(StreamVByteCodec::default()), + CppCodec::VByte => Box::new(VByteCodec::default()), + CppCodec::VarInt => Box::new(VarIntCodec::default()), + // CppCodec::VarIntG8iu => Box::new(VarIntG8iuCodec::default()), + CppCodec::VarIntGb => Box::new(VarIntGbCodec::default()), + // CppCodec::VsEncoding => Box::new(VsEncodingCodec::default()), + } + } +} diff --git a/fuzz/fuzz_targets/cpp_roundtrip.rs b/fuzz/fuzz_targets/cpp_roundtrip.rs new file mode 100644 index 0000000..8fd4052 --- /dev/null +++ b/fuzz/fuzz_targets/cpp_roundtrip.rs @@ -0,0 +1,33 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +mod common; +use common::*; + +fuzz_target!(|data: FuzzInput| { + let codec = BoxedCppCodec::from(data.codec); + let input = data.data; + + // Allocate output buffer with generous size + let mut output = vec![0u32; input.len() * 2 + 1024]; + + // Compress the data + let enc_slice = codec.encode32(&input, &mut output).unwrap(); + + // Now decompress + let mut decoded = vec![0u32; input.len() * 2 + 1024]; + let dec_slice = codec.decode32(enc_slice, &mut decoded).unwrap(); + + // Verify roundtrip + if dec_slice.len() + input.len() < 200 { + assert_eq!(input, dec_slice, "Decompressed output mismatches"); + } else { + assert_eq!(dec_slice.len(), input.len(), "Decompressed length mismatch"); + for (i, (&original, &decoded)) in input.iter().zip(dec_slice.iter()).enumerate() { + assert_eq!( + original, decoded, + "Mismatch at position {i}: expected {original}, got {decoded}" + ); + } + } +}); diff --git a/fuzz/fuzz_targets/fastpfor_cpp.rs b/fuzz/fuzz_targets/fastpfor_cpp.rs deleted file mode 100644 index f74d936..0000000 --- a/fuzz/fuzz_targets/fastpfor_cpp.rs +++ /dev/null @@ -1,131 +0,0 @@ -#![no_main] - -use fastpfor::cpp::*; -use libfuzzer_sys::fuzz_target; - -type BoxedCodec = Box; - -fuzz_target!(|data: FuzzInput| { - let codec = BoxedCodec::from(data.codec); - let input = data.data; - - // Allocate output buffer with generous size - let mut output = vec![0u32; input.len() * 2 + 1024]; - - // Compress the data - let enc_slice = codec.encode32(&input, &mut output).unwrap(); - - // Now decompress - let mut decoded = vec![0u32; input.len() * 2 + 1024]; - let dec_slice = codec.decode32(&enc_slice, &mut decoded).unwrap(); - - // Verify roundtrip - if dec_slice.len() + input.len() < 200 { - assert_eq!(input, dec_slice, "Decompressed output mismatches"); - } else { - assert_eq!(dec_slice.len(), input.len(), "Decompressed length mismatch"); - for (i, (&original, &decoded)) in input.iter().zip(dec_slice.iter()).enumerate() { - assert_eq!( - original, decoded, - "Mismatch at position {i}: expected {original}, got {decoded}" - ); - } - } - assert_eq!(dec_slice.len(), input.len()); -}); - -#[derive(arbitrary::Arbitrary)] -struct FuzzInput { - data: Vec, - codec: FuzzCodec, -} - -impl std::fmt::Debug for FuzzInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FuzzInput") - .field("data_length", &self.data.len()) - .field("codec", &self.codec) - .finish() - } -} - -#[derive(Clone, Copy, Eq, PartialEq, arbitrary::Arbitrary, Debug)] -enum FuzzCodec { - BP32Codec, - CopyCodec, - FastBinaryPacking8Codec, - FastPFor128Codec, - FastPFor256Codec, - FastBinaryPacking16Codec, - FastBinaryPacking32Codec, - MaskedVByteCodec, - NewPForCodec, - OptPForCodec, - PFor2008Codec, - PForCodec, - SimdBinaryPackingCodec, - SimdFastPFor128Codec, - SimdFastPFor256Codec, - SimdGroupSimpleCodec, - SimdGroupSimpleRingBufCodec, - SimdNewPForCodec, - SimdOptPForCodec, - SimdPForCodec, - SimdSimplePForCodec, - // Simple16Codec, // cannot encode arbitrary bytes - // Simple8bCodec, // cannot encode arbitrary bytes - // Simple8bRleCodec, // cannot encode arbitrary bytes - // Simple9Codec, // cannot encode arbitrary bytes - // Simple9RleCodec, // cannot encode arbitrary bytes - // SimplePForCodec, // cannot encode arbitrary bytes - // SnappyCodec, // Conditional with #ifdef - StreamVByteCodec, - VByteCodec, - VarIntCodec, - // VarIntG8iuCodec, // Conditional with #ifdef - VarIntGbCodec, - // VsEncodingCodec, // This is leaking memory -} - -impl From for BoxedCodec { - fn from(codec: FuzzCodec) -> Self { - match codec { - FuzzCodec::BP32Codec => Box::new(BP32Codec::default()), - FuzzCodec::CopyCodec => Box::new(CopyCodec::default()), - FuzzCodec::FastBinaryPacking8Codec => Box::new(FastBinaryPacking8Codec::default()), - FuzzCodec::FastPFor128Codec => Box::new(FastPFor128Codec::default()), - FuzzCodec::FastPFor256Codec => Box::new(FastPFor256Codec::default()), - FuzzCodec::FastBinaryPacking16Codec => Box::new(FastBinaryPacking16Codec::default()), - FuzzCodec::FastBinaryPacking32Codec => Box::new(FastBinaryPacking32Codec::default()), - FuzzCodec::MaskedVByteCodec => Box::new(MaskedVByteCodec::default()), - FuzzCodec::NewPForCodec => Box::new(NewPForCodec::default()), - FuzzCodec::OptPForCodec => Box::new(OptPForCodec::default()), - FuzzCodec::PFor2008Codec => Box::new(PFor2008Codec::default()), - FuzzCodec::PForCodec => Box::new(PForCodec::default()), - FuzzCodec::SimdBinaryPackingCodec => Box::new(SimdBinaryPackingCodec::default()), - FuzzCodec::SimdFastPFor128Codec => Box::new(SimdFastPFor128Codec::default()), - FuzzCodec::SimdFastPFor256Codec => Box::new(SimdFastPFor256Codec::default()), - FuzzCodec::SimdGroupSimpleCodec => Box::new(SimdGroupSimpleCodec::default()), - FuzzCodec::SimdGroupSimpleRingBufCodec => { - Box::new(SimdGroupSimpleRingBufCodec::default()) - } - FuzzCodec::SimdNewPForCodec => Box::new(SimdNewPForCodec::default()), - FuzzCodec::SimdOptPForCodec => Box::new(SimdOptPForCodec::default()), - FuzzCodec::SimdPForCodec => Box::new(SimdPForCodec::default()), - FuzzCodec::SimdSimplePForCodec => Box::new(SimdSimplePForCodec::default()), - // FuzzCodec::Simple16Codec => Box::new(Simple16Codec::default()), - // FuzzCodec::Simple8bCodec => Box::new(Simple8bCodec::default()), - // FuzzCodec::Simple8bRleCodec => Box::new(Simple8bRleCodec::default()), - // FuzzCodec::Simple9Codec => Box::new(Simple9Codec::default()), - // FuzzCodec::Simple9RleCodec => Box::new(Simple9RleCodec::default()), - // FuzzCodec::SimplePForCodec => Box::new(SimplePForCodec::default()), - // FuzzCodec::SnappyCodec => Box::new(SnappyCodec::default()), - FuzzCodec::StreamVByteCodec => Box::new(StreamVByteCodec::default()), - FuzzCodec::VByteCodec => Box::new(VByteCodec::default()), - FuzzCodec::VarIntCodec => Box::new(VarIntCodec::default()), - // FuzzCodec::VarIntG8iuCodec => Box::new(VarIntG8iuCodec::default()), - FuzzCodec::VarIntGbCodec => Box::new(VarIntGbCodec::default()), - // FuzzCodec::VsEncodingCodec => Box::new(VsEncodingCodec::default()), - } - } -} diff --git a/fuzz/fuzz_targets/fastpfor_rust.rs b/fuzz/fuzz_targets/fastpfor_rust.rs deleted file mode 100644 index 1737f47..0000000 --- a/fuzz/fuzz_targets/fastpfor_rust.rs +++ /dev/null @@ -1,109 +0,0 @@ -#![no_main] - -use std::io::Cursor; -use std::num::NonZeroU32; - -use fastpfor::rust::{BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE, FastPFOR, Integer}; -use libfuzzer_sys::fuzz_target; - -fuzz_target!(|data: FuzzInput| { - let input = data.data; - let block_size = NonZeroU32::from(data.codec); - let mut codec = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size); - - // TODO: empty input is encoded as empty, which does not match the CPP version - if input.is_empty() { - return; - } - - // TODO: only multiples of block size seem to be exported from the compress - decompress cycle - let bs = block_size.get() as usize; - if input.len() < bs { - return; - } - let last_block_size_multiple = input.len() / bs * bs; - let input = input - .into_iter() - .take(last_block_size_multiple as usize) - .collect::>(); - - // Allocate output buffer with generous size - let mut compressed = vec![0u32; input.len() * 2 + 1024]; - - // Compress the data - let mut output_offset = Cursor::new(0); - codec - .compress( - &input, - input.len() as u32, - &mut Cursor::new(0), - &mut compressed, - &mut output_offset, - ) - .unwrap(); - let compressed_size = output_offset.position() as u32; - assert!(compressed_size != 0, "compression should not be empty"); - - // Now decompress - let mut decompressed = vec![0u32; input.len()]; - let mut output_offset = Cursor::new(0); - - codec - .uncompress( - &compressed, - compressed_size, - &mut Cursor::new(0), - &mut decompressed, - &mut output_offset, - ) - .unwrap(); - let decompressed_length = output_offset.position() as usize; - - // Verify roundtrip - assert_eq!(decompressed_length, input.len()); - if decompressed_length + input.len() < 200 { - assert_eq!( - input, - decompressed[..decompressed_length], - "Decompressed mismatch: expected {}, got {decompressed_length}", - input.len() - ); - } else { - for (i, (&original, &decoded)) in input.iter().zip(decompressed.iter()).enumerate() { - assert_eq!( - original, decoded, - "Mismatch at position {}: expected {}, got {}", - i, original, decoded - ); - } - } -}); - -#[derive(arbitrary::Arbitrary)] -struct FuzzInput { - data: Vec, - codec: FuzzCodec, -} - -impl std::fmt::Debug for FuzzInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("FuzzInput") - .field("data_length", &self.data.len()) - .field("codec", &self.codec) - .finish() - } -} - -#[derive(arbitrary::Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] -enum FuzzCodec { - FastPFOR256, - FastPFOR128, -} -impl From for NonZeroU32 { - fn from(codec: FuzzCodec) -> Self { - match codec { - FuzzCodec::FastPFOR256 => BLOCK_SIZE_256, - FuzzCodec::FastPFOR128 => BLOCK_SIZE_128, - } - } -} diff --git a/fuzz/fuzz_targets/rust_compress_oracle.rs b/fuzz/fuzz_targets/rust_compress_oracle.rs new file mode 100644 index 0000000..6ed1f1b --- /dev/null +++ b/fuzz/fuzz_targets/rust_compress_oracle.rs @@ -0,0 +1,90 @@ +#![no_main] + +use fastpfor::{CodecToSlice, cpp, rust}; +use libfuzzer_sys::fuzz_target; +mod common; +use common::*; + +fuzz_target!(|data: FuzzInput| { + let input = data.data; + + // TODO: Behaviour differs + if input.is_empty() { + return; + } + + // TODO: Behaviour differs + if data.codec == RustCodec::VariableByte { + return; + } + + // TODO: To make the encoder not crash -> Skip inputs smaller than block size + let block_size = match data.codec { + RustCodec::FastPFOR256 => 256, + RustCodec::FastPFOR128 => 128, + RustCodec::VariableByte => 1, + RustCodec::JustCopy => 1, + }; + if input.len() < block_size { + return; + } + + // TODO: To make the encoder not crash -> Truncate to block size multiple + let last_block_size_multiple = input.len() / block_size * block_size; + let input = &input[..last_block_size_multiple]; + + // Allocate output buffers with generous size + let mut rust_compressed = vec![0u32; input.len() * 2 + 1024]; + let mut cpp_compressed = vec![0u32; input.len() * 2 + 1024]; + + // Compress with Rust implementation using Codec wrapper + let mut rust_codec = rust::Codec::from(data.codec); + let rust_result = rust_codec + .compress_to_slice(input, &mut rust_compressed) + .expect("Rust compression failed"); + + // Compress with C++ implementation + let cpp_result = match data.codec { + RustCodec::FastPFOR256 => { + let mut cpp_codec = cpp::FastPFor256Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::FastPFOR128 => { + let mut cpp_codec = cpp::FastPFor128Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::VariableByte => { + let mut cpp_codec = cpp::VByteCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::JustCopy => { + let mut cpp_codec = cpp::CopyCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + }; + + // Compare compressed outputs + assert_eq!( + rust_result.len(), + cpp_result.len(), + "Compressed length mismatch: Rust={}, C++={}", + rust_result.len(), + cpp_result.len() + ); + + for (i, (&rust_val, &cpp_val)) in rust_result.iter().zip(cpp_result.iter()).enumerate() { + assert_eq!( + rust_val, cpp_val, + "Compressed data mismatch at position {}: Rust={}, C++={}", + i, rust_val, cpp_val + ); + } +}); diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs new file mode 100644 index 0000000..1ccf20f --- /dev/null +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -0,0 +1,88 @@ +#![no_main] + +use fastpfor::{CodecToSlice, cpp, rust}; +use libfuzzer_sys::fuzz_target; +mod common; +use common::*; + +fuzz_target!(|data: FuzzInput| { + let input = data.data; + + // TODO: Behaviour differs + if input.is_empty() { + return; + } + + // TODO: Behaviour differs + if data.codec == RustCodec::VariableByte { + return; + } + + // TODO: To make the decoder not crash -> Skip inputs smaller than block size + let block_size = match data.codec { + RustCodec::FastPFOR256 => 256, + RustCodec::FastPFOR128 => 128, + RustCodec::VariableByte => 1, + RustCodec::JustCopy => 1, + }; + if input.len() < block_size { + return; + } + + // TODO: To make the decoder not crash -> Truncate to block size multiple + let last_block_size_multiple = input.len() / block_size * block_size; + let input = &input[..last_block_size_multiple]; + + // First, compress with C++ implementation to get valid compressed data + let mut cpp_compressed = vec![0u32; input.len() * 2 + 1024]; + let compressed_data = match data.codec { + RustCodec::FastPFOR256 => { + let mut cpp_codec = cpp::FastPFor256Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::FastPFOR128 => { + let mut cpp_codec = cpp::FastPFor128Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::VariableByte => { + let mut cpp_codec = cpp::VByteCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + RustCodec::JustCopy => { + let mut cpp_codec = cpp::CopyCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + }; + + // Now decompress with rust + let mut rust_decompressed = vec![0u32; input.len()]; + let mut rust_codec = rust::Codec::from(data.codec); + let rust_result = rust_codec + .decompress_to_slice(compressed_data, &mut rust_decompressed) + .expect("Rust decompression failed"); + + // Compare decompressed outputs + assert_eq!( + rust_result.len(), + input.len(), + "Decompressed length mismatch: Rust={}, C++={}", + rust_result.len(), + input.len() + ); + + for (i, (&rust_val, &cpp_val)) in rust_result.iter().zip(input.iter()).enumerate() { + assert_eq!( + rust_val, cpp_val, + "Decompressed data mismatch at position {}: Rust={}, C++={}", + i, rust_val, cpp_val + ); + } +});