From a3e2faa41231abc0603d26772feac9cc33aa6ea8 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 14:09:36 +0100 Subject: [PATCH 01/15] add an unified API --- src/cpp/mod.rs | 65 ++++++++++ src/lib.rs | 36 ++++++ src/rust/integer_compression/codec.rs | 118 +++++++++++++++++- src/rust/integer_compression/integer_codec.rs | 46 +------ 4 files changed, 219 insertions(+), 46 deletions(-) diff --git a/src/cpp/mod.rs b/src/cpp/mod.rs index e145ab0..0399a8b 100644 --- a/src/cpp/mod.rs +++ b/src/cpp/mod.rs @@ -4,6 +4,8 @@ pub use cxx::Exception; use cxx::UniquePtr; +use crate::CodecToSlice; + /// FFI bridge to the C++ FastPFOR library. /// /// This module contains the raw FFI declarations for interfacing with the C++ code. @@ -142,6 +144,51 @@ pub trait Codec32: CodecWrapper { } } +impl CodecToSlice for C { + type Error = Exception; + + fn compress_to_slice<'out>( + &mut self, + input: &[u32], + output: &'out mut [u32], + ) -> Result<&'out [u32], Self::Error> { + let result = self.encode32(input, output)?; + Ok(&*result) + } + + fn decompress_to_slice<'out>( + &mut self, + input: &[u32], + output: &'out mut [u32], + ) -> Result<&'out [u32], Self::Error> { + let result = self.decode32(input, output)?; + Ok(&*result) + } +} + +// Note: 64-bit integers are compressed into 32-bit word arrays. +impl CodecToSlice for C { + type Error = Exception; + + fn compress_to_slice<'out>( + &mut self, + input: &[u64], + output: &'out mut [u32], + ) -> Result<&'out [u32], Self::Error> { + let result = self.encode64(input, output)?; + Ok(&*result) + } + + fn decompress_to_slice<'out>( + &mut self, + input: &[u32], + output: &'out mut [u64], + ) -> Result<&'out [u64], Self::Error> { + let result = self.decode64(input, output)?; + Ok(&*result) + } +} + /// Trait for codecs that support 64-bit integer compression. /// /// Only certain codecs support 64-bit integers. These are marked with the `@ 64` @@ -420,4 +467,22 @@ mod tests { assert_eq!(decoded, input); } + + #[test] + fn supports_compress_to_slice() { + let mut cpp_codec = FastPFor128Codec::new(); + let data = vec![1, 2, 3, 4, 5]; + let mut compressed = vec![0u32; data.len() * 4]; + + let compressed_len = { + let result = cpp_codec.compress_to_slice(&data, &mut compressed).unwrap(); + result.len() + }; + + let mut decompressed = vec![0u32; data.len()]; + let result = cpp_codec + .decompress_to_slice(&compressed[..compressed_len], &mut decompressed) + .unwrap(); + assert_eq!(result, &data[..]); + } } diff --git a/src/lib.rs b/src/lib.rs index 92afc0f..6cadd12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,3 +16,39 @@ pub mod cpp; #[cfg(feature = "rust")] /// Rust re-implementation of `FastPFor` (work in progress) pub mod rust; + +/// Low-level compression interface using caller-provided buffers. +/// +/// Codecs write into pre-allocated slices and return a sub-slice showing exactly +/// what was written. Works across FFI boundaries and allows buffer reuse. +/// +/// # Type Parameters +/// +/// - `In`: Input data type (e.g., `u32` or `u64` for integer codecs) +/// - `Out`: Compressed output type (defaults to `In`, but may differ - e.g., +/// 64-bit integers compress to 32-bit words: `CodecToSlice`) +/// +/// # Buffer Sizing +/// +/// Caller must ensure output buffers are large enough. For compression, estimate +/// `input.len() * 2 + 1024`. For decompression, size depends on the codec. +pub trait CodecToSlice { + /// Error type returned by compression/decompression operations. + type Error; + + /// Compresses input into output buffer, returning slice of data written. + fn compress_to_slice<'out>( + &mut self, + input: &[In], + output: &'out mut [Out], + ) -> Result<&'out [Out], Self::Error>; + + /// Decompresses input into output buffer, returning slice of data written. + /// + /// Output size cannot be known in advance for some codecs (e.g., RLE). + fn decompress_to_slice<'out>( + &mut self, + input: &[Out], + output: &'out mut [In], + ) -> Result<&'out [In], Self::Error>; +} diff --git a/src/rust/integer_compression/codec.rs b/src/rust/integer_compression/codec.rs index 59de19e..cc123ae 100644 --- a/src/rust/integer_compression/codec.rs +++ b/src/rust/integer_compression/codec.rs @@ -1,4 +1,7 @@ -use crate::rust::{FastPFOR, JustCopy, VariableByte}; +use std::io::Cursor; + +use crate::rust::{FastPFOR, FastPForResult, Integer, JustCopy, VariableByte}; +use crate::CodecToSlice; /// Type-erased wrapper for compression codecs. /// @@ -12,6 +15,94 @@ pub enum Codec { JustCopy(JustCopy), } +impl Integer for Codec { + fn compress( + &mut self, + input: &[u32], + input_length: u32, + input_offset: &mut Cursor, + output: &mut [u32], + output_offset: &mut Cursor, + ) -> FastPForResult<()> { + match self { + Codec::FastPFor(fastpfor) => { + fastpfor.compress(input, input_length, input_offset, output, output_offset) + } + Codec::VariableByte(vb) => { + vb.compress(input, input_length, input_offset, output, output_offset) + } + Codec::JustCopy(jc) => { + jc.compress(input, input_length, input_offset, output, output_offset) + } + } + } + + fn uncompress( + &mut self, + input: &[u32], + input_length: u32, + input_offset: &mut Cursor, + output: &mut [u32], + output_offset: &mut Cursor, + ) -> FastPForResult<()> { + match self { + Codec::FastPFor(fastpfor) => { + fastpfor.uncompress(input, input_length, input_offset, output, output_offset) + } + Codec::VariableByte(vb) => { + vb.uncompress(input, input_length, input_offset, output, output_offset) + } + Codec::JustCopy(jc) => { + jc.uncompress(input, input_length, input_offset, output, output_offset) + } + } + } +} + +impl CodecToSlice for Codec { + type Error = crate::rust::FastPForError; + + fn compress_to_slice<'out>( + &mut self, + input: &[u32], + output: &'out mut [u32], + ) -> Result<&'out [u32], Self::Error> { + let mut input_offset = Cursor::new(0); + let mut output_offset = Cursor::new(0); + + self.compress( + input, + input.len() as u32, + &mut input_offset, + output, + &mut output_offset, + )?; + + let written = output_offset.position() as usize; + Ok(&output[..written]) + } + + fn decompress_to_slice<'out>( + &mut self, + input: &[u32], + output: &'out mut [u32], + ) -> Result<&'out [u32], Self::Error> { + let mut input_offset = Cursor::new(0); + let mut output_offset = Cursor::new(0); + + self.uncompress( + input, + input.len() as u32, + &mut input_offset, + output, + &mut output_offset, + )?; + + let written = output_offset.position() as usize; + Ok(&output[..written]) + } +} + impl From for Codec { fn from(fastpfor: FastPFOR) -> Self { Codec::FastPFor(Box::new(fastpfor)) @@ -29,3 +120,28 @@ impl From for Codec { Codec::JustCopy(jc) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn supports_compress_to_slice() { + let data = vec![1, 2, 3, 4, 5]; + let mut rust_codec = Codec::from(VariableByte::new()); + let mut compressed = vec![0u32; data.len() * 4]; + + let compressed_len = { + let result = rust_codec + .compress_to_slice(&data, &mut compressed) + .unwrap(); + result.len() + }; + + let mut decompressed = vec![0u32; data.len()]; + let result = rust_codec + .decompress_to_slice(&compressed[..compressed_len], &mut decompressed) + .unwrap(); + assert_eq!(result, &data[..]); + } +} diff --git a/src/rust/integer_compression/integer_codec.rs b/src/rust/integer_compression/integer_codec.rs index afceb40..4ab11fd 100644 --- a/src/rust/integer_compression/integer_codec.rs +++ b/src/rust/integer_compression/integer_codec.rs @@ -1,6 +1,6 @@ use std::io::Cursor; -use crate::rust::{Codec, FastPForResult}; +use crate::rust::FastPForResult; /// Integer compression/decompression interface with length headers. /// @@ -37,47 +37,3 @@ pub trait Integer { output_offset: &mut Cursor, ) -> FastPForResult<()>; } - -impl Integer for Codec { - fn compress( - &mut self, - input: &[u32], - input_length: u32, - input_offset: &mut Cursor, - output: &mut [u32], - output_offset: &mut Cursor, - ) -> FastPForResult<()> { - match self { - Codec::FastPFor(fastpfor) => { - fastpfor.compress(input, input_length, input_offset, output, output_offset) - } - Codec::VariableByte(vb) => { - vb.compress(input, input_length, input_offset, output, output_offset) - } - Codec::JustCopy(jc) => { - jc.compress(input, input_length, input_offset, output, output_offset) - } - } - } - - fn uncompress( - &mut self, - input: &[u32], - input_length: u32, - input_offset: &mut Cursor, - output: &mut [u32], - output_offset: &mut Cursor, - ) -> FastPForResult<()> { - match self { - Codec::FastPFor(fastpfor) => { - fastpfor.uncompress(input, input_length, input_offset, output, output_offset) - } - Codec::VariableByte(vb) => { - vb.uncompress(input, input_length, input_offset, output, output_offset) - } - Codec::JustCopy(jc) => { - jc.uncompress(input, input_length, input_offset, output, output_offset) - } - } - } -} From ca25b50fe34dc7833069cae92fe3e356e46bd7ae Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 14:39:55 +0100 Subject: [PATCH 02/15] make error handling more robust --- src/rust/error.rs | 4 ++++ src/rust/integer_compression/codec.rs | 18 ++++++++++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/rust/error.rs b/src/rust/error.rs index 440d7e9..23964ed 100644 --- a/src/rust/error.rs +++ b/src/rust/error.rs @@ -18,4 +18,8 @@ pub enum FastPForError { /// Output buffer too small #[error("Output buffer too small")] OutputBufferTooSmall, + + /// Invalid input length + #[error("Invalid input length {0}")] + InvalidInputLength(usize), } diff --git a/src/rust/integer_compression/codec.rs b/src/rust/integer_compression/codec.rs index cc123ae..b7dba6e 100644 --- a/src/rust/integer_compression/codec.rs +++ b/src/rust/integer_compression/codec.rs @@ -67,13 +67,16 @@ impl CodecToSlice for Codec { input: &[u32], output: &'out mut [u32], ) -> Result<&'out [u32], Self::Error> { - let mut input_offset = Cursor::new(0); let mut output_offset = Cursor::new(0); + let input_length = input + .len() + .try_into() + .map_err(|_| Self::Error::InvalidInputLength(input.len()))?; self.compress( input, - input.len() as u32, - &mut input_offset, + input_length, + &mut Cursor::new(0), output, &mut output_offset, )?; @@ -87,13 +90,16 @@ impl CodecToSlice for Codec { input: &[u32], output: &'out mut [u32], ) -> Result<&'out [u32], Self::Error> { - let mut input_offset = Cursor::new(0); let mut output_offset = Cursor::new(0); + let input_length: u32 = input + .len() + .try_into() + .map_err(|_| Self::Error::InvalidInputLength(input.len()))?; self.uncompress( input, - input.len() as u32, - &mut input_offset, + input_length, + &mut Cursor::new(0), output, &mut output_offset, )?; From 5848a0a8b3331092b9208fc5ea4e67b50166fb05 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:40:05 +0000 Subject: [PATCH 03/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/rust/integer_compression/codec.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/rust/integer_compression/codec.rs b/src/rust/integer_compression/codec.rs index b7dba6e..67ced9d 100644 --- a/src/rust/integer_compression/codec.rs +++ b/src/rust/integer_compression/codec.rs @@ -69,9 +69,9 @@ impl CodecToSlice for Codec { ) -> Result<&'out [u32], Self::Error> { let mut output_offset = Cursor::new(0); let input_length = input - .len() - .try_into() - .map_err(|_| Self::Error::InvalidInputLength(input.len()))?; + .len() + .try_into() + .map_err(|_| Self::Error::InvalidInputLength(input.len()))?; self.compress( input, From 4682eab5381226da1eb0c7e8f403dad8acb01e00 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 14:45:06 +0100 Subject: [PATCH 04/15] increase test coverage --- src/cpp/mod.rs | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/src/cpp/mod.rs b/src/cpp/mod.rs index 0399a8b..e2d6156 100644 --- a/src/cpp/mod.rs +++ b/src/cpp/mod.rs @@ -432,57 +432,51 @@ mod tests { #[test] fn test_32() { - let codec = FastPFor128Codec::new(); + let mut codec = FastPFor128Codec::new(); let input = vec![1, 2, 3, 4, 5]; let mut output = vec![0; 10]; let mut output2 = vec![0; 10]; + let mut output3 = vec![0; 10]; let encoded = codec.encode32(&input, &mut output).unwrap(); let encoded2 = codec.encode32(&input, &mut output2).unwrap(); + let encoded3 = codec.compress_to_slice(&input, &mut output3).unwrap(); assert_eq!(encoded, encoded2); + assert_eq!(encoded, encoded3); let mut decoded = vec![0; 10]; let mut decoded2 = vec![0; 10]; + let mut decoded3 = vec![0; 10]; let decoded = codec.decode32(encoded, &mut decoded).unwrap(); let decoded2 = codec.decode32(encoded, &mut decoded2).unwrap(); + let decoded3 = codec.decompress_to_slice(encoded, &mut decoded3).unwrap(); assert_eq!(decoded, decoded2); + assert_eq!(decoded, decoded3); assert_eq!(decoded, input); } #[test] fn test_64() { - let codec = FastPFor128Codec::new(); + let mut codec = FastPFor128Codec::new(); let input = vec![1, 2, 3, 4, 5]; let mut output = vec![0; 10]; let mut output2 = vec![0; 10]; + let mut output3 = vec![0; 10]; let encoded = codec.encode64(&input, &mut output).unwrap(); let encoded2 = codec.encode64(&input, &mut output2).unwrap(); + let encoded3 = codec.compress_to_slice(&input, &mut output3).unwrap(); assert_eq!(encoded, encoded2); + assert_eq!(encoded, encoded3); let mut decoded = vec![0; 10]; let mut decoded2 = vec![0; 10]; + let mut decoded3 = vec![0; 10]; let decoded = codec.decode64(encoded, &mut decoded).unwrap(); let decoded2 = codec.decode64(encoded, &mut decoded2).unwrap(); + let decoded3 = codec.decompress_to_slice(encoded, &mut decoded3).unwrap(); assert_eq!(decoded, decoded2); + assert_eq!(decoded, decoded3); assert_eq!(decoded, input); } - - #[test] - fn supports_compress_to_slice() { - let mut cpp_codec = FastPFor128Codec::new(); - let data = vec![1, 2, 3, 4, 5]; - let mut compressed = vec![0u32; data.len() * 4]; - - let compressed_len = { - let result = cpp_codec.compress_to_slice(&data, &mut compressed).unwrap(); - result.len() - }; - - let mut decompressed = vec![0u32; data.len()]; - let result = cpp_codec - .decompress_to_slice(&compressed[..compressed_len], &mut decompressed) - .unwrap(); - assert_eq!(result, &data[..]); - } } From bdac88bc7f28802f2fad5198c615dfce7ff2294a Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 15:26:49 +0100 Subject: [PATCH 05/15] simplify typs --- src/cpp/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cpp/mod.rs b/src/cpp/mod.rs index e2d6156..4855679 100644 --- a/src/cpp/mod.rs +++ b/src/cpp/mod.rs @@ -153,7 +153,7 @@ impl CodecToSlice for C { output: &'out mut [u32], ) -> Result<&'out [u32], Self::Error> { let result = self.encode32(input, output)?; - Ok(&*result) + Ok(result) } fn decompress_to_slice<'out>( @@ -162,7 +162,7 @@ impl CodecToSlice for C { output: &'out mut [u32], ) -> Result<&'out [u32], Self::Error> { let result = self.decode32(input, output)?; - Ok(&*result) + Ok(result) } } @@ -176,7 +176,7 @@ impl CodecToSlice for C { output: &'out mut [u32], ) -> Result<&'out [u32], Self::Error> { let result = self.encode64(input, output)?; - Ok(&*result) + Ok(result) } fn decompress_to_slice<'out>( @@ -185,7 +185,7 @@ impl CodecToSlice for C { output: &'out mut [u64], ) -> Result<&'out [u64], Self::Error> { let result = self.decode64(input, output)?; - Ok(&*result) + Ok(result) } } From a83d7af57279b690555a59c99648e2a35ea59827 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 15:54:27 +0100 Subject: [PATCH 06/15] refactor rust fuzzer to be oracle based --- .github/workflows/ci.yml | 5 +- fuzz/Cargo.toml | 14 +- fuzz/README.md | 141 ++++++++++++++---- .../{fastpfor_cpp.rs => cpp_roundtrip.rs} | 0 fuzz/fuzz_targets/fastpfor_rust.rs | 109 -------------- fuzz/fuzz_targets/rust_compress_oracle.rs | 93 ++++++++++++ fuzz/fuzz_targets/rust_decompress_oracle.rs | 91 +++++++++++ 7 files changed, 306 insertions(+), 147 deletions(-) rename fuzz/fuzz_targets/{fastpfor_cpp.rs => cpp_roundtrip.rs} (100%) delete mode 100644 fuzz/fuzz_targets/fastpfor_rust.rs create mode 100644 fuzz/fuzz_targets/rust_compress_oracle.rs create mode 100644 fuzz/fuzz_targets/rust_decompress_oracle.rs 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..2647d79 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,35 +1,63 @@ # 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. + +## Fuzz Targets + +### Oracle-Based Fuzzers (Recommended) + +These fuzzers compare the Rust implementation against the C++ reference implementation to find behavioral differences. + +**`rust_compress_oracle`** - Compares Rust and C++ compression outputs +- Generates random input data +- Compresses with both Rust and C++ implementations +- Verifies both produce **identical compressed output** +- Finds discrepancies in compression behavior between implementations + +**`rust_decompress_oracle`** - Compares Rust and C++ decompression outputs +- Generates random input data +- Compresses with C++ implementation (known good) +- Decompresses with both Rust and C++ implementations +- Verifies both produce **identical decompressed output** +- Ensures Rust can decompress C++-compressed data correctly +- Validates both implementations recover the original input + +### Roundtrip Fuzzer + +**`cpp_roundtrip`** - Tests C++ implementation roundtrip +- Compress → decompress → verify cycle +- Validates the reference C++ implementation +- Finds data corruption, crashes, and panics in the C++ FFI + +## Why Oracle-Based Fuzzing? + +Oracle fuzzing is the gold standard for verifying a reimplementation: + +1. **Detects behavioral differences** - The Rust implementation should produce byte-for-byte identical output to the C++ reference +2. **Catches subtle bugs** - Even if both implementations round-trip correctly in isolation, they might compress differently +3. **Validates compatibility** - Ensures Rust can decompress C++ data and vice versa +4. **No false positives** - If outputs differ, it's a real bug (not just random data causing a crash) ## 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 +## Known Limitations -The fuzzer has already discovered the following issues: +### Block Size Requirements -### Data Loss with Small Inputs (Issue #1) +FastPFOR requires input lengths to be multiples of the block size (128 or 256). The fuzzers currently skip inputs that don't meet this requirement. Both implementations: +- Silently process only complete blocks +- Drop data that doesn't fit into complete blocks +- Should either handle partial blocks or return clear errors -**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. +This is a known limitation documented in the original C++ implementation. ## Prerequisites @@ -40,34 +68,83 @@ cargo install cargo-fuzz rustup install nightly ``` -## Running the Fuzzer +## Running the Fuzzers + +Run the oracle-based compression fuzzer (recommended): ```bash cd fuzz -cargo +nightly fuzz run fastpfor_rust +cargo +nightly fuzz run rust_compress_oracle +``` + +Run the oracle-based decompression fuzzer: + +```bash +cargo +nightly fuzz run rust_decompress_oracle +``` + +Run the C++ roundtrip fuzzer: + +```bash +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 +```bash +cargo +nightly fuzz run rust_compress_oracle -- -runs=1000 +``` -This ensures the codec is lossless and doesn't corrupt data under any input pattern. +## What the Oracle Fuzzers Test + +1. Generate random sequences of u32 integers +2. Randomly select block size (128 or 256) +3. Truncate input to block size multiple +4. **Compress:** Compare Rust vs C++ compressed outputs (byte-for-byte) +5. **Decompress:** Compare Rust vs C++ decompressed outputs (element-by-element) +6. Verify both implementations recover the original input + +This ensures the Rust implementation is **behaviorally equivalent** to the C++ reference. ## 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 ``` + +## Interpreting Failures + +### Compression Oracle Failure + +If `rust_compress_oracle` fails, it means: +- Rust and C++ produce different compressed output for the same input +- This is a **critical bug** - the implementations are not compatible +- Compressed data from Rust may not decompress correctly with C++ + +### Decompression Oracle Failure + +If `rust_decompress_oracle` fails, it means: +- Rust and C++ produce different output when decompressing the same data +- This is a **critical bug** - Rust cannot correctly read C++-compressed data +- Or both implementations fail to recover the original input + +### Roundtrip Failure + +If `cpp_roundtrip` fails, it means: +- The C++ reference implementation has a bug +- Or the FFI bindings are incorrect +- This should be rare as the C++ implementation is mature \ No newline at end of file diff --git a/fuzz/fuzz_targets/fastpfor_cpp.rs b/fuzz/fuzz_targets/cpp_roundtrip.rs similarity index 100% rename from fuzz/fuzz_targets/fastpfor_cpp.rs rename to fuzz/fuzz_targets/cpp_roundtrip.rs 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..d7fd676 --- /dev/null +++ b/fuzz/fuzz_targets/rust_compress_oracle.rs @@ -0,0 +1,93 @@ +#![no_main] + +use std::num::NonZeroU32; + +use fastpfor::cpp::{FastPFor128Codec, FastPFor256Codec}; +use fastpfor::rust::{Codec, FastPFOR, BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE}; +use fastpfor::CodecToSlice; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: FuzzInput| { + let input = data.data; + + // TODO: Behaviour differs + if input.is_empty() { + return; + } + + // TODO: To make the encoder not crash -> Skip inputs smaller than block size + let block_size = NonZeroU32::from(data.codec); + let bs = block_size.get() as usize; + if input.len() < bs { + return; + } + + // TODO: To make the encoder not crash -> Truncate to block size multiple + let last_block_size_multiple = input.len() / bs * bs; + 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 fastpfor = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size); + let mut rust_codec = Codec::from(fastpfor); + 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 { + FuzzCodec::FastPFOR256 => { + let mut cpp_codec = FastPFor256Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + FuzzCodec::FastPFOR128 => { + let mut cpp_codec = FastPFor128Codec::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 + ); + } +}); + +#[derive(arbitrary::Arbitrary, Debug)] +struct FuzzInput { + data: Vec, + codec: FuzzCodec, +} + +#[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_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs new file mode 100644 index 0000000..06f51c7 --- /dev/null +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -0,0 +1,91 @@ +#![no_main] + +use std::num::NonZeroU32; + +use fastpfor::cpp::{FastPFor128Codec, FastPFor256Codec}; +use fastpfor::rust::{Codec, FastPFOR, BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE}; +use fastpfor::CodecToSlice; +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: FuzzInput| { + let input = data.data; + + // TODO: Behaviour differs + if input.is_empty() { + return; + } + + // TODO: To make the decder not crash -> Skip inputs smaller than block size + let block_size = NonZeroU32::from(data.codec); + let bs = block_size.get() as usize; + if input.len() < bs { + return; + } + + // TODO: To make the decder not crash -> Truncate to block size multiple + let last_block_size_multiple = input.len() / bs * bs; + 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 { + FuzzCodec::FastPFOR256 => { + let mut cpp_codec = FastPFor256Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + FuzzCodec::FastPFOR128 => { + let mut cpp_codec = FastPFor128Codec::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 fastpfor = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size); + let mut rust_codec = Codec::from(fastpfor); + 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(), + cpp_result.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 + ); + } +}); + +#[derive(arbitrary::Arbitrary, Debug)] +struct FuzzInput { + data: Vec, + codec: FuzzCodec, +} + +#[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, + } + } +} From 0b7f0a9515fd66cca4514bbfdba64252e80e50be Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:22:15 +0100 Subject: [PATCH 07/15] make fuzzing more actionable --- fuzz/fuzz_targets/rust_compress_oracle.rs | 58 ++++++++++++++----- fuzz/fuzz_targets/rust_decompress_oracle.rs | 64 +++++++++++++++------ 2 files changed, 89 insertions(+), 33 deletions(-) diff --git a/fuzz/fuzz_targets/rust_compress_oracle.rs b/fuzz/fuzz_targets/rust_compress_oracle.rs index d7fd676..0026c92 100644 --- a/fuzz/fuzz_targets/rust_compress_oracle.rs +++ b/fuzz/fuzz_targets/rust_compress_oracle.rs @@ -1,9 +1,7 @@ #![no_main] -use std::num::NonZeroU32; - -use fastpfor::cpp::{FastPFor128Codec, FastPFor256Codec}; -use fastpfor::rust::{Codec, FastPFOR, BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE}; +use fastpfor::cpp; +use fastpfor::rust; use fastpfor::CodecToSlice; use libfuzzer_sys::fuzz_target; @@ -15,15 +13,24 @@ fuzz_target!(|data: FuzzInput| { return; } + // TODO: Behaviour differs + if data.codec == FuzzCodec::VariableByte { + return; + } + // TODO: To make the encoder not crash -> Skip inputs smaller than block size - let block_size = NonZeroU32::from(data.codec); - let bs = block_size.get() as usize; - if input.len() < bs { + let block_size = match data.codec { + FuzzCodec::FastPFOR256 => 256, + FuzzCodec::FastPFOR128 => 128, + FuzzCodec::VariableByte => 1, + FuzzCodec::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() / bs * bs; + let last_block_size_multiple = input.len() / block_size * block_size; let input = &input[..last_block_size_multiple]; // Allocate output buffers with generous size @@ -31,8 +38,7 @@ fuzz_target!(|data: FuzzInput| { let mut cpp_compressed = vec![0u32; input.len() * 2 + 1024]; // Compress with Rust implementation using Codec wrapper - let fastpfor = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size); - let mut rust_codec = Codec::from(fastpfor); + 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"); @@ -40,13 +46,25 @@ fuzz_target!(|data: FuzzInput| { // Compress with C++ implementation let cpp_result = match data.codec { FuzzCodec::FastPFOR256 => { - let mut cpp_codec = FastPFor256Codec::new(); + let mut cpp_codec = cpp::FastPFor256Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } FuzzCodec::FastPFOR128 => { - let mut cpp_codec = FastPFor128Codec::new(); + let mut cpp_codec = cpp::FastPFor128Codec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + FuzzCodec::VariableByte => { + let mut cpp_codec = cpp::VByteCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + FuzzCodec::JustCopy => { + let mut cpp_codec = cpp::CopyCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") @@ -81,13 +99,23 @@ struct FuzzInput { enum FuzzCodec { FastPFOR256, FastPFOR128, + VariableByte, + JustCopy, } -impl From for NonZeroU32 { +impl From for rust::Codec { fn from(codec: FuzzCodec) -> Self { match codec { - FuzzCodec::FastPFOR256 => BLOCK_SIZE_256, - FuzzCodec::FastPFOR128 => BLOCK_SIZE_128, + FuzzCodec::FastPFOR256 => rust::Codec::from(rust::FastPFOR::new( + rust::DEFAULT_PAGE_SIZE, + rust::BLOCK_SIZE_256, + )), + FuzzCodec::FastPFOR128 => rust::Codec::from(rust::FastPFOR::new( + rust::DEFAULT_PAGE_SIZE, + rust::BLOCK_SIZE_128, + )), + FuzzCodec::VariableByte => rust::Codec::from(rust::VariableByte::new()), + FuzzCodec::JustCopy => rust::Codec::from(rust::JustCopy::new()), } } } diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs index 06f51c7..52a746e 100644 --- a/fuzz/fuzz_targets/rust_decompress_oracle.rs +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -1,9 +1,7 @@ #![no_main] -use std::num::NonZeroU32; - -use fastpfor::cpp::{FastPFor128Codec, FastPFor256Codec}; -use fastpfor::rust::{Codec, FastPFOR, BLOCK_SIZE_128, BLOCK_SIZE_256, DEFAULT_PAGE_SIZE}; +use fastpfor::cpp; +use fastpfor::rust; use fastpfor::CodecToSlice; use libfuzzer_sys::fuzz_target; @@ -15,38 +13,58 @@ fuzz_target!(|data: FuzzInput| { return; } - // TODO: To make the decder not crash -> Skip inputs smaller than block size - let block_size = NonZeroU32::from(data.codec); - let bs = block_size.get() as usize; - if input.len() < bs { + // TODO: Behaviour differs + if data.codec == FuzzCodec::VariableByte { return; } - // TODO: To make the decder not crash -> Truncate to block size multiple - let last_block_size_multiple = input.len() / bs * bs; + // TODO: To make the decoder not crash -> Skip inputs smaller than block size + let block_size = match data.codec { + FuzzCodec::FastPFOR256 => 256, + FuzzCodec::FastPFOR128 => 128, + FuzzCodec::VariableByte => 1, + FuzzCodec::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 { FuzzCodec::FastPFOR256 => { - let mut cpp_codec = FastPFor256Codec::new(); + let mut cpp_codec = cpp::FastPFor256Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } FuzzCodec::FastPFOR128 => { - let mut cpp_codec = FastPFor128Codec::new(); + let mut cpp_codec = cpp::FastPFor128Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } + FuzzCodec::VariableByte => { + let mut cpp_codec = cpp::VByteCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("C++ compression failed") + } + FuzzCodec::JustCopy => { + let mut cpp_codec = cpp::CopyCodec::new(); + cpp_codec + .compress_to_slice(input, &mut cpp_compressed) + .expect("Rust compression failed") + } }; // Now decompress with rust let mut rust_decompressed = vec![0u32; input.len()]; - let fastpfor = FastPFOR::new(DEFAULT_PAGE_SIZE, block_size); - let mut rust_codec = Codec::from(fastpfor); + 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"); @@ -57,7 +75,7 @@ fuzz_target!(|data: FuzzInput| { input.len(), "Decompressed length mismatch: Rust={}, C++={}", rust_result.len(), - cpp_result.len() + input.len() ); for (i, (&rust_val, &cpp_val)) in rust_result.iter().zip(input.iter()).enumerate() { @@ -79,13 +97,23 @@ struct FuzzInput { enum FuzzCodec { FastPFOR256, FastPFOR128, + VariableByte, + JustCopy, } -impl From for NonZeroU32 { +impl From for rust::Codec { fn from(codec: FuzzCodec) -> Self { match codec { - FuzzCodec::FastPFOR256 => BLOCK_SIZE_256, - FuzzCodec::FastPFOR128 => BLOCK_SIZE_128, + FuzzCodec::FastPFOR256 => rust::Codec::from(rust::FastPFOR::new( + rust::DEFAULT_PAGE_SIZE, + rust::BLOCK_SIZE_256, + )), + FuzzCodec::FastPFOR128 => rust::Codec::from(rust::FastPFOR::new( + rust::DEFAULT_PAGE_SIZE, + rust::BLOCK_SIZE_128, + )), + FuzzCodec::VariableByte => rust::Codec::from(rust::VariableByte::new()), + FuzzCodec::JustCopy => rust::Codec::from(rust::JustCopy::new()), } } } From 8995af0ed0155069fef42ff95a9fba9c73d53857 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:27:24 +0100 Subject: [PATCH 08/15] reduce some slop --- fuzz/README.md | 97 +++----------------------------------------------- 1 file changed, 4 insertions(+), 93 deletions(-) diff --git a/fuzz/README.md b/fuzz/README.md index 2647d79..03a678f 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -2,42 +2,6 @@ This directory contains fuzz tests for the FastPFOR compression codec to find bugs, panics, and data corruption issues. -## Fuzz Targets - -### Oracle-Based Fuzzers (Recommended) - -These fuzzers compare the Rust implementation against the C++ reference implementation to find behavioral differences. - -**`rust_compress_oracle`** - Compares Rust and C++ compression outputs -- Generates random input data -- Compresses with both Rust and C++ implementations -- Verifies both produce **identical compressed output** -- Finds discrepancies in compression behavior between implementations - -**`rust_decompress_oracle`** - Compares Rust and C++ decompression outputs -- Generates random input data -- Compresses with C++ implementation (known good) -- Decompresses with both Rust and C++ implementations -- Verifies both produce **identical decompressed output** -- Ensures Rust can decompress C++-compressed data correctly -- Validates both implementations recover the original input - -### Roundtrip Fuzzer - -**`cpp_roundtrip`** - Tests C++ implementation roundtrip -- Compress → decompress → verify cycle -- Validates the reference C++ implementation -- Finds data corruption, crashes, and panics in the C++ FFI - -## Why Oracle-Based Fuzzing? - -Oracle fuzzing is the gold standard for verifying a reimplementation: - -1. **Detects behavioral differences** - The Rust implementation should produce byte-for-byte identical output to the C++ reference -2. **Catches subtle bugs** - Even if both implementations round-trip correctly in isolation, they might compress differently -3. **Validates compatibility** - Ensures Rust can decompress C++ data and vice versa -4. **No false positives** - If outputs differ, it's a real bug (not just random data causing a crash) - ## Why Fuzz FastPFOR? The FastPFOR codec is a core compression algorithm. Fuzzing helps catch: @@ -48,17 +12,6 @@ The FastPFOR codec is a core compression algorithm. Fuzzing helps catch: - Incorrect handling of different block sizes (128 vs 256) - Issues with boundary conditions (empty data, very large values, etc.) -## Known Limitations - -### Block Size Requirements - -FastPFOR requires input lengths to be multiples of the block size (128 or 256). The fuzzers currently skip inputs that don't meet this requirement. Both implementations: -- Silently process only complete blocks -- Drop data that doesn't fit into complete blocks -- Should either handle partial blocks or return clear errors - -This is a known limitation documented in the original C++ implementation. - ## Prerequisites Install cargo-fuzz and switch to nightly Rust: @@ -70,23 +23,15 @@ rustup install nightly ## Running the Fuzzers -Run the oracle-based compression fuzzer (recommended): +Run the oracle-based compression fuzzer: ```bash cd fuzz cargo +nightly fuzz run rust_compress_oracle -``` - -Run the oracle-based decompression fuzzer: - -```bash +# or cargo +nightly fuzz run rust_decompress_oracle -``` - -Run the C++ roundtrip fuzzer: - -```bash -cargo +nightly fuzz run cpp_roundtrip +# or +cargo +nightly fuzz run rust_roundtrip_oracle ``` Run for a specific duration (e.g., 60 seconds): @@ -101,17 +46,6 @@ Run with specific number of iterations: cargo +nightly fuzz run rust_compress_oracle -- -runs=1000 ``` -## What the Oracle Fuzzers Test - -1. Generate random sequences of u32 integers -2. Randomly select block size (128 or 256) -3. Truncate input to block size multiple -4. **Compress:** Compare Rust vs C++ compressed outputs (byte-for-byte) -5. **Decompress:** Compare Rust vs C++ decompressed outputs (element-by-element) -6. Verify both implementations recover the original input - -This ensures the Rust implementation is **behaviorally equivalent** to the C++ reference. - ## If a Crash Is Found Crashes are saved to `fuzz/artifacts//`. To reproduce: @@ -125,26 +59,3 @@ For example: ```bash cargo +nightly fuzz run rust_compress_oracle fuzz/artifacts/rust_compress_oracle/crash-abc123 ``` - -## Interpreting Failures - -### Compression Oracle Failure - -If `rust_compress_oracle` fails, it means: -- Rust and C++ produce different compressed output for the same input -- This is a **critical bug** - the implementations are not compatible -- Compressed data from Rust may not decompress correctly with C++ - -### Decompression Oracle Failure - -If `rust_decompress_oracle` fails, it means: -- Rust and C++ produce different output when decompressing the same data -- This is a **critical bug** - Rust cannot correctly read C++-compressed data -- Or both implementations fail to recover the original input - -### Roundtrip Failure - -If `cpp_roundtrip` fails, it means: -- The C++ reference implementation has a bug -- Or the FFI bindings are incorrect -- This should be rare as the C++ implementation is mature \ No newline at end of file From 367f12f9718837954a4ceaad312d133cff45d4b5 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:27:35 +0100 Subject: [PATCH 09/15] fix fmt --- fuzz/fuzz_targets/rust_compress_oracle.rs | 4 +--- fuzz/fuzz_targets/rust_decompress_oracle.rs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/fuzz/fuzz_targets/rust_compress_oracle.rs b/fuzz/fuzz_targets/rust_compress_oracle.rs index 0026c92..58091df 100644 --- a/fuzz/fuzz_targets/rust_compress_oracle.rs +++ b/fuzz/fuzz_targets/rust_compress_oracle.rs @@ -1,8 +1,6 @@ #![no_main] -use fastpfor::cpp; -use fastpfor::rust; -use fastpfor::CodecToSlice; +use fastpfor::{CodecToSlice, cpp, rust}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: FuzzInput| { diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs index 52a746e..b14c36c 100644 --- a/fuzz/fuzz_targets/rust_decompress_oracle.rs +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -1,8 +1,6 @@ #![no_main] -use fastpfor::cpp; -use fastpfor::rust; -use fastpfor::CodecToSlice; +use fastpfor::{CodecToSlice, cpp, rust}; use libfuzzer_sys::fuzz_target; fuzz_target!(|data: FuzzInput| { From 8fd5a9914d3dacb055381104dc810bad69fa12e7 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:29:58 +0100 Subject: [PATCH 10/15] fix clippy --- fuzz/fuzz_targets/cpp_roundtrip.rs | 138 ++++++++++++++--------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/fuzz/fuzz_targets/cpp_roundtrip.rs b/fuzz/fuzz_targets/cpp_roundtrip.rs index f74d936..e0b11ce 100644 --- a/fuzz/fuzz_targets/cpp_roundtrip.rs +++ b/fuzz/fuzz_targets/cpp_roundtrip.rs @@ -17,7 +17,7 @@ fuzz_target!(|data: FuzzInput| { // Now decompress let mut decoded = vec![0u32; input.len() * 2 + 1024]; - let dec_slice = codec.decode32(&enc_slice, &mut decoded).unwrap(); + let dec_slice = codec.decode32(enc_slice, &mut decoded).unwrap(); // Verify roundtrip if dec_slice.len() + input.len() < 200 { @@ -51,81 +51,81 @@ impl std::fmt::Debug for FuzzInput { #[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 + 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 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 => { + FuzzCodec::BP32 => Box::new(BP32Codec::default()), + FuzzCodec::Copy => Box::new(CopyCodec::default()), + FuzzCodec::FastBinaryPacking8 => Box::new(FastBinaryPacking8Codec::default()), + FuzzCodec::FastPFor128 => Box::new(FastPFor128Codec::default()), + FuzzCodec::FastPFor256 => Box::new(FastPFor256Codec::default()), + FuzzCodec::FastBinaryPacking16 => Box::new(FastBinaryPacking16Codec::default()), + FuzzCodec::FastBinaryPacking32 => Box::new(FastBinaryPacking32Codec::default()), + FuzzCodec::MaskedVByte => Box::new(MaskedVByteCodec::default()), + FuzzCodec::NewPFor => Box::new(NewPForCodec::default()), + FuzzCodec::OptPFor => Box::new(OptPForCodec::default()), + FuzzCodec::PFor2008 => Box::new(PFor2008Codec::default()), + FuzzCodec::PFor => Box::new(PForCodec::default()), + FuzzCodec::SimdBinaryPacking => Box::new(SimdBinaryPackingCodec::default()), + FuzzCodec::SimdFastPFor128 => Box::new(SimdFastPFor128Codec::default()), + FuzzCodec::SimdFastPFor256 => Box::new(SimdFastPFor256Codec::default()), + FuzzCodec::SimdGroupSimple => Box::new(SimdGroupSimpleCodec::default()), + FuzzCodec::SimdGroupSimpleRingBuf => { 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()), + FuzzCodec::SimdNewPFor => Box::new(SimdNewPForCodec::default()), + FuzzCodec::SimdOptPFor => Box::new(SimdOptPForCodec::default()), + FuzzCodec::SimdPFor => Box::new(SimdPForCodec::default()), + FuzzCodec::SimdSimplePFor => Box::new(SimdSimplePForCodec::default()), + // FuzzCodec::Simple16 => Box::new(Simple16Codec::default()), + // FuzzCodec::Simple8b => Box::new(Simple8bCodec::default()), + // FuzzCodec::Simple8bRle => Box::new(Simple8bRleCodec::default()), + // FuzzCodec::Simple9 => Box::new(Simple9Codec::default()), + // FuzzCodec::Simple9Rle => Box::new(Simple9RleCodec::default()), + // FuzzCodec::SimplePFor => Box::new(SimplePForCodec::default()), + // FuzzCodec::Snappy => Box::new(SnappyCodec::default()), + FuzzCodec::StreamVByte => Box::new(StreamVByteCodec::default()), + FuzzCodec::VByte => Box::new(VByteCodec::default()), + FuzzCodec::VarInt => Box::new(VarIntCodec::default()), + // FuzzCodec::VarIntG8iu => Box::new(VarIntG8iuCodec::default()), + FuzzCodec::VarIntGb => Box::new(VarIntGbCodec::default()), + // FuzzCodec::VsEncoding => Box::new(VsEncodingCodec::default()), } } } From 0dee22994fae6a677af8f14e9d93e83df6cbd4fa Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:34:23 +0100 Subject: [PATCH 11/15] Update fuzz/fuzz_targets/cpp_roundtrip.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- fuzz/fuzz_targets/cpp_roundtrip.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/fuzz/fuzz_targets/cpp_roundtrip.rs b/fuzz/fuzz_targets/cpp_roundtrip.rs index e0b11ce..b649582 100644 --- a/fuzz/fuzz_targets/cpp_roundtrip.rs +++ b/fuzz/fuzz_targets/cpp_roundtrip.rs @@ -31,7 +31,6 @@ fuzz_target!(|data: FuzzInput| { ); } } - assert_eq!(dec_slice.len(), input.len()); }); #[derive(arbitrary::Arbitrary)] From 63a280c7f665dfabe9c79ce1c0d742b5a60ccf07 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:35:34 +0100 Subject: [PATCH 12/15] Update fuzz/fuzz_targets/rust_decompress_oracle.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- fuzz/fuzz_targets/rust_decompress_oracle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs index b14c36c..c6f97e0 100644 --- a/fuzz/fuzz_targets/rust_decompress_oracle.rs +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -56,7 +56,7 @@ fuzz_target!(|data: FuzzInput| { let mut cpp_codec = cpp::CopyCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) - .expect("Rust compression failed") + .expect("C++ compression failed") } }; From ad0e8edeae165370b15ad70fd9066bde2215fbfb Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:49:52 +0100 Subject: [PATCH 13/15] deduplicate some logic --- fuzz/fuzz_targets/common.rs | 119 ++++++++++++++++++++ fuzz/fuzz_targets/cpp_roundtrip.rs | 105 +---------------- fuzz/fuzz_targets/rust_compress_oracle.rs | 53 ++------- fuzz/fuzz_targets/rust_decompress_oracle.rs | 55 +++------ 4 files changed, 148 insertions(+), 184 deletions(-) create mode 100644 fuzz/fuzz_targets/common.rs 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 index b649582..8fd4052 100644 --- a/fuzz/fuzz_targets/cpp_roundtrip.rs +++ b/fuzz/fuzz_targets/cpp_roundtrip.rs @@ -1,12 +1,11 @@ #![no_main] -use fastpfor::cpp::*; use libfuzzer_sys::fuzz_target; +mod common; +use common::*; -type BoxedCodec = Box; - -fuzz_target!(|data: FuzzInput| { - let codec = BoxedCodec::from(data.codec); +fuzz_target!(|data: FuzzInput| { + let codec = BoxedCppCodec::from(data.codec); let input = data.data; // Allocate output buffer with generous size @@ -32,99 +31,3 @@ fuzz_target!(|data: FuzzInput| { } } }); - -#[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 { - 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 BoxedCodec { - fn from(codec: FuzzCodec) -> Self { - match codec { - FuzzCodec::BP32 => Box::new(BP32Codec::default()), - FuzzCodec::Copy => Box::new(CopyCodec::default()), - FuzzCodec::FastBinaryPacking8 => Box::new(FastBinaryPacking8Codec::default()), - FuzzCodec::FastPFor128 => Box::new(FastPFor128Codec::default()), - FuzzCodec::FastPFor256 => Box::new(FastPFor256Codec::default()), - FuzzCodec::FastBinaryPacking16 => Box::new(FastBinaryPacking16Codec::default()), - FuzzCodec::FastBinaryPacking32 => Box::new(FastBinaryPacking32Codec::default()), - FuzzCodec::MaskedVByte => Box::new(MaskedVByteCodec::default()), - FuzzCodec::NewPFor => Box::new(NewPForCodec::default()), - FuzzCodec::OptPFor => Box::new(OptPForCodec::default()), - FuzzCodec::PFor2008 => Box::new(PFor2008Codec::default()), - FuzzCodec::PFor => Box::new(PForCodec::default()), - FuzzCodec::SimdBinaryPacking => Box::new(SimdBinaryPackingCodec::default()), - FuzzCodec::SimdFastPFor128 => Box::new(SimdFastPFor128Codec::default()), - FuzzCodec::SimdFastPFor256 => Box::new(SimdFastPFor256Codec::default()), - FuzzCodec::SimdGroupSimple => Box::new(SimdGroupSimpleCodec::default()), - FuzzCodec::SimdGroupSimpleRingBuf => { - Box::new(SimdGroupSimpleRingBufCodec::default()) - } - FuzzCodec::SimdNewPFor => Box::new(SimdNewPForCodec::default()), - FuzzCodec::SimdOptPFor => Box::new(SimdOptPForCodec::default()), - FuzzCodec::SimdPFor => Box::new(SimdPForCodec::default()), - FuzzCodec::SimdSimplePFor => Box::new(SimdSimplePForCodec::default()), - // FuzzCodec::Simple16 => Box::new(Simple16Codec::default()), - // FuzzCodec::Simple8b => Box::new(Simple8bCodec::default()), - // FuzzCodec::Simple8bRle => Box::new(Simple8bRleCodec::default()), - // FuzzCodec::Simple9 => Box::new(Simple9Codec::default()), - // FuzzCodec::Simple9Rle => Box::new(Simple9RleCodec::default()), - // FuzzCodec::SimplePFor => Box::new(SimplePForCodec::default()), - // FuzzCodec::Snappy => Box::new(SnappyCodec::default()), - FuzzCodec::StreamVByte => Box::new(StreamVByteCodec::default()), - FuzzCodec::VByte => Box::new(VByteCodec::default()), - FuzzCodec::VarInt => Box::new(VarIntCodec::default()), - // FuzzCodec::VarIntG8iu => Box::new(VarIntG8iuCodec::default()), - FuzzCodec::VarIntGb => Box::new(VarIntGbCodec::default()), - // FuzzCodec::VsEncoding => Box::new(VsEncodingCodec::default()), - } - } -} diff --git a/fuzz/fuzz_targets/rust_compress_oracle.rs b/fuzz/fuzz_targets/rust_compress_oracle.rs index 58091df..6ed1f1b 100644 --- a/fuzz/fuzz_targets/rust_compress_oracle.rs +++ b/fuzz/fuzz_targets/rust_compress_oracle.rs @@ -2,8 +2,10 @@ use fastpfor::{CodecToSlice, cpp, rust}; use libfuzzer_sys::fuzz_target; +mod common; +use common::*; -fuzz_target!(|data: FuzzInput| { +fuzz_target!(|data: FuzzInput| { let input = data.data; // TODO: Behaviour differs @@ -12,16 +14,16 @@ fuzz_target!(|data: FuzzInput| { } // TODO: Behaviour differs - if data.codec == FuzzCodec::VariableByte { + 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 { - FuzzCodec::FastPFOR256 => 256, - FuzzCodec::FastPFOR128 => 128, - FuzzCodec::VariableByte => 1, - FuzzCodec::JustCopy => 1, + RustCodec::FastPFOR256 => 256, + RustCodec::FastPFOR128 => 128, + RustCodec::VariableByte => 1, + RustCodec::JustCopy => 1, }; if input.len() < block_size { return; @@ -43,25 +45,25 @@ fuzz_target!(|data: FuzzInput| { // Compress with C++ implementation let cpp_result = match data.codec { - FuzzCodec::FastPFOR256 => { + RustCodec::FastPFOR256 => { let mut cpp_codec = cpp::FastPFor256Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::FastPFOR128 => { + RustCodec::FastPFOR128 => { let mut cpp_codec = cpp::FastPFor128Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::VariableByte => { + RustCodec::VariableByte => { let mut cpp_codec = cpp::VByteCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::JustCopy => { + RustCodec::JustCopy => { let mut cpp_codec = cpp::CopyCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) @@ -86,34 +88,3 @@ fuzz_target!(|data: FuzzInput| { ); } }); - -#[derive(arbitrary::Arbitrary, Debug)] -struct FuzzInput { - data: Vec, - codec: FuzzCodec, -} - -#[derive(arbitrary::Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] -enum FuzzCodec { - FastPFOR256, - FastPFOR128, - VariableByte, - JustCopy, -} - -impl From for rust::Codec { - fn from(codec: FuzzCodec) -> Self { - match codec { - FuzzCodec::FastPFOR256 => rust::Codec::from(rust::FastPFOR::new( - rust::DEFAULT_PAGE_SIZE, - rust::BLOCK_SIZE_256, - )), - FuzzCodec::FastPFOR128 => rust::Codec::from(rust::FastPFOR::new( - rust::DEFAULT_PAGE_SIZE, - rust::BLOCK_SIZE_128, - )), - FuzzCodec::VariableByte => rust::Codec::from(rust::VariableByte::new()), - FuzzCodec::JustCopy => rust::Codec::from(rust::JustCopy::new()), - } - } -} diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs index c6f97e0..651e241 100644 --- a/fuzz/fuzz_targets/rust_decompress_oracle.rs +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -2,8 +2,10 @@ use fastpfor::{CodecToSlice, cpp, rust}; use libfuzzer_sys::fuzz_target; +mod common; +use common::*; -fuzz_target!(|data: FuzzInput| { +fuzz_target!(|data: FuzzInput| { let input = data.data; // TODO: Behaviour differs @@ -12,16 +14,16 @@ fuzz_target!(|data: FuzzInput| { } // TODO: Behaviour differs - if data.codec == FuzzCodec::VariableByte { + 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 { - FuzzCodec::FastPFOR256 => 256, - FuzzCodec::FastPFOR128 => 128, - FuzzCodec::VariableByte => 1, - FuzzCodec::JustCopy => 1, + RustCodec::FastPFOR256 => 256, + RustCodec::FastPFOR128 => 128, + RustCodec::VariableByte => 1, + RustCodec::JustCopy => 1, }; if input.len() < block_size { return; @@ -34,25 +36,25 @@ fuzz_target!(|data: FuzzInput| { // 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 { - FuzzCodec::FastPFOR256 => { + RustCodec::FastPFOR256 => { let mut cpp_codec = cpp::FastPFor256Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::FastPFOR128 => { + RustCodec::FastPFOR128 => { let mut cpp_codec = cpp::FastPFor128Codec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::VariableByte => { + RustCodec::VariableByte => { let mut cpp_codec = cpp::VByteCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) .expect("C++ compression failed") } - FuzzCodec::JustCopy => { + RustCodec::JustCopy => { let mut cpp_codec = cpp::CopyCodec::new(); cpp_codec .compress_to_slice(input, &mut cpp_compressed) @@ -83,35 +85,4 @@ fuzz_target!(|data: FuzzInput| { i, rust_val, cpp_val ); } -}); - -#[derive(arbitrary::Arbitrary, Debug)] -struct FuzzInput { - data: Vec, - codec: FuzzCodec, -} - -#[derive(arbitrary::Arbitrary, Debug, Clone, Copy, PartialEq, Eq)] -enum FuzzCodec { - FastPFOR256, - FastPFOR128, - VariableByte, - JustCopy, -} - -impl From for rust::Codec { - fn from(codec: FuzzCodec) -> Self { - match codec { - FuzzCodec::FastPFOR256 => rust::Codec::from(rust::FastPFOR::new( - rust::DEFAULT_PAGE_SIZE, - rust::BLOCK_SIZE_256, - )), - FuzzCodec::FastPFOR128 => rust::Codec::from(rust::FastPFOR::new( - rust::DEFAULT_PAGE_SIZE, - rust::BLOCK_SIZE_128, - )), - FuzzCodec::VariableByte => rust::Codec::from(rust::VariableByte::new()), - FuzzCodec::JustCopy => rust::Codec::from(rust::JustCopy::new()), - } - } -} +}); \ No newline at end of file From 7e55018f3ed2dc501fcd66cfa28c339518fbdbc8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 15:50:44 +0000 Subject: [PATCH 14/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- fuzz/fuzz_targets/rust_decompress_oracle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/fuzz_targets/rust_decompress_oracle.rs b/fuzz/fuzz_targets/rust_decompress_oracle.rs index 651e241..1ccf20f 100644 --- a/fuzz/fuzz_targets/rust_decompress_oracle.rs +++ b/fuzz/fuzz_targets/rust_decompress_oracle.rs @@ -85,4 +85,4 @@ fuzz_target!(|data: FuzzInput| { i, rust_val, cpp_val ); } -}); \ No newline at end of file +}); From b7b5011d65a75d0be7cf9cd8d9a273dafa05eba1 Mon Sep 17 00:00:00 2001 From: Frank Elsinga Date: Sat, 14 Feb 2026 16:56:48 +0100 Subject: [PATCH 15/15] Update fuzz/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- fuzz/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fuzz/README.md b/fuzz/README.md index 03a678f..4233efd 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -31,7 +31,7 @@ cargo +nightly fuzz run rust_compress_oracle # or cargo +nightly fuzz run rust_decompress_oracle # or -cargo +nightly fuzz run rust_roundtrip_oracle +cargo +nightly fuzz run cpp_roundtrip ``` Run for a specific duration (e.g., 60 seconds):