Skip to content

Commit e1148f2

Browse files
committed
chore: stable
1 parent e65e6cd commit e1148f2

24 files changed

Lines changed: 799 additions & 716 deletions

README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ let mut encoded = Vec::new();
3232
codec.encode(&input, &mut encoded).unwrap();
3333

3434
let mut decoded = Vec::new();
35-
codec.decode(&encoded, &mut decoded).unwrap();
35+
codec.decode(&encoded, &mut decoded, None).unwrap();
3636

3737
assert_eq!(decoded, input);
3838
```
@@ -103,14 +103,13 @@ Rust block codecs require block-aligned input. `CompositeCodec` chains a block c
103103

104104
### C++ (`cpp` feature)
105105

106-
All C++ codecs implement `AnyLenCodec` for `&[u32]`.
107-
Block-oriented codecs (`CppFastPFor128`, `CppFastPFor256`, `CppSimdFastPFor128`, `CppSimdFastPFor256`) implement both `BlockCodec` and `AnyLenCodec`; the C++ library exposes them as composites. Use them directly for arbitrary-length input.
108-
`u64`-capable codecs implement `BlockCodec64` with `encode64` / `decode64`.
106+
All C++ codecs are composite (any-length) and implement `AnyLenCodec` only.
107+
`u64`-capable codecs (`CppFastPFor128`, `CppFastPFor256`, `CppVarInt`) also implement `BlockCodec64` with `encode64` / `decode64`.
109108

110109
| Codec | Notes |
111110
|-----------------------------|------------------------------------------------------------------------|
112-
| `CppFastPFor128` | `FastPFor + VByte` composite, 128-element blocks. Also supports `u64`. |
113-
| `CppFastPFor256` | `FastPFor + VByte` composite, 256-element blocks. Also supports `u64`. |
111+
| `CppFastPFor128` | `FastPFor + VByte` composite, 128-element blocks. Also supports `u64`. |
112+
| `CppFastPFor256` | `FastPFor + VByte` composite, 256-element blocks. Also supports `u64`. |
114113
| `CppSimdFastPFor128` | SIMD-optimized 128-element variant |
115114
| `CppSimdFastPFor256` | SIMD-optimized 256-element variant |
116115
| `CppBP32` | Binary packing, 32-bit blocks |

benches/bench_utils.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
use core::ops::Range;
1313

1414
#[allow(unused_imports)]
15-
use fastpfor::{AnyLenCodec, BlockCodec, slice_to_blocks};
15+
use fastpfor::{AnyLenCodec, AsU32, BlockCodec, slice_to_blocks};
1616
use rand::rngs::StdRng;
1717
use rand::{RngExt as _, SeedableRng};
1818

@@ -138,6 +138,21 @@ pub fn decompress<C: BlockCodec + Default>(
138138
out.len()
139139
}
140140

141+
/// Decompress with any-length codec `C`, using `expected_len` for validation/pre-allocation.
142+
#[allow(dead_code)] // used by smoke_cpp_vs_rust
143+
pub fn decompress_anylen<C: AnyLenCodec + Default>(
144+
compressed: &[u32],
145+
expected_len: usize,
146+
out: &mut Vec<u32>,
147+
) -> usize {
148+
let mut codec = C::default();
149+
out.clear();
150+
codec
151+
.decode(compressed, out, Some(expected_len.as_u32()))
152+
.unwrap();
153+
out.len()
154+
}
155+
141156
// ---------------------------------------------------------------------------
142157
// Pre-computed fixtures
143158
// ---------------------------------------------------------------------------

benches/fastpfor_benchmark.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
33
use std::hint::black_box;
44

5-
#[cfg(feature = "cpp")]
6-
use bytemuck::cast_slice;
75
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
6+
#[cfg(feature = "cpp")]
7+
use fastpfor::{AnyLenCodec, AsU32};
88
use fastpfor::{BlockCodec as _, FastPForBlock128, FastPForBlock256, slice_to_blocks};
99

1010
#[path = "bench_utils.rs"]
@@ -186,8 +186,8 @@ fn benchmark_compression_ratio(c: &mut Criterion) {
186186
group.finish();
187187
}
188188

189-
/// Compare encoding and decoding speed of the C++ `FastPForBlock128` codec against
190-
/// the pure-Rust `FastPForBlock128` codec.
189+
/// Compare encoding and decoding speed of the C++ `CppFastPFor128` (`AnyLenCodec`) against
190+
/// the pure-Rust `FastPForBlock128` (`BlockCodec`). Same wire format for block-aligned data.
191191
#[cfg(feature = "cpp")]
192192
fn benchmark_cpp_vs_rust(c: &mut Criterion) {
193193
let mut group = c.benchmark_group("cpp_vs_rust/encode");
@@ -199,11 +199,10 @@ fn benchmark_cpp_vs_rust(c: &mut Criterion) {
199199
&fix.data,
200200
|b, data| {
201201
let mut codec = CppFastPFor128::default();
202-
let blocks: &[[u32; 128]] = cast_slice(data);
203202
let mut out = Vec::new();
204203
b.iter(|| {
205204
out.clear();
206-
codec.encode_blocks(black_box(blocks), &mut out).unwrap();
205+
codec.encode(black_box(data), &mut out).unwrap();
207206
black_box(out.len())
208207
});
209208
},
@@ -213,7 +212,7 @@ fn benchmark_cpp_vs_rust(c: &mut Criterion) {
213212
&fix.data,
214213
|b, data| {
215214
let mut codec = FastPForBlock128::default();
216-
let blocks: &[[u32; 128]] = cast_slice(data);
215+
let (blocks, _) = slice_to_blocks::<FastPForBlock128>(data);
217216
let mut out = Vec::new();
218217
b.iter(|| {
219218
out.clear();
@@ -228,6 +227,7 @@ fn benchmark_cpp_vs_rust(c: &mut Criterion) {
228227
let mut group = c.benchmark_group("cpp_vs_rust/decode");
229228
for (bc, fix) in compress_fixtures::<FastPForBlock128>(BLOCK_COUNTS) {
230229
let n_elem = fix.n_blocks * FastPForBlock128::values_per_block();
230+
let expected_len = n_elem.as_u32();
231231
group.throughput(Throughput::Elements(n_elem as u64));
232232
group.bench_with_input(
233233
BenchmarkId::new(format!("cpp/{}", fix.name), bc),
@@ -238,7 +238,7 @@ fn benchmark_cpp_vs_rust(c: &mut Criterion) {
238238
b.iter(|| {
239239
out.clear();
240240
codec
241-
.decode_blocks(black_box(compressed), fix.n_blocks, &mut out)
241+
.decode(black_box(compressed), &mut out, Some(expected_len))
242242
.unwrap();
243243
black_box(out.len())
244244
});

fuzz/fuzz_targets/common.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,7 @@ macro_rules! codec_list {
4949
}
5050

5151
/// Rust codecs. Block codecs are wrapped in `CompositeCodec<_, VariableByte>`.
52-
pub static RUST: &[CodecEntry] = codec_list!(
53-
FastPFor256,
54-
FastPFor128,
55-
VariableByte,
56-
JustCopy,
57-
);
52+
pub static RUST: &[CodecEntry] = codec_list!(FastPFor256, FastPFor128, VariableByte, JustCopy,);
5853

5954
/// C++ codecs (any-length; block codecs are already composites in the C++ library).
6055
pub static CPP: &[CodecEntry] = codec_list!(

fuzz/fuzz_targets/cpp_roundtrip.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ fuzz_target!(|data: FuzzInput<AnyLenSelector>| {
2121

2222
let mut decompressed = Vec::new();
2323
codec
24-
.decode(&compressed, &mut decompressed)
24+
.decode(&compressed, &mut decompressed, None)
2525
.expect("C++ decompression failed");
2626

2727
assert_eq!(

fuzz/fuzz_targets/decode_arbitrary.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,5 +57,5 @@ fuzz_target!(|data: FuzzInput| {
5757

5858
// The decoder must either succeed or return an error — a panic is a bug.
5959
let mut output = Vec::new();
60-
let _ = codec.decode(&compressed, &mut output);
60+
let _ = codec.decode(&compressed, &mut output, None);
6161
});

fuzz/fuzz_targets/decode_oracle.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ fuzz_target!(|data: FuzzInput<CompatSelector>| {
4141
}
4242
let mut rust_decompressed = Vec::new();
4343
rust_codec
44-
.decode(&rust_compressed, &mut rust_decompressed)
44+
.decode(&rust_compressed, &mut rust_decompressed, None)
4545
.expect("Rust decompress of self-compressed data must not fail");
4646

4747
// C++ roundtrip (independent oracle)
@@ -51,7 +51,7 @@ fuzz_target!(|data: FuzzInput<CompatSelector>| {
5151
.expect("C++ compression failed");
5252
let mut cpp_decompressed = Vec::new();
5353
cpp_codec
54-
.decode(&cpp_compressed, &mut cpp_decompressed)
54+
.decode(&cpp_compressed, &mut cpp_decompressed, None)
5555
.expect("C++ decompression failed");
5656

5757
assert_eq!(

fuzz/fuzz_targets/encode_oracle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ fuzz_target!(|data: FuzzInput<AnyLenSelector>| {
2525

2626
let mut decompressed = Vec::new();
2727
codec
28-
.decode(&compressed, &mut decompressed)
28+
.decode(&compressed, &mut decompressed, None)
2929
.expect("Rust decompress of self-compressed data must not fail");
3030

3131
assert_eq!(

src/codec.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ use bytemuck::{Pod, cast_slice};
22

33
use crate::FastPForError;
44

5+
/// Internal default for max decompressed length. Used by trait defaults and C++ FFI.
6+
#[inline]
7+
pub(crate) fn default_max_decoded_len(compressed_words: usize) -> usize {
8+
compressed_words.saturating_mul(1024)
9+
}
10+
511
/// Compresses and decompresses fixed-size blocks of `u32` values.
612
///
713
/// The associated type [`Block`](BlockCodec::Block) is the concrete fixed-size
@@ -61,6 +67,17 @@ pub trait BlockCodec {
6167
n_blocks: usize,
6268
out: &mut Vec<u32>,
6369
) -> Result<(), FastPForError>;
70+
71+
/// Maximum decompressed element count for a given compressed input length.
72+
/// Reject `expected_len` values exceeding this to avoid allocation from bad data.
73+
#[inline]
74+
#[must_use]
75+
fn max_decompressed_len(compressed_words: usize) -> usize
76+
where
77+
Self: Sized,
78+
{
79+
default_max_decoded_len(compressed_words)
80+
}
6481
}
6582

6683
/// Codec that supports compressing 64-bit integers into a 32-bit word stream.
@@ -89,8 +106,32 @@ pub trait AnyLenCodec {
89106
/// Compress an arbitrary-length slice of `u32` values.
90107
fn encode(&mut self, input: &[u32], out: &mut Vec<u32>) -> Result<(), FastPForError>;
91108

109+
/// Maximum decompressed element count for a given compressed input length.
110+
/// Reject `expected_len` values exceeding this to avoid allocation from bad data.
111+
#[inline]
112+
#[must_use]
113+
fn max_decompressed_len(compressed_words: usize) -> usize
114+
where
115+
Self: Sized,
116+
{
117+
default_max_decoded_len(compressed_words)
118+
}
119+
92120
/// Decompress a previously compressed slice of `u32` values.
93-
fn decode(&mut self, input: &[u32], out: &mut Vec<u32>) -> Result<(), FastPForError>;
121+
///
122+
/// When `expected_len` is `Some(n)`:
123+
/// - Rejects if `n` exceeds [`max_decompressed_len`](AnyLenCodec::max_decompressed_len)(`input.len()`)
124+
/// - Pre-allocates output capacity for `n` elements
125+
/// - Returns [`DecodedCountMismatch`](crate::FastPForError::DecodedCountMismatch) if the
126+
/// actual decoded count differs from `n`
127+
///
128+
/// The hint is not trusted: values from untrusted metadata are capped before use.
129+
fn decode(
130+
&mut self,
131+
input: &[u32],
132+
out: &mut Vec<u32>,
133+
expected_len: Option<u32>,
134+
) -> Result<(), FastPForError>;
94135
}
95136

96137
/// Split a flat `&[u32]` into `(&[Blocks::Block], &[u32])` without copying.

0 commit comments

Comments
 (0)