feat(compress): move external sequence literal loop into Rust

Move the external-sequence-and-literals block loop into the Rust compression
module while preserving the C-owned CCtx initialization and sequence
conversion callback. Rust now owns block summaries, literal cursor movement,
entropy emission, block framing, repcode state updates, and completion
validation; C remains the private-context ABI adapter.

Test Plan:
- cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1
- cargo clippy --manifest-path rust/Cargo.toml --tests -- -D warnings
- make -B -C lib -j2 lib
- make -B -C tests -j2 test-cli-tests
- FUZZERTEST=-T5s make -B -C tests -j2 test-fuzzer
This commit is contained in:
2026-07-18 23:45:55 +02:00
parent 0d187f9327
commit 693b142837
2 changed files with 339 additions and 105 deletions
+262 -3
View File
@@ -31,9 +31,9 @@ use crate::zstd_compress_stats::{
ZSTD_entropyCTables_t, ZSTD_rust_confirmRepcodesAndEntropyTables, ZSTD_rust_copyBlockSequences,
ZSTD_rust_countSeqStoreLiteralsBytes, ZSTD_rust_countSeqStoreMatchBytes,
ZSTD_rust_deriveSeqStoreChunk, 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,
ZSTD_rust_entropyCompressSeqStore_internal, ZSTD_rust_get1BlockSummary, 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;
@@ -577,6 +577,71 @@ const _: () = {
);
};
/// C callback signature for the CCtx-dependent sequence conversion leaf.
type SequenceLiteralsConvertFn =
unsafe extern "C" fn(*mut c_void, *const ZSTD_Sequence, usize, c_int) -> usize;
/// Explicit projection of the state used by
/// ZSTD_compressSequencesAndLiterals_internal.
///
/// CCtx initialization, parameter validation, and the sequence conversion
/// callback remain in C. Rust owns the block loop, external literal cursor,
/// entropy invocation, block framing, and per-frame completion checks.
#[repr(C)]
pub struct ZSTD_rust_sequenceLiteralsState {
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,
strategy: c_int,
disable_literal_compression: c_int,
repcode_resolution: c_int,
is_first_block: *mut c_int,
callback_context: *mut c_void,
convert_block_sequences: SequenceLiteralsConvertFn,
}
const _: () = {
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, seq_store) == 0);
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, prev_c_block) == size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, next_c_block) == 2 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, tmp_workspace) == 3 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, tmp_wksp_size) == 4 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, block_size_max) == 5 * size_of::<usize>());
assert!(offset_of!(ZSTD_rust_sequenceLiteralsState, bmi2) == 6 * size_of::<usize>());
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, strategy)
== 6 * size_of::<usize>() + size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, disable_literal_compression)
== 6 * size_of::<usize>() + 2 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, repcode_resolution)
== 6 * size_of::<usize>() + 3 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, is_first_block)
== 6 * size_of::<usize>() + 4 * size_of::<c_int>()
);
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, callback_context)
== 6 * size_of::<usize>() + 4 * size_of::<c_int>() + size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rust_sequenceLiteralsState, convert_block_sequences)
== 6 * size_of::<usize>() + 4 * size_of::<c_int>() + 2 * size_of::<usize>()
);
assert!(
size_of::<ZSTD_rust_sequenceLiteralsState>()
== 6 * size_of::<usize>() + 4 * size_of::<c_int>() + 3 * size_of::<usize>()
);
};
/// Explicit projection of the state used by `ZSTD_compressSeqStore_singleBlock`.
///
/// Sequence-store construction and split discovery remain in C. Only the
@@ -2580,6 +2645,181 @@ pub unsafe extern "C" fn ZSTD_rust_compressSequencesInternal(
/// This path owns the match tables and carries their history across the
/// 128 KiB block boundaries, while keeping the private C
/// `ZSTD_MatchState_t` window out of the Rust ABI.
/// Writes the three-byte raw header used by the external
/// sequences-and-literals empty-frame special case.
#[inline]
unsafe fn write_empty_sequence_literals_block(dst: *mut u8, dst_capacity: usize) -> usize {
if dst_capacity < ZSTD_BLOCK_HEADER_SIZE {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let header = [1u8, 0, 0];
unsafe { ptr::copy_nonoverlapping(header.as_ptr(), dst, header.len()) };
ZSTD_BLOCK_HEADER_SIZE
}
/// Rust implementation of the block loop from
/// ZSTD_compressSequencesAndLiterals_internal.
///
/// The C wrapper retains public-API initialization and the CCtx-dependent
/// sequence conversion callback. Rust owns the external literal cursor,
/// per-block entropy pass, compressed-block framing, and completion checks.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_compressSequencesAndLiteralsInternal(
state: *const ZSTD_rust_sequenceLiteralsState,
dst: *mut c_void,
dst_capacity: usize,
in_seqs: *const ZSTD_Sequence,
nb_sequences: usize,
literals: *const c_void,
lit_size: usize,
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()
|| unsafe { (*state.prev_c_block).is_null() || (*state.next_c_block).is_null() }
{
return ERROR(ZstdErrorCode::Generic);
}
if nb_sequences == 0 {
return ERROR(ZstdErrorCode::ExternalSequencesInvalid);
}
let mut remaining = src_size;
let mut nb_sequences = nb_sequences;
let mut c_size = 0usize;
let mut op = dst.cast::<u8>();
let mut dst_capacity = dst_capacity;
let mut in_seqs = in_seqs;
let mut literals = literals;
let mut lit_size = lit_size;
/* Special case: empty frame. Keep the C ordering: this header is emitted
* before the ordinary sequence loop. */
if nb_sequences == 1 && unsafe { (*in_seqs).litLength } == 0 {
let written = unsafe { write_empty_sequence_literals_block(op, dst_capacity) };
if ERR_isError(written) {
return written;
}
unsafe {
op = op.add(written);
}
dst_capacity -= written;
c_size += written;
}
while nb_sequences != 0 {
let block = unsafe { ZSTD_rust_get1BlockSummary(in_seqs, nb_sequences) };
let last_block = c_uint::from(block.nbSequences == nb_sequences);
if ERR_isError(block.nbSequences) {
return block.nbSequences;
}
debug_assert!(block.nbSequences <= nb_sequences);
if block.litSize > lit_size {
return ERROR(ZstdErrorCode::ExternalSequencesInvalid);
}
unsafe { ZSTD_rust_resetSeqStore(state.seq_store) };
let conversion_status = unsafe {
(state.convert_block_sequences)(
state.callback_context,
in_seqs,
block.nbSequences,
state.repcode_resolution,
)
};
if ERR_isError(conversion_status) {
return conversion_status;
}
unsafe {
in_seqs = in_seqs.add(block.nbSequences);
}
nb_sequences -= block.nbSequences;
remaining = remaining.wrapping_sub(block.blockSize);
if dst_capacity < ZSTD_BLOCK_HEADER_SIZE {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
let prev_block = unsafe { *state.prev_c_block };
let next_block = unsafe { *state.next_c_block };
if prev_block.is_null() || next_block.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let compressed_size = unsafe {
ZSTD_rust_entropyCompressSeqStore_internal(
op.add(ZSTD_BLOCK_HEADER_SIZE).cast(),
dst_capacity - ZSTD_BLOCK_HEADER_SIZE,
literals,
block.litSize,
state.seq_store,
ptr::addr_of!((*prev_block).entropy),
ptr::addr_of_mut!((*next_block).entropy),
state.strategy,
state.disable_literal_compression,
state.tmp_workspace,
state.tmp_wksp_size,
state.bmi2,
)
};
if ERR_isError(compressed_size) {
return compressed_size;
}
let compressed_size = if compressed_size > state.block_size_max {
0
} else {
compressed_size
};
unsafe {
literals = literals.cast::<u8>().add(block.litSize).cast();
}
lit_size = lit_size.wrapping_sub(block.litSize);
if compressed_size == 0 {
return ERROR(ZstdErrorCode::CannotProduceUncompressedBlock);
}
debug_assert!(compressed_size > 1);
unsafe {
ZSTD_rust_confirmRepcodesAndEntropyTables(state.prev_c_block, state.next_c_block);
}
let prev_block = unsafe { *state.prev_c_block };
if prev_block.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
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.blockSize, last_block);
}
let c_block_size = ZSTD_BLOCK_HEADER_SIZE + compressed_size;
c_size = c_size.wrapping_add(c_block_size);
unsafe {
op = op.add(c_block_size);
*state.is_first_block = 0;
}
dst_capacity = dst_capacity.wrapping_sub(c_block_size);
if last_block != 0 {
break;
}
}
if lit_size != 0 || remaining != 0 {
return ERROR(ZstdErrorCode::ExternalSequencesInvalid);
}
c_size
}
unsafe fn compress_frame(
dst: *mut c_void,
dst_capacity: usize,
@@ -5278,6 +5518,25 @@ mod tests {
assert_eq!(short_output, [0xa5; 3]);
}
#[test]
fn empty_sequence_literals_block_writes_only_the_three_byte_header() {
let mut output = [0xa5; 4];
assert_eq!(
unsafe { write_empty_sequence_literals_block(output.as_mut_ptr(), output.len()) },
ZSTD_BLOCK_HEADER_SIZE
);
assert_eq!(output, [1, 0, 0, 0xa5]);
let mut short_output = [0xa5; 2];
assert_eq!(
unsafe {
write_empty_sequence_literals_block(short_output.as_mut_ptr(), short_output.len())
},
ERROR(ZstdErrorCode::DstSizeTooSmall)
);
assert_eq!(short_output, [0xa5; 2]);
}
#[test]
fn invalidate_rep_codes_clears_all_entries() {
let mut rep = [11u32, 22, 33];