From f3bc5e98f15a069e8a8536f79a5006a77aaaeb71 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 22:08:37 +0200 Subject: [PATCH] feat(compress): move frame-chunk loop into Rust Port ZSTD_compress_frameChunk's per-block orchestration into Rust behind an explicit C-layout projection and callback table. Rust now owns block sizing, target/split/internal dispatch, block framing and accounting, checksum sequencing, and terminal frame-state updates. C retains CCtx and match-state preparation plus the codec-specific callbacks. Test Plan: - cargo test --manifest-path rust/Cargo.toml --all-targets -- --test-threads=1 - cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings - make -B -C lib -j2 lib - make -B -C programs -j2 zstd - make -B -C tests -j2 test-zstd --- lib/compress/zstd_compress.c | 239 ++++++++++------- rust/src/zstd_compress.rs | 490 ++++++++++++++++++++++++++++++++++- 2 files changed, 630 insertions(+), 99 deletions(-) diff --git a/lib/compress/zstd_compress.c b/lib/compress/zstd_compress.c index 00cc6bb8a..a86a6bfae 100644 --- a/lib/compress/zstd_compress.c +++ b/lib/compress/zstd_compress.c @@ -79,6 +79,74 @@ void ZSTD_rust_copyCDictTableIntoCCtx(U32* dst, U32 const* src, U64 ZSTD_rust_advanceHashSalt(U64 hashSalt, U64 hashSaltEntropy); int ZSTD_rust_indexTooCloseToMax(size_t nextSrcBaseOffset); int ZSTD_rust_dictTooBig(size_t loadedDictSize); + +/* The frame-chunk loop is Rust-owned. Its callbacks keep the private + * ZSTD_CCtx and match-state layout in C: Rust only drives the block loop and + * passes this context back to these C-owned state-preparation/dispatch seams. */ +typedef void (*ZSTD_rust_frameChunkPrepare_f)(void* context, + const void* src, + size_t blockSize); +typedef size_t (*ZSTD_rust_frameChunkCompress_f)(void* context, + void* dst, + size_t dstCapacity, + const void* src, + size_t srcSize, + U32 lastBlock); +typedef void (*ZSTD_rust_frameChunkChecksum_f)(void* state, + const void* src, + size_t srcSize); +typedef struct { + void* callbackContext; + void* tmpWorkspace; + void* checksumState; + int* isFirstBlock; + ZSTD_compressionStage_e* stage; + size_t tmpWkspSize; + size_t blockSizeMax; + S64 savings; + int preBlockSplitterLevel; + int strategy; + int useTargetCBlockSize; + int blockSplitterEnabled; + int checksumFlag; + int endingStage; + ZSTD_rust_frameChunkPrepare_f prepareBlock; + ZSTD_rust_frameChunkCompress_f compressTarget; + ZSTD_rust_frameChunkCompress_f compressSplit; + ZSTD_rust_frameChunkCompress_f compressInternal; + ZSTD_rust_frameChunkChecksum_f updateChecksum; +} ZSTD_rust_frameChunkState; +size_t ZSTD_rust_compressFrameChunk( + const ZSTD_rust_frameChunkState* state, + void* dst, size_t dstCapacity, + const void* src, size_t srcSize, + U32 lastFrameChunk); +typedef char ZSTD_rust_frame_chunk_state_layout[ + (offsetof(ZSTD_rust_frameChunkState, callbackContext) == 0 + && offsetof(ZSTD_rust_frameChunkState, tmpWorkspace) == sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, checksumState) == 2 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, isFirstBlock) == 3 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, stage) == 4 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, tmpWkspSize) == 5 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, blockSizeMax) == 6 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, savings) == 7 * sizeof(void*) + && offsetof(ZSTD_rust_frameChunkState, preBlockSplitterLevel) + == 7 * sizeof(void*) + sizeof(S64) + && offsetof(ZSTD_rust_frameChunkState, strategy) + == 7 * sizeof(void*) + sizeof(S64) + sizeof(int) + && offsetof(ZSTD_rust_frameChunkState, useTargetCBlockSize) + == 7 * sizeof(void*) + sizeof(S64) + 2 * sizeof(int) + && offsetof(ZSTD_rust_frameChunkState, blockSplitterEnabled) + == 7 * sizeof(void*) + sizeof(S64) + 3 * sizeof(int) + && offsetof(ZSTD_rust_frameChunkState, checksumFlag) + == 7 * sizeof(void*) + sizeof(S64) + 4 * sizeof(int) + && offsetof(ZSTD_rust_frameChunkState, endingStage) + == 7 * sizeof(void*) + sizeof(S64) + 5 * sizeof(int) + && offsetof(ZSTD_rust_frameChunkState, prepareBlock) + == 7 * sizeof(void*) + sizeof(S64) + 6 * sizeof(int) + && sizeof(ZSTD_rust_frameChunkState) + == 12 * sizeof(void*) + sizeof(S64) + 6 * sizeof(int)) + ? 1 : -1]; /* The target-sized block body only needs this narrow projection of ZSTD_CCtx. * Matchfinder/window state, sequence-store construction, and outer repeat-mode * cleanup remain in C. */ @@ -2896,11 +2964,59 @@ static void ZSTD_overflowCorrectIfNeeded(ZSTD_MatchState_t* ms, #include "zstd_preSplit.h" -static size_t ZSTD_optimalBlockSize(ZSTD_CCtx* cctx, const void* src, size_t srcSize, size_t blockSizeMax, int splitLevel, ZSTD_strategy strat, S64 savings) +static void ZSTD_rust_frameChunk_prepareBlock(void* context, + const void* src, + size_t blockSize) { - return ZSTD_rust_optimalBlockSize(src, srcSize, blockSizeMax, splitLevel, - (int)strat, savings, - cctx->tmpWorkspace, cctx->tmpWkspSize); + ZSTD_CCtx* const cctx = (ZSTD_CCtx*)context; + ZSTD_MatchState_t* const ms = &cctx->blockState.matchState; + U32 const maxDist = (U32)1 << cctx->appliedParams.cParams.windowLog; + + ZSTD_overflowCorrectIfNeeded( + ms, &cctx->workspace, &cctx->appliedParams, + src, (const BYTE*)src + blockSize); + ZSTD_checkDictValidity( + &ms->window, (const BYTE*)src + blockSize, maxDist, + &ms->loadedDictEnd, &ms->dictMatchState); + ZSTD_window_enforceMaxDist( + &ms->window, src, maxDist, + &ms->loadedDictEnd, &ms->dictMatchState); + + /* Ensure hash/chain table insertion resumes no sooner than lowlimit. */ + if (ms->nextToUpdate < ms->window.lowLimit) + ms->nextToUpdate = ms->window.lowLimit; +} + +static size_t ZSTD_rust_frameChunk_compressTarget( + void* context, void* dst, size_t dstCapacity, + const void* src, size_t srcSize, U32 lastBlock) +{ + return ZSTD_compressBlock_targetCBlockSize( + (ZSTD_CCtx*)context, dst, dstCapacity, src, srcSize, lastBlock); +} + +static size_t ZSTD_rust_frameChunk_compressSplit( + void* context, void* dst, size_t dstCapacity, + const void* src, size_t srcSize, U32 lastBlock) +{ + return ZSTD_compressBlock_splitBlock( + (ZSTD_CCtx*)context, dst, dstCapacity, src, srcSize, lastBlock); +} + +static size_t ZSTD_rust_frameChunk_compressInternal( + void* context, void* dst, size_t dstCapacity, + const void* src, size_t srcSize, U32 lastBlock) +{ + (void)lastBlock; + return ZSTD_compressBlock_internal( + (ZSTD_CCtx*)context, dst, dstCapacity, src, srcSize, + 1 /* frame */); +} + +static void ZSTD_rust_frameChunk_updateChecksum( + void* state, const void* src, size_t srcSize) +{ + (void)XXH64_update((XXH64_state_t*)state, src, srcSize); } /*! ZSTD_compress_frameChunk() : @@ -2915,99 +3031,28 @@ static size_t ZSTD_compress_frameChunk(ZSTD_CCtx* cctx, const void* src, size_t srcSize, U32 lastFrameChunk) { - size_t blockSizeMax = cctx->blockSizeMax; - size_t remaining = srcSize; - const BYTE* ip = (const BYTE*)src; - BYTE* const ostart = (BYTE*)dst; - BYTE* op = ostart; - U32 const maxDist = (U32)1 << cctx->appliedParams.cParams.windowLog; - S64 savings = (S64)cctx->consumedSrcSize - (S64)cctx->producedCSize; - - assert(cctx->appliedParams.cParams.windowLog <= ZSTD_WINDOWLOG_MAX); - - DEBUGLOG(5, "ZSTD_compress_frameChunk (srcSize=%u, blockSizeMax=%u)", (unsigned)srcSize, (unsigned)blockSizeMax); - if (cctx->appliedParams.fParams.checksumFlag && srcSize) - XXH64_update(&cctx->xxhState, src, srcSize); - - while (remaining) { - ZSTD_MatchState_t* const ms = &cctx->blockState.matchState; - size_t const blockSize = ZSTD_optimalBlockSize(cctx, - ip, remaining, - blockSizeMax, - cctx->appliedParams.preBlockSplitter_level, - cctx->appliedParams.cParams.strategy, - savings); - U32 const lastBlock = lastFrameChunk & (blockSize == remaining); - assert(blockSize <= remaining); - - /* 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 */ - RETURN_ERROR_IF(dstCapacity < ZSTD_blockHeaderSize + MIN_CBLOCK_SIZE + 1, - dstSize_tooSmall, - "not enough space to store compressed block"); - - ZSTD_overflowCorrectIfNeeded( - ms, &cctx->workspace, &cctx->appliedParams, ip, ip + blockSize); - ZSTD_checkDictValidity(&ms->window, ip + blockSize, maxDist, &ms->loadedDictEnd, &ms->dictMatchState); - ZSTD_window_enforceMaxDist(&ms->window, ip, maxDist, &ms->loadedDictEnd, &ms->dictMatchState); - - /* Ensure hash/chain table insertion resumes no sooner than lowlimit */ - if (ms->nextToUpdate < ms->window.lowLimit) ms->nextToUpdate = ms->window.lowLimit; - - { size_t cSize; - if (ZSTD_useTargetCBlockSize(&cctx->appliedParams)) { - cSize = ZSTD_compressBlock_targetCBlockSize(cctx, op, dstCapacity, ip, blockSize, lastBlock); - FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_targetCBlockSize failed"); - assert(cSize > 0); - assert(cSize <= blockSize + ZSTD_blockHeaderSize); - } else if (ZSTD_blockSplitterEnabled(&cctx->appliedParams)) { - cSize = ZSTD_compressBlock_splitBlock(cctx, op, dstCapacity, ip, blockSize, lastBlock); - FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_splitBlock failed"); - assert(cSize > 0 || cctx->seqCollector.collectSequences == 1); - } else { - cSize = ZSTD_compressBlock_internal(cctx, - op+ZSTD_blockHeaderSize, dstCapacity-ZSTD_blockHeaderSize, - ip, blockSize, 1 /* frame */); - FORWARD_IF_ERROR(cSize, "ZSTD_compressBlock_internal failed"); - - if (cSize == 0) { /* block is not compressible */ - cSize = ZSTD_rust_noCompressBlock(op, dstCapacity, ip, blockSize, lastBlock); - FORWARD_IF_ERROR(cSize, "ZSTD_noCompressBlock failed"); - } else { - ZSTD_rust_writeBlockHeader(op, cSize, blockSize, lastBlock); - cSize += ZSTD_blockHeaderSize; - } - } /* if (ZSTD_useTargetCBlockSize(&cctx->appliedParams))*/ - - /* @savings is employed to ensure that splitting doesn't worsen expansion of incompressible data. - * Without splitting, the maximum expansion is 3 bytes per full block. - * An adversarial input could attempt to fudge the split detector, - * and make it split incompressible data, resulting in more block headers. - * Note that, since ZSTD_COMPRESSBOUND() assumes a worst case scenario of 1KB per block, - * and the splitter never creates blocks that small (current lower limit is 8 KB), - * there is already no risk to expand beyond ZSTD_COMPRESSBOUND() limit. - * But if the goal is to not expand by more than 3-bytes per 128 KB full block, - * then yes, it becomes possible to make the block splitter oversplit incompressible data. - * Using @savings, we enforce an even more conservative condition, - * requiring the presence of enough savings (at least 3 bytes) to authorize splitting, - * otherwise only full blocks are used. - * But being conservative is fine, - * since splitting barely compressible blocks is not fruitful anyway */ - savings += (S64)blockSize - (S64)cSize; - - ip += blockSize; - assert(remaining >= blockSize); - remaining -= blockSize; - op += cSize; - assert(dstCapacity >= cSize); - dstCapacity -= cSize; - cctx->isFirstBlock = 0; - DEBUGLOG(5, "ZSTD_compress_frameChunk: adding a block of size %u", - (unsigned)cSize); - } } - - if (lastFrameChunk && (op>ostart)) cctx->stage = ZSTDcs_ending; - return (size_t)(op-ostart); + ZSTD_rust_frameChunkState state; + state.callbackContext = cctx; + state.tmpWorkspace = cctx->tmpWorkspace; + state.checksumState = &cctx->xxhState; + state.isFirstBlock = &cctx->isFirstBlock; + state.stage = &cctx->stage; + state.tmpWkspSize = cctx->tmpWkspSize; + state.blockSizeMax = cctx->blockSizeMax; + state.savings = (S64)cctx->consumedSrcSize - (S64)cctx->producedCSize; + state.preBlockSplitterLevel = cctx->appliedParams.preBlockSplitter_level; + state.strategy = (int)cctx->appliedParams.cParams.strategy; + state.useTargetCBlockSize = ZSTD_useTargetCBlockSize(&cctx->appliedParams); + state.blockSplitterEnabled = ZSTD_blockSplitterEnabled(&cctx->appliedParams); + state.checksumFlag = cctx->appliedParams.fParams.checksumFlag; + state.endingStage = (int)ZSTDcs_ending; + state.prepareBlock = ZSTD_rust_frameChunk_prepareBlock; + state.compressTarget = ZSTD_rust_frameChunk_compressTarget; + state.compressSplit = ZSTD_rust_frameChunk_compressSplit; + state.compressInternal = ZSTD_rust_frameChunk_compressInternal; + state.updateChecksum = ZSTD_rust_frameChunk_updateChecksum; + return ZSTD_rust_compressFrameChunk( + &state, dst, dstCapacity, src, srcSize, lastFrameChunk); } diff --git a/rust/src/zstd_compress.rs b/rust/src/zstd_compress.rs index db57ae118..dfd9309ce 100644 --- a/rust/src/zstd_compress.rs +++ b/rust/src/zstd_compress.rs @@ -38,7 +38,7 @@ use crate::zstd_compress_stats::{ use crate::zstd_compress_superblock::ZSTD_rust_compressSuperBlock; use std::ffi::c_void; use std::mem::{offset_of, size_of, MaybeUninit}; -use std::os::raw::{c_int, c_uint}; +use std::os::raw::{c_int, c_longlong, c_uint}; use std::ptr; #[cfg(not(test))] @@ -92,7 +92,8 @@ const ZSTD_BLOCKSIZE_MAX: usize = 1 << 17; const ZSTD_CONTENTSIZE_UNKNOWN: u64 = u64::MAX; const ZSTD_TARGET_CBLOCK_BSS_COMPRESS: c_int = 0; const ZSTD_BLOCK_HEADER_SIZE: usize = 3; -const MIN_COMPRESSIBLE_BLOCK_SIZE: usize = 2 + ZSTD_BLOCK_HEADER_SIZE + 1 + 1; +const MIN_CBLOCK_SIZE: usize = 2; +const MIN_COMPRESSIBLE_BLOCK_SIZE: usize = MIN_CBLOCK_SIZE + ZSTD_BLOCK_HEADER_SIZE + 1 + 1; const ZSTD_ROWSIZE: usize = 16; const ZSTD_CSTREAM_STAGE_INIT: c_int = 0; const ZSTD_WINDOW_START_INDEX: u32 = 2; @@ -108,6 +109,237 @@ const ZSTD_CHUNKSIZE_MAX: usize = u32::MAX as usize - ZSTD_CURRENT_MAX; #[cfg(not(test))] const ZSTD_E_END: c_int = 2; +type FrameChunkPrepareFn = unsafe extern "C" fn(*mut c_void, *const c_void, usize); +type FrameChunkCompressFn = + unsafe extern "C" fn(*mut c_void, *mut c_void, usize, *const c_void, usize, c_uint) -> usize; +type FrameChunkChecksumFn = unsafe extern "C" fn(*mut c_void, *const c_void, usize); + +/// Explicit projection of the state used by `ZSTD_compress_frameChunk`. +/// +/// The Rust side owns the per-frame block loop and its savings/dispatch +/// policy. The callback context is opaque to Rust and is only returned to +/// C-owned callbacks, which retain private window, workspace, and CCtx +/// layout-sensitive operations. +#[repr(C)] +pub struct ZSTD_rust_frameChunkState { + callback_context: *mut c_void, + tmp_workspace: *mut c_void, + checksum_state: *mut c_void, + is_first_block: *mut c_int, + stage: *mut c_int, + tmp_wksp_size: usize, + block_size_max: usize, + savings: c_longlong, + pre_block_splitter_level: c_int, + strategy: c_int, + use_target_c_block_size: c_int, + block_splitter_enabled: c_int, + checksum_flag: c_int, + ending_stage: c_int, + prepare_block: FrameChunkPrepareFn, + compress_target: FrameChunkCompressFn, + compress_split: FrameChunkCompressFn, + compress_internal: FrameChunkCompressFn, + update_checksum: FrameChunkChecksumFn, +} + +const _: () = { + assert!(offset_of!(ZSTD_rust_frameChunkState, callback_context) == 0); + assert!(offset_of!(ZSTD_rust_frameChunkState, tmp_workspace) == size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, checksum_state) == 2 * size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, is_first_block) == 3 * size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, stage) == 4 * size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, tmp_wksp_size) == 5 * size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, block_size_max) == 6 * size_of::()); + assert!(offset_of!(ZSTD_rust_frameChunkState, savings) == 7 * size_of::()); + assert!( + offset_of!(ZSTD_rust_frameChunkState, pre_block_splitter_level) + == 7 * size_of::() + size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, strategy) + == 7 * size_of::() + size_of::() + size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, use_target_c_block_size) + == 7 * size_of::() + size_of::() + 2 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, block_splitter_enabled) + == 7 * size_of::() + size_of::() + 3 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, checksum_flag) + == 7 * size_of::() + size_of::() + 4 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, ending_stage) + == 7 * size_of::() + size_of::() + 5 * size_of::() + ); + assert!( + offset_of!(ZSTD_rust_frameChunkState, prepare_block) + == 7 * size_of::() + size_of::() + 6 * size_of::() + ); + assert!( + size_of::() + == 12 * size_of::() + size_of::() + 6 * size_of::() + ); +}; + +/// Rust implementation of `ZSTD_compress_frameChunk`. +/// +/// C still prepares match-state windows and invokes the selected block body +/// through callbacks. Rust owns the block-size heuristic call, output +/// framing, savings accounting, checksum sequencing, and frame-state update. +#[allow(clippy::too_many_arguments)] +unsafe fn compress_frame_chunk_body_with( + state: &ZSTD_rust_frameChunkState, + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, + last_frame_chunk: c_uint, +) -> usize { + if state.is_first_block.is_null() || state.stage.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + + if state.checksum_flag != 0 && src_size != 0 { + if state.checksum_state.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + unsafe { (state.update_checksum)(state.checksum_state, src, src_size) }; + } + + let mut remaining = src_size; + let mut ip = src.cast::(); + let mut op = dst.cast::(); + let mut remaining_capacity = dst_capacity; + let mut savings = state.savings; + let mut compressed_size_total = 0usize; + + while remaining != 0 { + let block_size = unsafe { + crate::zstd_compress_frame::ZSTD_rust_optimalBlockSize( + ip.cast(), + remaining, + state.block_size_max, + state.pre_block_splitter_level, + state.strategy, + savings, + state.tmp_workspace, + state.tmp_wksp_size, + ) + }; + if block_size == 0 || block_size > remaining { + return ERROR(ZstdErrorCode::Generic); + } + + let last_block = last_frame_chunk & u32::from(block_size == remaining); + + /* Keep the original early capacity guard: even a raw block needs the + * minimum block header plus the minimum compressible payload budget. */ + if remaining_capacity < ZSTD_BLOCK_HEADER_SIZE + MIN_CBLOCK_SIZE + 1 { + return ERROR(ZstdErrorCode::DstSizeTooSmall); + } + + unsafe { (state.prepare_block)(state.callback_context, ip.cast(), block_size) }; + + let c_size = if state.use_target_c_block_size != 0 { + unsafe { + (state.compress_target)( + state.callback_context, + op.cast(), + remaining_capacity, + ip.cast(), + block_size, + last_block, + ) + } + } else if state.block_splitter_enabled != 0 { + unsafe { + (state.compress_split)( + state.callback_context, + op.cast(), + remaining_capacity, + ip.cast(), + block_size, + last_block, + ) + } + } else { + let compressed_size = unsafe { + (state.compress_internal)( + state.callback_context, + op.add(ZSTD_BLOCK_HEADER_SIZE).cast(), + remaining_capacity - ZSTD_BLOCK_HEADER_SIZE, + ip.cast(), + block_size, + last_block, + ) + }; + if ERR_isError(compressed_size) { + return compressed_size; + } + + if compressed_size == 0 { + unsafe { + ZSTD_rust_noCompressBlock( + op.cast(), + remaining_capacity, + ip.cast(), + block_size, + last_block, + ) + } + } else { + unsafe { + ZSTD_rust_writeBlockHeader(op.cast(), compressed_size, block_size, last_block) + }; + compressed_size + ZSTD_BLOCK_HEADER_SIZE + } + }; + + if ERR_isError(c_size) { + return c_size; + } + + savings = + savings.wrapping_add((block_size as c_longlong).wrapping_sub(c_size as c_longlong)); + unsafe { + ip = ip.add(block_size); + op = op.add(c_size); + } + remaining -= block_size; + debug_assert!(c_size <= remaining_capacity); + remaining_capacity = remaining_capacity.wrapping_sub(c_size); + compressed_size_total = compressed_size_total.wrapping_add(c_size); + unsafe { *state.is_first_block = 0 }; + } + + if last_frame_chunk != 0 && compressed_size_total != 0 { + unsafe { *state.stage = state.ending_stage }; + } + compressed_size_total +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_compressFrameChunk( + state: *const ZSTD_rust_frameChunkState, + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, + last_frame_chunk: c_uint, +) -> usize { + if state.is_null() { + return ERROR(ZstdErrorCode::Generic); + } + unsafe { + compress_frame_chunk_body_with(&*state, dst, dst_capacity, src, src_size, last_frame_chunk) + } +} + /// Explicit projection of the state used by `ZSTD_compressSequences_internal`. /// /// The C context and its function-pointer-bearing parameter structure remain @@ -2675,6 +2907,260 @@ mod tests { output } + #[derive(Default)] + struct FrameChunkTestContext { + prepare_calls: usize, + prepared_sizes: [usize; 4], + target_calls: usize, + split_calls: usize, + internal_calls: usize, + last_blocks: [c_uint; 4], + checksum_calls: usize, + checksum_size: usize, + target_result: usize, + split_result: usize, + internal_result: usize, + } + + unsafe fn frame_chunk_test_context(context: *mut c_void) -> &'static mut FrameChunkTestContext { + unsafe { &mut *context.cast::() } + } + + unsafe extern "C" fn frame_chunk_test_prepare( + context: *mut c_void, + _src: *const c_void, + block_size: usize, + ) { + let context = unsafe { frame_chunk_test_context(context) }; + if context.prepare_calls < context.prepared_sizes.len() { + context.prepared_sizes[context.prepare_calls] = block_size; + } + context.prepare_calls += 1; + } + + unsafe extern "C" fn frame_chunk_test_target( + context: *mut c_void, + _dst: *mut c_void, + _dst_capacity: usize, + _src: *const c_void, + _src_size: usize, + last_block: c_uint, + ) -> usize { + let context = unsafe { frame_chunk_test_context(context) }; + if context.target_calls < context.last_blocks.len() { + context.last_blocks[context.target_calls] = last_block; + } + context.target_calls += 1; + context.target_result + } + + unsafe extern "C" fn frame_chunk_test_split( + context: *mut c_void, + _dst: *mut c_void, + _dst_capacity: usize, + _src: *const c_void, + _src_size: usize, + last_block: c_uint, + ) -> usize { + let context = unsafe { frame_chunk_test_context(context) }; + if context.split_calls < context.last_blocks.len() { + context.last_blocks[context.split_calls] = last_block; + } + context.split_calls += 1; + context.split_result + } + + unsafe extern "C" fn frame_chunk_test_internal( + context: *mut c_void, + _dst: *mut c_void, + _dst_capacity: usize, + _src: *const c_void, + _src_size: usize, + last_block: c_uint, + ) -> usize { + let context = unsafe { frame_chunk_test_context(context) }; + if context.internal_calls < context.last_blocks.len() { + context.last_blocks[context.internal_calls] = last_block; + } + context.internal_calls += 1; + context.internal_result + } + + unsafe extern "C" fn frame_chunk_test_checksum( + context: *mut c_void, + _src: *const c_void, + src_size: usize, + ) { + let context = unsafe { frame_chunk_test_context(context) }; + context.checksum_calls += 1; + context.checksum_size = context.checksum_size.wrapping_add(src_size); + } + + fn frame_chunk_test_state( + context: &mut FrameChunkTestContext, + is_first_block: &mut c_int, + stage: &mut c_int, + use_target_c_block_size: c_int, + block_splitter_enabled: c_int, + checksum_flag: c_int, + ) -> ZSTD_rust_frameChunkState { + let context = context as *mut FrameChunkTestContext as *mut c_void; + ZSTD_rust_frameChunkState { + callback_context: context, + tmp_workspace: ptr::null_mut(), + checksum_state: context, + is_first_block, + stage, + tmp_wksp_size: 0, + block_size_max: 4, + savings: 0, + pre_block_splitter_level: 1, + strategy: ZSTD_FAST, + use_target_c_block_size, + block_splitter_enabled, + checksum_flag, + ending_stage: 77, + prepare_block: frame_chunk_test_prepare, + compress_target: frame_chunk_test_target, + compress_split: frame_chunk_test_split, + compress_internal: frame_chunk_test_internal, + update_checksum: frame_chunk_test_checksum, + } + } + + #[test] + fn frame_chunk_internal_path_emits_blocks_and_updates_state_once() { + let mut context = FrameChunkTestContext { + internal_result: 2, + ..FrameChunkTestContext::default() + }; + let mut is_first_block = 1; + let mut stage = 0; + let state = frame_chunk_test_state(&mut context, &mut is_first_block, &mut stage, 0, 0, 1); + let source = [0x11u8; 8]; + let mut output = [0xa5u8; 16]; + + let result = unsafe { + compress_frame_chunk_body_with( + &state, + output.as_mut_ptr().cast(), + output.len(), + source.as_ptr().cast(), + source.len(), + 1, + ) + }; + + assert_eq!(result, 10); + assert_eq!(context.prepare_calls, 2); + assert_eq!(context.prepared_sizes[..2], [4, 4]); + assert_eq!(context.internal_calls, 2); + assert_eq!(context.last_blocks[..2], [0, 1]); + assert_eq!(context.checksum_calls, 1); + assert_eq!(context.checksum_size, source.len()); + assert_eq!(is_first_block, 0); + assert_eq!(stage, 77); + assert_ne!(output[..3], [0xa5; 3]); + assert_ne!(output[5..8], [0xa5; 3]); + } + + #[test] + fn frame_chunk_dispatches_target_and_split_paths_before_internal() { + let source = [0x22u8; 4]; + + let mut target_context = FrameChunkTestContext { + target_result: 4, + split_result: 5, + internal_result: 6, + ..FrameChunkTestContext::default() + }; + let mut target_first = 1; + let mut target_stage = 0; + let target_state = frame_chunk_test_state( + &mut target_context, + &mut target_first, + &mut target_stage, + 1, + 1, + 0, + ); + let mut target_output = [0xa5u8; 8]; + let target_result = unsafe { + compress_frame_chunk_body_with( + &target_state, + target_output.as_mut_ptr().cast(), + target_output.len(), + source.as_ptr().cast(), + source.len(), + 0, + ) + }; + assert_eq!(target_result, 4); + assert_eq!(target_context.target_calls, 1); + assert_eq!(target_context.split_calls, 0); + assert_eq!(target_context.internal_calls, 0); + + let mut split_context = FrameChunkTestContext { + split_result: 5, + internal_result: 6, + ..FrameChunkTestContext::default() + }; + let mut split_first = 1; + let mut split_stage = 0; + let split_state = frame_chunk_test_state( + &mut split_context, + &mut split_first, + &mut split_stage, + 0, + 1, + 0, + ); + let mut split_output = [0xa5u8; 8]; + let split_result = unsafe { + compress_frame_chunk_body_with( + &split_state, + split_output.as_mut_ptr().cast(), + split_output.len(), + source.as_ptr().cast(), + source.len(), + 0, + ) + }; + assert_eq!(split_result, 5); + assert_eq!(split_context.target_calls, 0); + assert_eq!(split_context.split_calls, 1); + assert_eq!(split_context.internal_calls, 0); + } + + #[test] + fn frame_chunk_keeps_state_unchanged_on_early_capacity_error() { + let mut context = FrameChunkTestContext::default(); + let mut is_first_block = 1; + let mut stage = 23; + let state = frame_chunk_test_state(&mut context, &mut is_first_block, &mut stage, 0, 0, 1); + let source = [0x33u8; 4]; + let mut output = [0xa5u8; 5]; + + let result = unsafe { + compress_frame_chunk_body_with( + &state, + output.as_mut_ptr().cast(), + output.len(), + source.as_ptr().cast(), + source.len(), + 1, + ) + }; + + assert_eq!(result, ERROR(ZstdErrorCode::DstSizeTooSmall)); + assert_eq!(context.checksum_calls, 1); + assert_eq!(context.prepare_calls, 0); + assert_eq!(context.internal_calls, 0); + assert_eq!(is_first_block, 1); + assert_eq!(stage, 23); + assert_eq!(output, [0xa5; 5]); + } + #[test] fn simple_strategy_follows_source_size_tiers() { assert_eq!(ZSTD_rust_compressCCtxStrategy(0, 1), ZSTD_FAST);