feat(compress): move frame epilogue serialization to Rust

Port the scalar frame-finalization policy and byte serialization into the Rust compression leaf while keeping checksum finalization, tracing, and the compression context in C. Preserve the init-to-ongoing transition on partial failure and reset the stage only after a successful epilogue.

Test Plan:

- cargo test --manifest-path rust/Cargo.toml --no-default-features --features compression

- cargo clippy --manifest-path rust/Cargo.toml

- cargo clippy --manifest-path rust/Cargo.toml --benches

- cargo clippy --manifest-path rust/Cargo.toml --tests

- make -B -C lib -j2 lib

- make -B -C tests -j2 test-zstream (84 deterministic, 6737 and 9012 fuzzer cases)
This commit is contained in:
2026-07-18 05:49:37 +02:00
parent 52c0eb4b9b
commit 459782ad55
2 changed files with 189 additions and 34 deletions
+16 -33
View File
@@ -37,6 +37,10 @@ size_t ZSTD_rust_writeFrameHeader(void* dst, size_t dstCapacity,
int contentSizeFlag, int format,
U32 windowLog, U64 pledgedSrcSize,
U32 dictID);
size_t ZSTD_rust_writeEpilogue(void* dst, size_t dstCapacity, int* stage,
int noDictIDFlag, int checksumFlag,
int contentSizeFlag, int format, U32 windowLog,
U32 checksum);
size_t ZSTD_rust_resetCCtxForSimpleCompression(void* cctx);
size_t ZSTD_rust_prepareCCtxForSimpleCompression(void* cctx,
size_t srcSize,
@@ -3938,41 +3942,20 @@ size_t ZSTD_compressBegin(ZSTD_CCtx* cctx, int compressionLevel)
* @return : nb of bytes written into dst (or an error code) */
static size_t ZSTD_writeEpilogue(ZSTD_CCtx* cctx, void* dst, size_t dstCapacity)
{
BYTE* const ostart = (BYTE*)dst;
BYTE* op = ostart;
U32 const checksum = cctx->appliedParams.fParams.checksumFlag
? (U32)XXH64_digest(&cctx->xxhState)
: 0;
DEBUGLOG(4, "ZSTD_writeEpilogue");
RETURN_ERROR_IF(cctx->stage == ZSTDcs_created, stage_wrong, "init missing");
/* special case : empty frame */
if (cctx->stage == ZSTDcs_init) {
size_t fhSize = ZSTD_writeFrameHeader(dst, dstCapacity, &cctx->appliedParams, 0, 0);
FORWARD_IF_ERROR(fhSize, "ZSTD_writeFrameHeader failed");
dstCapacity -= fhSize;
op += fhSize;
cctx->stage = ZSTDcs_ongoing;
}
if (cctx->stage != ZSTDcs_ending) {
/* write one last empty block, make it the "last" block */
U32 const cBlockHeader24 = 1 /* last block */ + (((U32)bt_raw)<<1) + 0;
ZSTD_STATIC_ASSERT(ZSTD_BLOCKHEADERSIZE == 3);
RETURN_ERROR_IF(dstCapacity<3, dstSize_tooSmall, "no room for epilogue");
MEM_writeLE24(op, cBlockHeader24);
op += ZSTD_blockHeaderSize;
dstCapacity -= ZSTD_blockHeaderSize;
}
if (cctx->appliedParams.fParams.checksumFlag) {
U32 const checksum = (U32) XXH64_digest(&cctx->xxhState);
RETURN_ERROR_IF(dstCapacity<4, dstSize_tooSmall, "no room for checksum");
if (cctx->appliedParams.fParams.checksumFlag)
DEBUGLOG(4, "ZSTD_writeEpilogue: write checksum : %08X", (unsigned)checksum);
MEM_writeLE32(op, checksum);
op += 4;
}
cctx->stage = ZSTDcs_created; /* return to "created but no init" status */
return (size_t)(op-ostart);
return ZSTD_rust_writeEpilogue(
dst, dstCapacity, (int*)&cctx->stage,
cctx->appliedParams.fParams.noDictIDFlag,
cctx->appliedParams.fParams.checksumFlag,
cctx->appliedParams.fParams.contentSizeFlag,
(int)cctx->appliedParams.format,
cctx->appliedParams.cParams.windowLog,
checksum);
}
void ZSTD_CCtx_trace(ZSTD_CCtx* cctx, size_t extraCSize)
+173 -1
View File
@@ -8,7 +8,7 @@
//! five scalar frame parameters they need, so no `ZSTD_CCtx_params` layout
//! crosses the language boundary.
use crate::errors::{ZstdErrorCode, ERROR};
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
use crate::zstd_presplit::ZSTD_splitBlock;
use std::ffi::c_void;
use std::os::raw::{c_int, c_longlong, c_uint};
@@ -26,6 +26,10 @@ const ZSTD_BLOCK_SIZE: usize = 128 << 10;
const ZSTD_FAST: c_int = 1;
const ZSTD_BTULTRA2: c_int = 9;
const SPLIT_LEVELS: [c_int; 10] = [0, 0, 1, 2, 2, 3, 3, 4, 4, 4];
const ZSTDCS_CREATED: c_int = 0;
const ZSTDCS_INIT: c_int = 1;
const ZSTDCS_ONGOING: c_int = 2;
const ZSTDCS_ENDING: c_int = 3;
#[inline]
unsafe fn write_le16(dst: *mut u8, value: u16) {
@@ -250,6 +254,82 @@ pub unsafe extern "C" fn ZSTD_rust_optimalBlockSize(
unsafe { ZSTD_splitBlock(src, block_size_max, split_level, workspace, workspace_size) }
}
/// Rust implementation of the private `ZSTD_writeEpilogue()` serializer.
///
/// The C caller owns the compression context and passes its stage by scalar
/// pointer so the transition to `ongoing` after an empty-frame header remains
/// visible even when a later write fails. The checksum is already finalized
/// by C; Rust only serializes its low 32 bits.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_writeEpilogue(
dst: *mut c_void,
dst_capacity: usize,
stage: *mut c_int,
no_dict_id_flag: c_int,
checksum_flag: c_int,
content_size_flag: c_int,
format: c_int,
window_log: c_uint,
checksum: u32,
) -> usize {
debug_assert!(!stage.is_null());
if stage.is_null() {
return ERROR(ZstdErrorCode::StageWrong);
}
let mut stage_value = unsafe { stage.read() };
if stage_value == ZSTDCS_CREATED {
return ERROR(ZstdErrorCode::StageWrong);
}
let start = dst.cast::<u8>();
let mut output = start;
let mut capacity = dst_capacity;
if stage_value == ZSTDCS_INIT {
let header_size = unsafe {
ZSTD_rust_writeFrameHeader(
output.cast(),
capacity,
no_dict_id_flag,
checksum_flag,
content_size_flag,
format,
window_log,
0,
0,
)
};
if ERR_isError(header_size) {
return header_size;
}
output = unsafe { output.add(header_size) };
capacity -= header_size;
stage_value = ZSTDCS_ONGOING;
unsafe { stage.write(stage_value) };
}
if stage_value != ZSTDCS_ENDING {
let block_size = unsafe { ZSTD_writeLastEmptyBlock(output.cast(), capacity) };
if ERR_isError(block_size) {
return block_size;
}
output = unsafe { output.add(block_size) };
capacity -= block_size;
}
if checksum_flag != 0 {
if capacity < 4 {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
unsafe { write_le32(output, checksum) };
output = unsafe { output.add(4) };
}
unsafe { stage.write(ZSTDCS_CREATED) };
output as usize - start as usize
}
#[cfg(test)]
mod tests {
use super::*;
@@ -530,4 +610,96 @@ mod tests {
assert!(result > 0);
assert!(result <= ZSTD_BLOCK_SIZE);
}
#[test]
fn epilogue_rejects_created_stage() {
let mut output = [0u8; ZSTD_FRAMEHEADERSIZE_MAX];
let mut stage = ZSTDCS_CREATED;
let result = unsafe {
ZSTD_rust_writeEpilogue(
output.as_mut_ptr().cast(),
output.len(),
&mut stage,
0,
0,
1,
ZSTD_F_ZSTD1,
20,
0,
)
};
assert_eq!(ERR_getErrorCode(result), ZstdErrorCode::StageWrong as i32);
assert_eq!(stage, ZSTDCS_CREATED);
}
#[test]
fn epilogue_writes_empty_frame_and_resets_stage() {
let mut output = [0u8; 18];
let mut stage = ZSTDCS_INIT;
let result = unsafe {
ZSTD_rust_writeEpilogue(
output.as_mut_ptr().cast(),
output.len(),
&mut stage,
0,
0,
1,
ZSTD_F_ZSTD1,
20,
0,
)
};
assert_eq!(result, 9);
assert_eq!(
&output[..result],
&[0x28, 0xB5, 0x2F, 0xFD, 0x20, 0x00, 1, 0, 0]
);
assert_eq!(stage, ZSTDCS_CREATED);
}
#[test]
fn epilogue_ending_stage_writes_only_checksum() {
let mut output = [0u8; 4];
let mut stage = ZSTDCS_ENDING;
let result = unsafe {
ZSTD_rust_writeEpilogue(
output.as_mut_ptr().cast(),
output.len(),
&mut stage,
0,
1,
0,
ZSTD_F_ZSTD1,
10,
0x1234_5678,
)
};
assert_eq!(result, 4);
assert_eq!(output, [0x78, 0x56, 0x34, 0x12]);
assert_eq!(stage, ZSTDCS_CREATED);
}
#[test]
fn epilogue_preserves_ongoing_stage_when_checksum_does_not_fit() {
let mut output = [0u8; 6];
let mut stage = ZSTDCS_ONGOING;
let result = unsafe {
ZSTD_rust_writeEpilogue(
output.as_mut_ptr().cast(),
output.len(),
&mut stage,
0,
1,
0,
ZSTD_F_ZSTD1,
10,
0x1234_5678,
)
};
assert_eq!(
ERR_getErrorCode(result),
ZstdErrorCode::DstSizeTooSmall as i32
);
assert_eq!(stage, ZSTDCS_ONGOING);
}
}