feat(compress): move single-block serialization into Rust

`ZSTD_compressSeqStore_singleBlock` was still a C-owned decision body after
sequence encoding and block-format leaves had moved into Rust. That left the
C function responsible for mixing entropy errors, partition repcode repair,
RLE/raw fallback, sequence collection, compressed-state confirmation, and
block-header emission through the full private compression context.

Keep sequence-store construction, matchfinding, and split-block orchestration
in C, but pass a narrow repr(C) projection to Rust. The Rust body preserves the
original order of partition off-code resolution, entropy compression, the
non-first-block RLE rule, collector handling, dRep restoration for raw/RLE
blocks, compressed-state confirmation, header serialization, and repeat-mode
cleanup. Injectable leaf seams and focused tests cover entropy errors, raw
fallback, RLE gating, pointer swaps, and repeat-mode transitions.

Test Plan:
- `cargo test --manifest-path rust/Cargo.toml --lib -- --test-threads=1` -- 447 passed
- `cargo clippy --manifest-path rust/Cargo.toml -- -D warnings` -- passed
- `cargo clippy --manifest-path rust/Cargo.toml --benches -- -D warnings` -- passed
- `cargo clippy --manifest-path rust/Cargo.toml --tests -- -D warnings` -- blocked by the pre-existing manual_repeat_n lint in one_shot_promotes_nonfirst_rle_blocks
- `cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check` -- passed
- `make -B -C lib -j2 lib` -- passed
- `make -B -C tests -j2 test-cli-tests` -- passed
- `ZSTREAM_TESTTIME=-T2s make -B -C tests -j2 test-zstream` -- passed
- `FUZZERTEST=-T5s make -B -C tests -j2 test-fuzzer` -- passed
- `make -B -C tests/fuzz -j2 all` and `sequence_compression_api` -- passed
- `make -C tests -j2 test-zstd` -- passed, including large-data cases
This commit is contained in:
2026-07-18 20:16:17 +02:00
parent 57e3399930
commit 20bbb66676
2 changed files with 559 additions and 98 deletions
+503 -5
View File
@@ -27,11 +27,11 @@ use crate::zstd_compress_params::{
};
use crate::zstd_compress_sequences::SeqDef;
use crate::zstd_compress_stats::{
SeqStore_t, ZSTD_Sequence, ZSTD_SequencePosition, ZSTD_compressedBlockState_t,
ZSTD_rust_confirmRepcodesAndEntropyTables, ZSTD_rust_determineBlockSize,
ZSTD_rust_entropyCompressSeqStore, ZSTD_rust_isRLE, ZSTD_rust_maybeRLE,
ZSTD_rust_resetSeqStore, ZSTD_rust_transferSequencesNoDelim,
ZSTD_rust_transferSequencesWBlockDelim,
SeqCollector, SeqStore_t, ZSTD_Sequence, ZSTD_SequencePosition, ZSTD_compressedBlockState_t,
ZSTD_entropyCTables_t, ZSTD_rust_confirmRepcodesAndEntropyTables, ZSTD_rust_copyBlockSequences,
ZSTD_rust_determineBlockSize, ZSTD_rust_entropyCompressSeqStore, ZSTD_rust_isRLE,
ZSTD_rust_maybeRLE, ZSTD_rust_resetSeqStore, ZSTD_rust_seqStore_resolveOffCodes,
ZSTD_rust_transferSequencesNoDelim, ZSTD_rust_transferSequencesWBlockDelim,
};
use crate::zstd_compress_superblock::ZSTD_rust_compressSuperBlock;
use std::ffi::c_void;
@@ -164,6 +164,64 @@ const _: () = {
);
};
/// Explicit projection of the state used by `ZSTD_compressSeqStore_singleBlock`.
///
/// Sequence-store construction and the surrounding split-block bookkeeping
/// remain in C. Only the sequence store, simulated repcode histories,
/// compressed-block state slots, sequence collector, and scalar compression
/// settings cross the ABI.
#[repr(C)]
pub struct ZSTD_rust_seqStoreSingleBlockState {
seq_store: *const SeqStore_t,
d_rep: *mut u32,
c_rep: *mut u32,
prev_c_block: *mut *mut ZSTD_compressedBlockState_t,
next_c_block: *mut *mut ZSTD_compressedBlockState_t,
tmp_workspace: *mut c_void,
tmp_wksp_size: usize,
seq_collector: *mut SeqCollector,
strategy: c_int,
disable_literal_compression: c_int,
bmi2: c_int,
is_first_block: c_int,
}
const _: () = {
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, seq_store) == 0);
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, d_rep) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, c_rep) == 2 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, prev_c_block) == 3 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, next_c_block) == 4 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_seqStoreSingleBlockState, tmp_workspace) == 5 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_seqStoreSingleBlockState, tmp_wksp_size) == 6 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_seqStoreSingleBlockState, seq_collector) == 7 * size_of::<usize>()
);
assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, strategy) == usize::BITS as usize);
assert!(
offset_of!(
ZSTD_rust_seqStoreSingleBlockState,
disable_literal_compression
) == usize::BITS as usize + size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_seqStoreSingleBlockState, bmi2)
== usize::BITS as usize + 2 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_seqStoreSingleBlockState, is_first_block)
== usize::BITS as usize + 3 * size_of::<c_int>()
);
assert!(
size_of::<ZSTD_rust_seqStoreSingleBlockState>()
== usize::BITS as usize + 4 * size_of::<c_int>()
);
};
/// Explicit projection of the state used by the target-sized block body.
///
/// Sequence-store construction and the matchfinder remain in C. This state
@@ -233,6 +291,60 @@ enum SequenceBlockAction {
Compressed,
}
type SingleBlockEntropyFn = unsafe extern "C" fn(
*const SeqStore_t,
*const ZSTD_entropyCTables_t,
*mut ZSTD_entropyCTables_t,
c_int,
c_int,
*mut c_void,
usize,
usize,
*mut c_void,
usize,
c_int,
) -> usize;
type SingleBlockResolveOffCodesFn =
unsafe extern "C" fn(*mut u32, *mut u32, *const SeqStore_t, c_uint);
type SingleBlockCopySequencesFn =
unsafe extern "C" fn(*mut SeqCollector, *const SeqStore_t, *const u32) -> usize;
type SingleBlockConfirmFn = unsafe extern "C" fn(
*mut *mut ZSTD_compressedBlockState_t,
*mut *mut ZSTD_compressedBlockState_t,
);
type SingleBlockRawFn =
unsafe extern "C" fn(*mut c_void, usize, *const c_void, usize, c_uint) -> usize;
type SingleBlockRleFn = unsafe extern "C" fn(*mut c_void, usize, u8, usize, c_uint) -> usize;
type SingleBlockWriteHeaderFn = unsafe extern "C" fn(*mut c_void, usize, usize, c_uint);
type SingleBlockIsRleFn = unsafe extern "C" fn(*const u8, usize) -> c_int;
#[derive(Clone, Copy)]
struct SingleBlockSeams {
entropy_compress: SingleBlockEntropyFn,
resolve_off_codes: SingleBlockResolveOffCodesFn,
copy_sequences: SingleBlockCopySequencesFn,
confirm: SingleBlockConfirmFn,
raw: SingleBlockRawFn,
rle: SingleBlockRleFn,
write_header: SingleBlockWriteHeaderFn,
is_rle: SingleBlockIsRleFn,
}
impl SingleBlockSeams {
fn production() -> Self {
Self {
entropy_compress: ZSTD_rust_entropyCompressSeqStore,
resolve_off_codes: ZSTD_rust_seqStore_resolveOffCodes,
copy_sequences: ZSTD_rust_copyBlockSequences,
confirm: ZSTD_rust_confirmRepcodesAndEntropyTables,
raw: ZSTD_rust_noCompressBlock,
rle: ZSTD_rust_rleCompressBlock,
write_header: ZSTD_rust_writeBlockHeader,
is_rle: ZSTD_rust_isRLE,
}
}
}
#[inline]
fn sequence_block_action(
is_first_block: c_int,
@@ -424,6 +536,168 @@ pub unsafe extern "C" fn ZSTD_rust_compressBlockTargetCBlockSize(
}
}
/// Rust implementation of `ZSTD_compressSeqStore_singleBlock`.
///
/// The C caller still owns sequence-store construction, split-block control,
/// and the private compression context. This body owns the exact block
/// decision and the associated state transitions after the sequence store is
/// ready.
#[allow(clippy::too_many_arguments)]
unsafe fn compress_seq_store_single_block_body_with(
state: &ZSTD_rust_seqStoreSingleBlockState,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
last_block: c_uint,
is_partition: c_uint,
seams: SingleBlockSeams,
) -> usize {
const RLE_MAX_LENGTH: usize = 25;
if state.seq_store.is_null()
|| state.d_rep.is_null()
|| state.c_rep.is_null()
|| state.prev_c_block.is_null()
|| state.next_c_block.is_null()
|| state.seq_collector.is_null()
{
return ERROR(ZstdErrorCode::Generic);
}
let prev_c_block = unsafe { *state.prev_c_block };
let next_c_block = unsafe { *state.next_c_block };
if prev_c_block.is_null() || next_c_block.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
/* Preserve the decompression-side history for raw/RLE output and for the
* sequence collector, exactly as the original C body does. */
let mut d_rep_original = [0u32; ZSTD_REP_NUM];
unsafe {
ptr::copy_nonoverlapping(state.d_rep, d_rep_original.as_mut_ptr(), ZSTD_REP_NUM);
}
if is_partition != 0 {
let seq_store = unsafe { &*state.seq_store };
let nb_seq = unsafe { seq_store.sequences.offset_from(seq_store.sequencesStart) } as c_uint;
unsafe {
(seams.resolve_off_codes)(state.d_rep, state.c_rep, state.seq_store, nb_seq);
}
}
if dst_capacity < ZSTD_BLOCK_HEADER_SIZE {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let op = dst.cast::<u8>();
let ip = src.cast::<u8>();
let mut compressed_sequences_size = unsafe {
(seams.entropy_compress)(
state.seq_store,
ptr::addr_of!((*prev_c_block).entropy),
ptr::addr_of_mut!((*next_c_block).entropy),
state.strategy,
state.disable_literal_compression,
op.add(ZSTD_BLOCK_HEADER_SIZE).cast(),
dst_capacity - ZSTD_BLOCK_HEADER_SIZE,
src_size,
state.tmp_workspace,
state.tmp_wksp_size,
state.bmi2,
)
};
if ERR_isError(compressed_sequences_size) {
return compressed_sequences_size;
}
if state.is_first_block == 0
&& compressed_sequences_size < RLE_MAX_LENGTH
&& unsafe { (seams.is_rle)(ip, src_size) } != 0
{
/* Do not emit the first frame block as RLE; this preserves the legacy
* decoder compatibility rule in the original implementation. */
compressed_sequences_size = 1;
}
/* Sequence collection is deliberately before block serialization and
* returns without the outer repeat-mode cleanup, matching C. */
if unsafe { (*state.seq_collector).collectSequences } != 0 {
let result = unsafe {
(seams.copy_sequences)(
state.seq_collector,
state.seq_store,
d_rep_original.as_ptr(),
)
};
if ERR_isError(result) {
return result;
}
unsafe { (seams.confirm)(state.prev_c_block, state.next_c_block) };
return 0;
}
let c_size = if compressed_sequences_size == 0 {
let c_size = unsafe { (seams.raw)(dst, dst_capacity, src, src_size, last_block) };
if ERR_isError(c_size) {
return c_size;
}
unsafe {
ptr::copy_nonoverlapping(d_rep_original.as_ptr(), state.d_rep, ZSTD_REP_NUM);
}
c_size
} else if compressed_sequences_size == 1 {
let c_size = unsafe { (seams.rle)(dst, dst_capacity, *ip, src_size, last_block) };
if ERR_isError(c_size) {
return c_size;
}
unsafe {
ptr::copy_nonoverlapping(d_rep_original.as_ptr(), state.d_rep, ZSTD_REP_NUM);
}
c_size
} else {
unsafe { (seams.confirm)(state.prev_c_block, state.next_c_block) };
unsafe {
(seams.write_header)(dst, compressed_sequences_size, src_size, last_block);
}
ZSTD_BLOCK_HEADER_SIZE.wrapping_add(compressed_sequences_size)
};
let prev_c_block = unsafe { *state.prev_c_block };
if unsafe { (*prev_c_block).entropy.fse.offcode_repeatMode } == 2 {
unsafe { (*prev_c_block).entropy.fse.offcode_repeatMode = 1 };
}
c_size
}
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_compressSeqStoreSingleBlock(
state: *const ZSTD_rust_seqStoreSingleBlockState,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
last_block: c_uint,
is_partition: c_uint,
) -> usize {
if state.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
unsafe {
compress_seq_store_single_block_body_with(
&*state,
dst,
dst_capacity,
src,
src_size,
last_block,
is_partition,
SingleBlockSeams::production(),
)
}
}
/// Select the strategy used by the simple compression entry points.
///
/// This is the Rust equivalent of the strategy portion of
@@ -3058,6 +3332,230 @@ mod tests {
);
}
#[allow(clippy::too_many_arguments)]
unsafe extern "C" fn single_block_test_entropy_compress(
_seq_store: *const SeqStore_t,
_prev_entropy: *const ZSTD_entropyCTables_t,
_next_entropy: *mut ZSTD_entropyCTables_t,
strategy: c_int,
_disable_literal_compression: c_int,
dst: *mut c_void,
dst_capacity: usize,
_src_size: usize,
_workspace: *mut c_void,
_workspace_size: usize,
_bmi2: c_int,
) -> usize {
match strategy {
0 => 0,
1 => {
assert!(dst_capacity >= 2);
unsafe {
*dst.cast::<u8>() = 0x11;
*dst.cast::<u8>().add(1) = 0x22;
}
2
}
_ => ERROR(ZstdErrorCode::Generic),
}
}
fn single_block_test_state(
seq_store: &SeqStore_t,
d_rep: &mut [u32; ZSTD_REP_NUM],
c_rep: &mut [u32; ZSTD_REP_NUM],
prev_block: &mut ZSTD_compressedBlockState_t,
next_block: &mut ZSTD_compressedBlockState_t,
prev_c_block: &mut *mut ZSTD_compressedBlockState_t,
next_c_block: &mut *mut ZSTD_compressedBlockState_t,
seq_collector: &mut SeqCollector,
strategy: c_int,
is_first_block: c_int,
) -> ZSTD_rust_seqStoreSingleBlockState {
*prev_c_block = prev_block;
*next_c_block = next_block;
ZSTD_rust_seqStoreSingleBlockState {
seq_store,
d_rep: d_rep.as_mut_ptr(),
c_rep: c_rep.as_mut_ptr(),
prev_c_block,
next_c_block,
tmp_workspace: ptr::null_mut(),
tmp_wksp_size: 0,
seq_collector,
strategy,
disable_literal_compression: 0,
bmi2: 0,
is_first_block,
}
}
fn single_block_test_seams() -> SingleBlockSeams {
SingleBlockSeams {
entropy_compress: single_block_test_entropy_compress,
..SingleBlockSeams::production()
}
}
#[test]
fn single_block_body_propagates_entropy_errors_without_serializing() {
let (seq_store, _sequences, _literals) = target_block_test_seq_store();
let mut d_rep = [1, 4, 8];
let mut c_rep = d_rep;
let mut prev_block = zeroed_state();
let mut next_block = zeroed_state();
let mut prev_c_block = ptr::null_mut();
let mut next_c_block = ptr::null_mut();
let mut seq_collector = SeqCollector {
collectSequences: 0,
seqStart: ptr::null_mut(),
seqIndex: 0,
maxSequences: 0,
};
let state = single_block_test_state(
&seq_store,
&mut d_rep,
&mut c_rep,
&mut prev_block,
&mut next_block,
&mut prev_c_block,
&mut next_c_block,
&mut seq_collector,
-1,
1,
);
let source = *b"entropy error";
let mut output = [0xa5u8; 32];
let result = unsafe {
compress_seq_store_single_block_body_with(
&state,
output.as_mut_ptr().cast(),
output.len(),
source.as_ptr().cast(),
source.len(),
1,
0,
single_block_test_seams(),
)
};
assert_eq!(result, ERROR(ZstdErrorCode::Generic));
assert_eq!(output, [0xa5u8; 32]);
assert_eq!(d_rep, [1, 4, 8]);
assert_eq!(c_rep, [1, 4, 8]);
}
#[test]
fn single_block_body_serializes_raw_entropy_fallback() {
let (seq_store, _sequences, _literals) = target_block_test_seq_store();
let mut d_rep = [1, 4, 8];
let mut c_rep = d_rep;
let mut prev_block = zeroed_state();
let mut next_block = zeroed_state();
let mut prev_c_block = ptr::null_mut();
let mut next_c_block = ptr::null_mut();
let mut seq_collector = SeqCollector {
collectSequences: 0,
seqStart: ptr::null_mut(),
seqIndex: 0,
maxSequences: 0,
};
let state = single_block_test_state(
&seq_store,
&mut d_rep,
&mut c_rep,
&mut prev_block,
&mut next_block,
&mut prev_c_block,
&mut next_c_block,
&mut seq_collector,
0,
1,
);
let source = *b"raw fallback";
let mut output = [0xa5u8; 32];
let result = unsafe {
compress_seq_store_single_block_body_with(
&state,
output.as_mut_ptr().cast(),
output.len(),
source.as_ptr().cast(),
source.len(),
1,
0,
single_block_test_seams(),
)
};
assert_eq!(result, source.len() + ZSTD_BLOCK_HEADER_SIZE);
assert_eq!(&output[ZSTD_BLOCK_HEADER_SIZE..result], &source);
}
#[test]
fn single_block_body_gates_rle_on_first_block_and_confirms_compressed_state() {
for is_first_block in [0, 1] {
let (seq_store, _sequences, _literals) = target_block_test_seq_store();
let mut d_rep = [1, 4, 8];
let mut c_rep = d_rep;
let mut prev_block = zeroed_state();
let mut next_block = zeroed_state();
next_block.entropy.fse.offcode_repeatMode = 2;
let mut prev_c_block = ptr::null_mut();
let mut next_c_block = ptr::null_mut();
let mut seq_collector = SeqCollector {
collectSequences: 0,
seqStart: ptr::null_mut(),
seqIndex: 0,
maxSequences: 0,
};
let state = single_block_test_state(
&seq_store,
&mut d_rep,
&mut c_rep,
&mut prev_block,
&mut next_block,
&mut prev_c_block,
&mut next_c_block,
&mut seq_collector,
1,
is_first_block,
);
let source = [0x5au8; 16];
let mut output = [0xa5u8; 32];
let result = unsafe {
compress_seq_store_single_block_body_with(
&state,
output.as_mut_ptr().cast(),
output.len(),
source.as_ptr().cast(),
source.len(),
1,
0,
single_block_test_seams(),
)
};
if is_first_block == 0 {
assert_eq!(result, 4);
assert_eq!(output[3], source[0]);
assert!(std::ptr::eq(prev_c_block, &prev_block));
} else {
assert_eq!(result, ZSTD_BLOCK_HEADER_SIZE + 2);
assert_eq!(&output[ZSTD_BLOCK_HEADER_SIZE..result], &[0x11, 0x22]);
assert!(std::ptr::eq(prev_c_block, &next_block));
assert!(std::ptr::eq(next_c_block, &prev_block));
}
let expected_repeat_mode = if is_first_block == 0 { 0 } else { 1 };
assert_eq!(
unsafe { (*prev_c_block).entropy.fse.offcode_repeatMode },
expected_repeat_mode
);
}
}
#[test]
fn empty_sequence_block_preserves_header_and_capacity_contract() {
let mut output = [0xa5; 4];