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
+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);
}
}