diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index e4a623b94..209e3d689 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -120,6 +120,47 @@ typedef char ZSTD_rust_target_cblock_state_layout[ && sizeof(ZSTD_rust_targetCBlockSizeState) == (sizeof(void*) == 8 ? 72 : 44)) ? 1 : -1]; +/* The single-block sequence-store body only needs this narrow projection of + * ZSTD_CCtx. Matchfinding, sequence-store construction, and split-block + * control remain in C. */ +typedef struct { + const SeqStore_t* seqStore; + U32* dRep; + U32* cRep; + ZSTD_compressedBlockState_t** prevCBlock; + ZSTD_compressedBlockState_t** nextCBlock; + void* tmpWorkspace; + size_t tmpWkspSize; + SeqCollector* seqCollector; + int strategy; + int disableLiteralCompression; + int bmi2; + int isFirstBlock; +} ZSTD_rust_seqStoreSingleBlockState; +size_t ZSTD_rust_compressSeqStoreSingleBlock( + const ZSTD_rust_seqStoreSingleBlockState* state, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 lastBlock, U32 isPartition); +typedef char ZSTD_rust_seqstore_single_block_state_layout[ + (offsetof(ZSTD_rust_seqStoreSingleBlockState, seqStore) == 0 + && offsetof(ZSTD_rust_seqStoreSingleBlockState, dRep) == sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, cRep) == 2 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, prevCBlock) == 3 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, nextCBlock) == 4 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, tmpWorkspace) == 5 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, tmpWkspSize) == 6 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, seqCollector) == 7 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, strategy) == 8 * sizeof(void*) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, disableLiteralCompression) + == 8 * sizeof(void*) + sizeof(int) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, bmi2) + == 8 * sizeof(void*) + 2 * sizeof(int) + && offsetof(ZSTD_rust_seqStoreSingleBlockState, isFirstBlock) + == 8 * sizeof(void*) + 3 * sizeof(int) + && sizeof(ZSTD_rust_seqStoreSingleBlockState) + == 8 * sizeof(void*) + 4 * sizeof(int)) + ? 1 : -1]; size_t ZSTD_rust_nextInputSizeHint(int inBufferMode, size_t blockSizeMax, size_t stableInNotConsumed, @@ -423,10 +464,6 @@ size_t ZSTD_rust_deriveBlockSplits( int strategy, int disableLiteralCompression, ZSTD_entropyCTablesMetadata_t* entropyMetadata, void* workspace, size_t workspaceSize); -void ZSTD_rust_seqStore_resolveOffCodes(U32 dRep[ZSTD_REP_NUM], - U32 cRep[ZSTD_REP_NUM], - const SeqStore_t* seqStore, - U32 nbSeq); 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); @@ -2671,14 +2708,6 @@ ZSTD_blockState_confirmRepcodesAndEntropyTables(ZSTD_blockState_t* const bs) &bs->prevCBlock, &bs->nextCBlock); } -/* Writes the block header */ -static void -writeBlockHeader(void* op, size_t cSize, size_t blockSize, U32 lastBlock) -{ - ZSTD_rust_writeBlockHeader(op, cSize, blockSize, lastBlock); - DEBUGLOG(5, "writeBlockHeader: cSize: %zu blockSize: %zu lastBlock: %u", cSize, blockSize, lastBlock); -} - /** ZSTD_buildBlockEntropyStats() : * Builds entropy for the block. * Requires workspace size ENTROPY_WORKSPACE_SIZE @@ -2773,27 +2802,6 @@ static void ZSTD_deriveSeqStoreChunk(SeqStore_t* resultSeqStore, startIdx, endIdx); } -/** - * ZSTD_seqStore_resolveOffCodes() reconciles any possible divergences in offset history that may arise - * due to emission of RLE/raw blocks that disturb the offset history, - * and replaces any repcodes within the seqStore that may be invalid. - * - * dRepcodes are updated as would be on the decompression side. - * cRepcodes are updated exactly in accordance with the seqStore. - * - * Note : this function assumes seq->offBase respects the following numbering scheme : - * 0 : invalid - * 1-3 : repcode 1-3 - * 4+ : real_offset+3 - */ -static void -ZSTD_seqStore_resolveOffCodes(Repcodes_t* const dRepcodes, Repcodes_t* const cRepcodes, - const SeqStore_t* const seqStore, U32 const nbSeq) -{ - ZSTD_rust_seqStore_resolveOffCodes(dRepcodes->rep, cRepcodes->rep, - seqStore, nbSeq); -} - /* ZSTD_compressSeqStore_singleBlock(): * Compresses a seqStore into a block with a block header, into the buffer dst. * @@ -2807,66 +2815,21 @@ ZSTD_compressSeqStore_singleBlock(ZSTD_CCtx* zc, const void* src, size_t srcSize, U32 lastBlock, U32 isPartition) { - const U32 rleMaxLength = 25; - BYTE* op = (BYTE*)dst; - const BYTE* ip = (const BYTE*)src; - size_t cSize; - size_t cSeqsSize; - - /* In case of an RLE or raw block, the simulated decompression repcode history must be reset */ - Repcodes_t const dRepOriginal = *dRep; - DEBUGLOG(5, "ZSTD_compressSeqStore_singleBlock"); - if (isPartition) - ZSTD_seqStore_resolveOffCodes(dRep, cRep, seqStore, (U32)(seqStore->sequences - seqStore->sequencesStart)); - - RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize, dstSize_tooSmall, "Block header doesn't fit"); - cSeqsSize = ZSTD_entropyCompressSeqStore(seqStore, - &zc->blockState.prevCBlock->entropy, &zc->blockState.nextCBlock->entropy, - &zc->appliedParams, - op + ZSTD_blockHeaderSize, dstCapacity - ZSTD_blockHeaderSize, - srcSize, - zc->tmpWorkspace, zc->tmpWkspSize /* statically allocated in resetCCtx */, - zc->bmi2); - FORWARD_IF_ERROR(cSeqsSize, "ZSTD_entropyCompressSeqStore failed!"); - - if (!zc->isFirstBlock && - cSeqsSize < rleMaxLength && - ZSTD_isRLE((BYTE const*)src, srcSize)) { - /* We don't want to emit our first block as a RLE even if it qualifies because - * doing so will cause the decoder (cli only) to throw a "should consume all input error." - * This is only an issue for zstd <= v1.4.3 - */ - cSeqsSize = 1; - } - - /* Sequence collection not supported when block splitting */ - if (zc->seqCollector.collectSequences) { - FORWARD_IF_ERROR(ZSTD_copyBlockSequences(&zc->seqCollector, seqStore, dRepOriginal.rep), "copyBlockSequences failed"); - ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); - return 0; - } - - if (cSeqsSize == 0) { - cSize = ZSTD_rust_noCompressBlock(op, dstCapacity, ip, srcSize, lastBlock); - FORWARD_IF_ERROR(cSize, "Nocompress block failed"); - DEBUGLOG(5, "Writing out nocompress block, size: %zu", cSize); - *dRep = dRepOriginal; /* reset simulated decompression repcode history */ - } else if (cSeqsSize == 1) { - cSize = ZSTD_rust_rleCompressBlock(op, dstCapacity, *ip, srcSize, lastBlock); - FORWARD_IF_ERROR(cSize, "RLE compress block failed"); - DEBUGLOG(5, "Writing out RLE block, size: %zu", cSize); - *dRep = dRepOriginal; /* reset simulated decompression repcode history */ - } else { - ZSTD_blockState_confirmRepcodesAndEntropyTables(&zc->blockState); - writeBlockHeader(op, cSeqsSize, srcSize, lastBlock); - cSize = ZSTD_blockHeaderSize + cSeqsSize; - DEBUGLOG(5, "Writing out compressed block, size: %zu", cSize); - } - - if (zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode == FSE_repeat_valid) - zc->blockState.prevCBlock->entropy.fse.offcode_repeatMode = FSE_repeat_check; - - return cSize; + ZSTD_rust_seqStoreSingleBlockState state; + state.seqStore = seqStore; + state.dRep = dRep->rep; + state.cRep = cRep->rep; + state.prevCBlock = &zc->blockState.prevCBlock; + state.nextCBlock = &zc->blockState.nextCBlock; + state.tmpWorkspace = zc->tmpWorkspace; + state.tmpWkspSize = zc->tmpWkspSize; + state.seqCollector = &zc->seqCollector; + state.strategy = (int)zc->appliedParams.cParams.strategy; + state.disableLiteralCompression = ZSTD_literalsCompressionIsDisabled(&zc->appliedParams); + state.bmi2 = zc->bmi2; + state.isFirstBlock = zc->isFirstBlock; + return ZSTD_rust_compressSeqStoreSingleBlock( + &state, dst, dstCapacity, src, srcSize, lastBlock, isPartition); } /* Base recursive function. diff --git a/rust/src/zstd_compress.rs b/rust/src/zstd_compress.rs index bf9bf2d97..09ccd2a33 100644 --- a/rust/src/zstd_compress.rs +++ b/rust/src/zstd_compress.rs @@ -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::()); + assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, c_rep) == 2 * size_of::()); + assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, prev_c_block) == 3 * size_of::()); + assert!(offset_of!(ZSTD_rust_seqStoreSingleBlockState, next_c_block) == 4 * size_of::()); + assert!( + offset_of!(ZSTD_rust_seqStoreSingleBlockState, tmp_workspace) == 5 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_seqStoreSingleBlockState, tmp_wksp_size) == 6 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_seqStoreSingleBlockState, seq_collector) == 7 * size_of::() + ); + 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::() + ); + assert!( + offset_of!(ZSTD_rust_seqStoreSingleBlockState, bmi2) + == usize::BITS as usize + 2 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_seqStoreSingleBlockState, is_first_block) + == usize::BITS as usize + 3 * size_of::() + ); + assert!( + size_of::() + == usize::BITS as usize + 4 * size_of::() + ); +}; + /// 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::(); + let ip = src.cast::(); + 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::() = 0x11; + *dst.cast::().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];