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
+79 -198
View File
@@ -400,12 +400,6 @@ void ZSTD_rust_seqStore_resolveOffCodes(U32 dRep[ZSTD_REP_NUM],
U32 ZSTD_rust_resolveRepcodeToRawOffset(const U32 rep[ZSTD_REP_NUM],
U32 offBase, U32 ll0);
U32 ZSTD_rust_finalizeOffBase(U32 rawOffset, const U32 rep[ZSTD_REP_NUM], U32 ll0);
size_t ZSTD_rust_blockSizeExplicitDelimiter(const ZSTD_Sequence* inSeqs,
size_t inSeqsSize, U32 seqIdx);
size_t ZSTD_rust_determineBlockSize(int mode, size_t blockSize, size_t remaining,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
U32 seqIdx);
int ZSTD_rust_selectSequenceCopier(int mode);
size_t ZSTD_rust_loadCEntropy(ZSTD_compressedBlockState_t* bs, void* workspace,
const void* dict, size_t dictSize);
size_t ZSTD_rust_transferSequencesWBlockDelim(
@@ -416,19 +410,58 @@ size_t ZSTD_rust_transferSequencesWBlockDelim(
U32 nextRepcodes[ZSTD_REP_NUM], U32 dictSize,
int validateSequences, U32 minMatch, U32 windowLog,
int useSequenceProducer);
size_t ZSTD_rust_transferSequencesNoDelim(
SeqStore_t* seqStore, ZSTD_SequencePosition* seqPos,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
const BYTE* src, size_t blockSize,
const U32 prevRepcodes[ZSTD_REP_NUM],
U32 nextRepcodes[ZSTD_REP_NUM], U32 dictSize,
int validateSequences, U32 minMatch, U32 windowLog,
int useSequenceProducer);
size_t ZSTD_rust_optimalBlockSize(const void* src, size_t srcSize,
size_t blockSizeMax, int splitLevel,
int strategy, S64 savings,
void* workspace, size_t workspaceSize);
/* The sequence-compression loop receives only the state it actually reads or
* updates. In particular, neither ZSTD_CCtx nor a C function pointer crosses
* the Rust ABI. */
typedef struct {
SeqStore_t* seqStore;
ZSTD_compressedBlockState_t** prevCBlock;
ZSTD_compressedBlockState_t** nextCBlock;
void* tmpWorkspace;
size_t tmpWkspSize;
size_t blockSizeMax;
int bmi2;
int blockDelimiters;
int strategy;
int disableLiteralCompression;
int searchForExternalRepcodes;
int validateSequences;
U32 minMatch;
U32 windowLog;
U32 dictSize;
int useSequenceProducer;
int* isFirstBlock;
} ZSTD_rust_sequenceCompressionState;
size_t ZSTD_rust_compressSequencesInternal(
const ZSTD_rust_sequenceCompressionState* state,
void* dst, size_t dstCapacity,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
const void* src, size_t srcSize);
typedef char ZSTD_rust_sequence_state_layout[
(offsetof(ZSTD_rust_sequenceCompressionState, seqStore) == 0
&& offsetof(ZSTD_rust_sequenceCompressionState, prevCBlock) == sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, nextCBlock) == 2 * sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, tmpWorkspace) == 3 * sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, tmpWkspSize) == 4 * sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, blockSizeMax) == 5 * sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, bmi2) == 6 * sizeof(void*)
&& offsetof(ZSTD_rust_sequenceCompressionState, minMatch)
== 6 * sizeof(void*) + 6 * sizeof(int)
&& offsetof(ZSTD_rust_sequenceCompressionState, useSequenceProducer)
== 6 * sizeof(void*) + 3 * sizeof(U32) + 6 * sizeof(int)
&& offsetof(ZSTD_rust_sequenceCompressionState, isFirstBlock)
== 6 * sizeof(void*) + 3 * sizeof(U32) + 7 * sizeof(int)
&& sizeof(ZSTD_rust_sequenceCompressionState)
== offsetof(ZSTD_rust_sequenceCompressionState, isFirstBlock) + sizeof(void*))
? 1 : -1];
typedef char ZSTD_rust_stats_seqdef_layout[(sizeof(SeqDef) == 8) ? 1 : -1];
typedef char ZSTD_rust_stats_block_summary_layout[
(sizeof(BlockSummary) == 3 * sizeof(size_t)) ? 1 : -1];
@@ -5110,12 +5143,9 @@ static U32 ZSTD_finalizeOffBase(U32 rawOffset, const U32 rep[ZSTD_REP_NUM], U32
return ZSTD_rust_finalizeOffBase(rawOffset, rep, ll0);
}
/* This function scans through an array of ZSTD_Sequence,
* storing the sequences it reads, until it reaches a block delimiter.
* Note that the block delimiter includes the last literals of the block.
* @blockSize must be == sum(sequence_lengths).
* @returns @blockSize on success, and a ZSTD_error otherwise.
*/
/* The explicit-delimiter adapter is also used by the external sequence
* producer path. Keep that C-context-facing call site separate from the
* Rust-owned compressSequences block loop. */
static size_t
ZSTD_transferSequences_wBlockDelim(ZSTD_CCtx* cctx,
ZSTD_SequencePosition* seqPos,
@@ -5143,78 +5173,6 @@ ZSTD_transferSequences_wBlockDelim(ZSTD_CCtx* cctx,
ZSTD_hasExtSeqProd(&cctx->appliedParams));
}
/*
* This function attempts to scan through @blockSize bytes in @src
* represented by the sequences in @inSeqs,
* storing any (partial) sequences.
*
* Occasionally, we may want to reduce the actual number of bytes consumed from @src
* to avoid splitting a match, notably if it would produce a match smaller than MINMATCH.
*
* @returns the number of bytes consumed from @src, necessarily <= @blockSize.
* Otherwise, it may return a ZSTD error if something went wrong.
*/
static size_t
ZSTD_transferSequences_noDelim(ZSTD_CCtx* cctx,
ZSTD_SequencePosition* seqPos,
const ZSTD_Sequence* const inSeqs, size_t inSeqsSize,
const void* src, size_t blockSize,
ZSTD_ParamSwitch_e externalRepSearch)
{
size_t dictSize;
if (cctx->cdict) {
dictSize = cctx->cdict->dictContentSize;
} else if (cctx->prefixDict.dict) {
dictSize = cctx->prefixDict.dictSize;
} else {
dictSize = 0;
}
(void)externalRepSearch;
return ZSTD_rust_transferSequencesNoDelim(
&cctx->seqStore, seqPos, inSeqs, inSeqsSize,
(const BYTE*)src, blockSize,
cctx->blockState.prevCBlock->rep,
cctx->blockState.nextCBlock->rep, (U32)dictSize,
cctx->appliedParams.validateSequences,
cctx->appliedParams.cParams.minMatch,
cctx->appliedParams.cParams.windowLog,
ZSTD_hasExtSeqProd(&cctx->appliedParams));
}
/* @seqPos represents a position within @inSeqs,
* it is read and updated by this function,
* once the goal to produce a block of size @blockSize is reached.
* @return: nb of bytes consumed from @src, necessarily <= @blockSize.
*/
typedef size_t (*ZSTD_SequenceCopier_f)(ZSTD_CCtx* cctx,
ZSTD_SequencePosition* seqPos,
const ZSTD_Sequence* const inSeqs, size_t inSeqsSize,
const void* src, size_t blockSize,
ZSTD_ParamSwitch_e externalRepSearch);
static ZSTD_SequenceCopier_f ZSTD_selectSequenceCopier(ZSTD_SequenceFormat_e mode)
{
switch (ZSTD_rust_selectSequenceCopier((int)mode)) {
case ZSTD_sf_explicitBlockDelimiters:
return ZSTD_transferSequences_wBlockDelim;
case ZSTD_sf_noBlockDelimiters:
default:
return ZSTD_transferSequences_noDelim;
}
}
static size_t determine_blockSize(ZSTD_SequenceFormat_e mode,
size_t blockSize, size_t remaining,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
ZSTD_SequencePosition seqPos)
{
DEBUGLOG(6, "determine_blockSize : remainingSize = %zu", remaining);
assert(mode == ZSTD_sf_noBlockDelimiters || mode == ZSTD_sf_explicitBlockDelimiters);
return ZSTD_rust_determineBlockSize((int)mode, blockSize, remaining,
inSeqs, inSeqsSize, seqPos.idx);
}
/* Compress all provided sequences, block-by-block.
*
* Returns the cumulative size of all compressed blocks (including their headers),
@@ -5226,115 +5184,38 @@ ZSTD_compressSequences_internal(ZSTD_CCtx* cctx,
const ZSTD_Sequence* inSeqs, size_t inSeqsSize,
const void* src, size_t srcSize)
{
size_t cSize = 0;
size_t remaining = srcSize;
ZSTD_SequencePosition seqPos = {0, 0, 0};
U32 dictSize;
ZSTD_rust_sequenceCompressionState state;
const BYTE* ip = (BYTE const*)src;
BYTE* op = (BYTE*)dst;
ZSTD_SequenceCopier_f const sequenceCopier = ZSTD_selectSequenceCopier(cctx->appliedParams.blockDelimiters);
if (cctx->cdict) {
dictSize = (U32)cctx->cdict->dictContentSize;
} else if (cctx->prefixDict.dict) {
dictSize = (U32)cctx->prefixDict.dictSize;
} else {
dictSize = 0;
}
state.seqStore = &cctx->seqStore;
state.prevCBlock = &cctx->blockState.prevCBlock;
state.nextCBlock = &cctx->blockState.nextCBlock;
state.tmpWorkspace = cctx->tmpWorkspace;
state.tmpWkspSize = cctx->tmpWkspSize;
state.blockSizeMax = cctx->blockSizeMax;
state.bmi2 = cctx->bmi2;
state.blockDelimiters = (int)cctx->appliedParams.blockDelimiters;
state.strategy = (int)cctx->appliedParams.cParams.strategy;
state.disableLiteralCompression = ZSTD_literalsCompressionIsDisabled(&cctx->appliedParams);
state.searchForExternalRepcodes = (int)cctx->appliedParams.searchForExternalRepcodes;
state.validateSequences = cctx->appliedParams.validateSequences;
state.minMatch = cctx->appliedParams.cParams.minMatch;
state.windowLog = cctx->appliedParams.cParams.windowLog;
state.dictSize = dictSize;
state.useSequenceProducer = ZSTD_hasExtSeqProd(&cctx->appliedParams);
state.isFirstBlock = &cctx->isFirstBlock;
DEBUGLOG(4, "ZSTD_compressSequences_internal srcSize: %zu, inSeqsSize: %zu", srcSize, inSeqsSize);
/* Special case: empty frame */
if (remaining == 0) {
U32 const cBlockHeader24 = 1 /* last block */ + (((U32)bt_raw)<<1);
RETURN_ERROR_IF(dstCapacity<4, dstSize_tooSmall, "No room for empty frame block header");
MEM_writeLE32(op, cBlockHeader24);
op += ZSTD_blockHeaderSize;
dstCapacity -= ZSTD_blockHeaderSize;
cSize += ZSTD_blockHeaderSize;
}
while (remaining) {
size_t compressedSeqsSize;
size_t cBlockSize;
size_t blockSize = determine_blockSize(cctx->appliedParams.blockDelimiters,
cctx->blockSizeMax, remaining,
inSeqs, inSeqsSize, seqPos);
U32 const lastBlock = (blockSize == remaining);
FORWARD_IF_ERROR(blockSize, "Error while trying to determine block size");
assert(blockSize <= remaining);
ZSTD_resetSeqStore(&cctx->seqStore);
blockSize = sequenceCopier(cctx,
&seqPos, inSeqs, inSeqsSize,
ip, blockSize,
cctx->appliedParams.searchForExternalRepcodes);
FORWARD_IF_ERROR(blockSize, "Bad sequence copy");
/* If blocks are too small, emit as a nocompress block */
/* TODO: See 3090. We reduced MIN_CBLOCK_SIZE from 3 to 2 so to compensate we are adding
* additional 1. We need to revisit and change this logic to be more consistent */
if (blockSize < MIN_CBLOCK_SIZE+ZSTD_blockHeaderSize+1+1) {
cBlockSize = ZSTD_rust_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock);
FORWARD_IF_ERROR(cBlockSize, "Nocompress block failed");
DEBUGLOG(5, "Block too small (%zu): data remains uncompressed: cSize=%zu", blockSize, cBlockSize);
cSize += cBlockSize;
ip += blockSize;
op += cBlockSize;
remaining -= blockSize;
dstCapacity -= cBlockSize;
continue;
}
RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize, dstSize_tooSmall, "not enough dstCapacity to write a new compressed block");
compressedSeqsSize = ZSTD_entropyCompressSeqStore(&cctx->seqStore,
&cctx->blockState.prevCBlock->entropy, &cctx->blockState.nextCBlock->entropy,
&cctx->appliedParams,
op + ZSTD_blockHeaderSize /* Leave space for block header */, dstCapacity - ZSTD_blockHeaderSize,
blockSize,
cctx->tmpWorkspace, cctx->tmpWkspSize /* statically allocated in resetCCtx */,
cctx->bmi2);
FORWARD_IF_ERROR(compressedSeqsSize, "Compressing sequences of block failed");
DEBUGLOG(5, "Compressed sequences size: %zu", compressedSeqsSize);
if (!cctx->isFirstBlock &&
ZSTD_maybeRLE(&cctx->seqStore) &&
ZSTD_isRLE(ip, blockSize)) {
/* Note: don't emit the first block as RLE even if it qualifies because
* doing so will cause the decoder (cli <= v1.4.3 only) to throw an (invalid) error
* "should consume all input error."
*/
compressedSeqsSize = 1;
}
if (compressedSeqsSize == 0) {
/* ZSTD_noCompressBlock writes the block header as well */
cBlockSize = ZSTD_rust_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock);
FORWARD_IF_ERROR(cBlockSize, "ZSTD_noCompressBlock failed");
DEBUGLOG(5, "Writing out nocompress block, size: %zu", cBlockSize);
} else if (compressedSeqsSize == 1) {
cBlockSize = ZSTD_rust_rleCompressBlock(op, dstCapacity, *ip, blockSize, lastBlock);
FORWARD_IF_ERROR(cBlockSize, "ZSTD_rleCompressBlock failed");
DEBUGLOG(5, "Writing out RLE block, size: %zu", cBlockSize);
} else {
/* Error checking and repcodes update */
ZSTD_blockState_confirmRepcodesAndEntropyTables(&cctx->blockState);
if (cctx->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid)
cctx->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check;
/* Write block header into beginning of block*/
ZSTD_rust_writeBlockHeader(op, compressedSeqsSize, blockSize, lastBlock);
cBlockSize = ZSTD_blockHeaderSize + compressedSeqsSize;
DEBUGLOG(5, "Writing out compressed block, size: %zu", cBlockSize);
}
cSize += cBlockSize;
if (lastBlock) {
break;
} else {
ip += blockSize;
op += cBlockSize;
remaining -= blockSize;
dstCapacity -= cBlockSize;
cctx->isFirstBlock = 0;
}
DEBUGLOG(5, "cSize running total: %zu (remaining dstCapacity=%zu)", cSize, dstCapacity);
}
DEBUGLOG(4, "cSize final total: %zu", cSize);
return cSize;
return ZSTD_rust_compressSequencesInternal(&state, dst, dstCapacity,
inSeqs, inSeqsSize, src, srcSize);
}
size_t ZSTD_compressSequences(ZSTD_CCtx* cctx,