feat(compress): move sequence block loop into Rust

Move the per-block body of ZSTD_compressSequences_internal behind an explicit
Rust projection.  The C wrapper still owns context initialization, public API
validation, frame headers, checksums, and the private CCtx layout.  Rust now
owns block sizing and sequence transfer, sequence-store reset, entropy
compression, raw/RLE/compressed block selection, block headers, repcode/state
swapping, repeat-mode transition, and first-block handling.

The bridge passes only the sequence store, block-state pointer slots, workspace,
policy scalars, dictionary size, and the isFirstBlock slot.  It does not pass a
CCtx or C callback across the ABI.  The tests cover empty-block headers,
capacity errors, first-block RLE restrictions, and entropy fallback decisions.

Test Plan:
- cargo test --manifest-path rust/Cargo.toml --lib zstd_compress -- --test-threads=1
- cargo clippy --manifest-path rust/Cargo.toml --lib -- -D warnings
- cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check
- cargo test --manifest-path rust/Cargo.toml --lib -- --test-threads=1
- make -B -C lib -j2 lib
- make -B -C tests -j2 test-cli-tests
- ZSTREAM_TESTTIME=-T2s make -B -C tests -j2 test-zstream
- FUZZERTEST=-T5s make -C tests -j2 test-fuzzer (covers ZSTD_compressSequences at fuzzer test 190)
- git diff --cached --check
This commit is contained in:
2026-07-18 19:14:19 +02:00
parent 07ecf63e76
commit 20db5861e5
2 changed files with 440 additions and 202 deletions
+361 -4
View File
@@ -17,8 +17,8 @@ use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
#[cfg(not(test))]
use crate::zstd_compress_api::ZSTD_compressBound;
use crate::zstd_compress_frame::{
write_raw_block, ZSTD_rust_writeBlockHeader, ZSTD_rust_writeFrameHeader,
ZSTD_writeLastEmptyBlock,
write_raw_block, ZSTD_rust_noCompressBlock, ZSTD_rust_rleCompressBlock,
ZSTD_rust_writeBlockHeader, ZSTD_rust_writeFrameHeader, ZSTD_writeLastEmptyBlock,
};
use crate::zstd_compress_literals::min_gain;
use crate::zstd_compress_params::{
@@ -27,10 +27,14 @@ use crate::zstd_compress_params::{
};
use crate::zstd_compress_sequences::SeqDef;
use crate::zstd_compress_stats::{
SeqStore_t, ZSTD_compressedBlockState_t, ZSTD_rust_entropyCompressSeqStore, ZSTD_rust_isRLE,
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,
};
use std::ffi::c_void;
use std::mem::{size_of, MaybeUninit};
use std::mem::{offset_of, size_of, MaybeUninit};
use std::os::raw::{c_int, c_uint};
use std::ptr;
@@ -101,6 +105,64 @@ const ZSTD_CHUNKSIZE_MAX: usize = u32::MAX as usize - ZSTD_CURRENT_MAX;
#[cfg(not(test))]
const ZSTD_E_END: c_int = 2;
/// Explicit projection of the state used by `ZSTD_compressSequences_internal`.
///
/// The C context and its function-pointer-bearing parameter structure remain
/// private to C. Only the sequence store, compressed-block state slots, and
/// scalar policy/workspace fields read by the per-block loop cross the ABI.
#[repr(C)]
pub struct ZSTD_rust_sequenceCompressionState {
seq_store: *mut SeqStore_t,
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,
block_size_max: usize,
bmi2: c_int,
block_delimiters: c_int,
strategy: c_int,
disable_literal_compression: c_int,
search_for_external_repcodes: c_int,
validate_sequences: c_int,
min_match: c_uint,
window_log: c_uint,
dict_size: c_uint,
use_sequence_producer: c_int,
is_first_block: *mut c_int,
}
const _: () = {
assert!(offset_of!(ZSTD_rust_sequenceCompressionState, seq_store) == 0);
assert!(offset_of!(ZSTD_rust_sequenceCompressionState, prev_c_block) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceCompressionState, next_c_block) == 2 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, tmp_workspace) == 3 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, tmp_wksp_size) == 4 * size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, block_size_max) == 5 * size_of::<usize>()
);
assert!(offset_of!(ZSTD_rust_sequenceCompressionState, bmi2) == 6 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, min_match)
== 6 * size_of::<usize>() + 6 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, use_sequence_producer)
== 6 * size_of::<usize>() + 6 * size_of::<c_int>() + 3 * size_of::<c_uint>()
);
assert!(
offset_of!(ZSTD_rust_sequenceCompressionState, is_first_block)
== 6 * size_of::<usize>() + 7 * size_of::<c_int>() + 3 * size_of::<c_uint>()
);
assert!(
size_of::<ZSTD_rust_sequenceCompressionState>()
== if size_of::<usize>() == 8 { 96 } else { 68 }
);
};
#[repr(i32)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum TargetCBlockAction {
@@ -110,6 +172,30 @@ enum TargetCBlockAction {
Error = 3,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum SequenceBlockAction {
Raw,
Rle,
Compressed,
}
#[inline]
fn sequence_block_action(
is_first_block: c_int,
maybe_rle: c_int,
is_rle: c_int,
compressed_size: usize,
) -> SequenceBlockAction {
if is_first_block == 0 && maybe_rle != 0 && is_rle != 0 {
return SequenceBlockAction::Rle;
}
match compressed_size {
0 => SequenceBlockAction::Raw,
1 => SequenceBlockAction::Rle,
_ => SequenceBlockAction::Compressed,
}
}
#[inline]
fn target_c_block_size_action(
bss: c_int,
@@ -952,6 +1038,241 @@ fn ceil_log2(size: usize) -> u32 {
}
}
#[inline]
unsafe fn write_empty_sequence_block(dst: *mut u8, dst_capacity: usize) -> usize {
/* Keep the original C helper's four-byte write and capacity check. */
if dst_capacity < 4 {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let header = 1u32.to_le_bytes();
unsafe { ptr::copy_nonoverlapping(header.as_ptr(), dst, header.len()) };
ZSTD_BLOCK_HEADER_SIZE
}
/// Rust implementation of the per-block loop from
/// `ZSTD_compressSequences_internal()`.
///
/// Context initialization, frame-header/checksum handling, and public API
/// validation remain in C. The projected state keeps the ABI explicit while
/// allowing the loop to reuse the existing Rust sequence-transfer, entropy,
/// and block-serialization leaves.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_compressSequencesInternal(
state: *const ZSTD_rust_sequenceCompressionState,
dst: *mut c_void,
dst_capacity: usize,
in_seqs: *const ZSTD_Sequence,
in_seqs_size: usize,
src: *const c_void,
src_size: usize,
) -> usize {
if state.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let state = unsafe { &*state };
if state.seq_store.is_null()
|| state.prev_c_block.is_null()
|| state.next_c_block.is_null()
|| state.is_first_block.is_null()
{
return ERROR(ZstdErrorCode::Generic);
}
if unsafe { (*state.prev_c_block).is_null() || (*state.next_c_block).is_null() } {
return ERROR(ZstdErrorCode::Generic);
}
let mut c_size = 0usize;
let mut remaining = src_size;
let mut seq_pos = ZSTD_SequencePosition::default();
let mut ip = src.cast::<u8>();
let mut op = dst.cast::<u8>();
let mut dst_capacity = dst_capacity;
let explicit_delimiters =
ZSTD_rust_selectSequenceCopier(state.block_delimiters) == ZSTD_SF_EXPLICIT_BLOCK_DELIMITERS;
/* Special case: empty frame. */
if remaining == 0 {
return unsafe { write_empty_sequence_block(op, dst_capacity) };
}
while remaining != 0 {
let mut block_size = unsafe {
ZSTD_rust_determineBlockSize(
state.block_delimiters,
state.block_size_max,
remaining,
in_seqs,
in_seqs_size,
seq_pos.idx,
)
};
let last_block = u32::from(block_size == remaining);
if ERR_isError(block_size) {
return block_size;
}
debug_assert!(block_size <= remaining);
unsafe { ZSTD_rust_resetSeqStore(state.seq_store) };
let prev_block = unsafe { *state.prev_c_block };
let next_block = unsafe { *state.next_c_block };
block_size = if explicit_delimiters {
unsafe {
ZSTD_rust_transferSequencesWBlockDelim(
state.seq_store,
&mut seq_pos,
in_seqs,
in_seqs_size,
ip,
block_size,
state.search_for_external_repcodes,
(*prev_block).rep.as_ptr(),
(*next_block).rep.as_mut_ptr(),
state.dict_size,
state.validate_sequences,
state.min_match,
state.window_log,
state.use_sequence_producer,
)
}
} else {
unsafe {
ZSTD_rust_transferSequencesNoDelim(
state.seq_store,
&mut seq_pos,
in_seqs,
in_seqs_size,
ip,
block_size,
(*prev_block).rep.as_ptr(),
(*next_block).rep.as_mut_ptr(),
state.dict_size,
state.validate_sequences,
state.min_match,
state.window_log,
state.use_sequence_producer,
)
}
};
if ERR_isError(block_size) {
return block_size;
}
/* If blocks are too small, emit as a nocompress block. */
if block_size < MIN_COMPRESSIBLE_BLOCK_SIZE {
let c_block_size = unsafe {
ZSTD_rust_noCompressBlock(
op.cast(),
dst_capacity,
ip.cast(),
block_size,
last_block,
)
};
if ERR_isError(c_block_size) {
return c_block_size;
}
c_size = c_size.wrapping_add(c_block_size);
unsafe {
ip = ip.add(block_size);
op = op.add(c_block_size);
}
remaining -= block_size;
dst_capacity -= c_block_size;
continue;
}
if dst_capacity < ZSTD_BLOCK_HEADER_SIZE {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let compressed_size = unsafe {
ZSTD_rust_entropyCompressSeqStore(
state.seq_store,
ptr::addr_of!((*prev_block).entropy),
ptr::addr_of_mut!((*next_block).entropy),
state.strategy,
state.disable_literal_compression,
op.add(ZSTD_BLOCK_HEADER_SIZE).cast(),
dst_capacity - ZSTD_BLOCK_HEADER_SIZE,
block_size,
state.tmp_workspace,
state.tmp_wksp_size,
state.bmi2,
)
};
if ERR_isError(compressed_size) {
return compressed_size;
}
let is_first_block = unsafe { *state.is_first_block };
let (maybe_rle, is_rle) = if is_first_block == 0 {
let maybe_rle = unsafe { ZSTD_rust_maybeRLE(state.seq_store) };
let is_rle = if maybe_rle != 0 {
unsafe { ZSTD_rust_isRLE(ip, block_size) }
} else {
0
};
(maybe_rle, is_rle)
} else {
(0, 0)
};
let action = sequence_block_action(is_first_block, maybe_rle, is_rle, compressed_size);
let c_block_size = match action {
SequenceBlockAction::Raw => unsafe {
/* ZSTD_noCompressBlock writes the block header as well. */
ZSTD_rust_noCompressBlock(
op.cast(),
dst_capacity,
ip.cast(),
block_size,
last_block,
)
},
SequenceBlockAction::Rle => unsafe {
ZSTD_rust_rleCompressBlock(op.cast(), dst_capacity, *ip, block_size, last_block)
},
SequenceBlockAction::Compressed => {
/* Error checking and repcodes update. */
unsafe {
ZSTD_rust_confirmRepcodesAndEntropyTables(
state.prev_c_block,
state.next_c_block,
);
}
let prev_block = unsafe { *state.prev_c_block };
if unsafe { (*prev_block).entropy.fse.offcode_repeatMode } == 2 {
unsafe { (*prev_block).entropy.fse.offcode_repeatMode = 1 };
}
unsafe {
ZSTD_rust_writeBlockHeader(op.cast(), compressed_size, block_size, last_block)
};
ZSTD_BLOCK_HEADER_SIZE + compressed_size
}
};
if ERR_isError(c_block_size) {
return c_block_size;
}
c_size = c_size.wrapping_add(c_block_size);
if last_block != 0 {
break;
}
unsafe {
ip = ip.add(block_size);
op = op.add(c_block_size);
*state.is_first_block = 0;
}
remaining -= block_size;
dst_capacity -= c_block_size;
}
c_size
}
/// Compress one frame using the already migrated block leaves.
///
/// This path owns the match tables and carries their history across the
@@ -2242,6 +2563,42 @@ mod tests {
);
}
#[test]
fn sequence_block_action_keeps_first_block_from_using_rle() {
assert_eq!(
sequence_block_action(1, 1, 1, 2),
SequenceBlockAction::Compressed
);
assert_eq!(sequence_block_action(0, 1, 1, 2), SequenceBlockAction::Rle);
}
#[test]
fn sequence_block_action_maps_entropy_fallbacks() {
assert_eq!(sequence_block_action(0, 0, 0, 0), SequenceBlockAction::Raw);
assert_eq!(sequence_block_action(0, 0, 0, 1), SequenceBlockAction::Rle);
assert_eq!(
sequence_block_action(0, 0, 0, 2),
SequenceBlockAction::Compressed
);
}
#[test]
fn empty_sequence_block_preserves_header_and_capacity_contract() {
let mut output = [0xa5; 4];
assert_eq!(
unsafe { write_empty_sequence_block(output.as_mut_ptr(), output.len()) },
ZSTD_BLOCK_HEADER_SIZE
);
assert_eq!(output, [1, 0, 0, 0]);
let mut short_output = [0xa5; 3];
assert_eq!(
unsafe { write_empty_sequence_block(short_output.as_mut_ptr(), short_output.len()) },
ERROR(ZstdErrorCode::DstSizeTooSmall)
);
assert_eq!(short_output, [0xa5; 3]);
}
#[test]
fn invalidate_rep_codes_clears_all_entries() {
let mut rep = [11u32, 22, 33];