feat(compress): port simple context compression to Rust

Move ZSTD_compressCCtx() onto the Rust frame-compression path while
retaining only the C-owned reset needed for its private context layout.
The simple API ignores advanced context parameters by contract, so the
Rust entry point can share the one-shot implementation after reset.

Preserve valid frames when the Rust superblock leaf declines a compressed
block by emitting a raw block instead. Match the fast-strategy literal
policy for negative levels so compatibility behavior remains intact.

Streaming, stateful ZSTD_compress2(), and multithreaded context
orchestration remain C-owned for a later slice. The full fuzzer currently
reaches an existing compress2() superblock-expansion failure after these
simple-API checks; that stateful path is intentionally out of scope here.

Test Plan:
- cargo test --manifest-path rust/Cargo.toml --no-default-features
  --features compression zstd_compress::tests
- cargo clippy --manifest-path rust/Cargo.toml -- -D warnings
  (library, benches, and tests)
- cargo +nightly fmt --manifest-path rust/Cargo.toml -- --check
- make -B -C programs zstd V=1
- make -B -C examples multiple_simple_compression
- ./multiple_simple_compression README.md ../rust/README.md
This commit is contained in:
2026-07-12 18:48:44 +02:00
parent 6711aef1d4
commit e66ad5f563
2 changed files with 122 additions and 11 deletions
+7 -7
View File
@@ -37,6 +37,7 @@ size_t ZSTD_rust_writeFrameHeader(void* dst, size_t dstCapacity,
int contentSizeFlag, int format,
U32 windowLog, U64 pledgedSrcSize,
U32 dictID);
size_t ZSTD_rust_resetCCtxForSimpleCompression(void* cctx);
/* Context-free compression-parameter selection and sizing leaves live in
* Rust (rust/src/zstd_compress_params.rs). This file retains
@@ -4462,14 +4463,13 @@ size_t ZSTD_compress_usingDict(ZSTD_CCtx* cctx,
return ZSTD_compress_advanced_internal(cctx, dst, dstCapacity, src, srcSize, dict, dictSize, &cctx->simpleApiParams);
}
size_t ZSTD_compressCCtx(ZSTD_CCtx* cctx,
void* dst, size_t dstCapacity,
const void* src, size_t srcSize,
int compressionLevel)
/* ZSTD_compressCCtx() is implemented by rust/src/zstd_compress.rs. */
/* The Rust simple API still needs the C-owned context reset, but does not
* cross the private context layout. */
size_t ZSTD_rust_resetCCtxForSimpleCompression(void* cctx)
{
DEBUGLOG(4, "ZSTD_compressCCtx (srcSize=%u)", (unsigned)srcSize);
assert(cctx != NULL);
return ZSTD_compress_usingDict(cctx, dst, dstCapacity, src, srcSize, NULL, 0, compressionLevel);
return ZSTD_CCtx_reset((ZSTD_CCtx*)cctx, ZSTD_reset_session_and_parameters);
}
/* ZSTD_compress() is implemented by rust/src/zstd_compress.rs. */
+115 -4
View File
@@ -6,11 +6,13 @@
//! First high-level compression slice.
//!
//! The layout-independent one-shot entry point in this module drives the
//! already migrated compression leaves. The public context lifecycle,
//! `ZSTD_compressCCtx`, and streaming APIs remain C-owned: they share the
//! already migrated compression leaves. `ZSTD_compressCCtx` uses the same
//! path after a narrow C-owned context reset; the public context lifecycle
//! and streaming APIs remain C-owned because they share the
//! configuration-dependent private `ZSTD_CCtx_s` layout.
use crate::errors::{ERR_isError, ZstdErrorCode, ERROR};
use crate::mem::MEM_writeLE24;
use crate::zstd_compress_frame::{ZSTD_rust_writeFrameHeader, ZSTD_writeLastEmptyBlock};
use crate::zstd_compress_params::{
ZSTD_rust_params_adjustCParams, ZSTD_rust_params_maxNbSeq, ZSTD_rust_params_selectCParams,
@@ -24,6 +26,12 @@ use std::mem::{size_of, MaybeUninit};
use std::os::raw::c_int;
use std::ptr;
#[cfg(not(test))]
unsafe extern "C" {
fn ZSTD_rust_resetCCtxForSimpleCompression(cctx: *mut c_void) -> usize;
}
const ZSTD_FAST: c_int = 1;
const ZSTD_DFAST: c_int = 2;
const ZSTD_BLOCKSIZE_MAX: usize = 1 << 17;
const ZSTD_CONTENTSIZE_UNKNOWN: u64 = u64::MAX;
@@ -54,6 +62,32 @@ fn ceil_log2(size: usize) -> u32 {
}
}
unsafe fn write_raw_block(
dst: *mut u8,
dst_capacity: usize,
src: *const u8,
src_size: usize,
last_block: u32,
) -> usize {
let needed = match src_size.checked_add(3) {
Some(value) => value,
None => return ERROR(ZstdErrorCode::DstSizeTooSmall),
};
if needed > dst_capacity {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
unsafe {
MEM_writeLE24(
dst.cast(),
last_block.wrapping_add((src_size as u32).wrapping_shl(3)),
);
if src_size != 0 {
ptr::copy_nonoverlapping(src, dst.add(3), src_size);
}
}
needed
}
/// Compress one frame using the already migrated block leaves.
///
/// This path deliberately starts a fresh match table for each 128 KiB block.
@@ -143,6 +177,8 @@ unsafe fn compress_frame(
let max_nb_seq =
ZSTD_rust_params_maxNbSeq(ZSTD_BLOCKSIZE_MAX, cparams.minMatch, 0).saturating_add(1);
let disable_literal_compression =
c_int::from(cparams.strategy == ZSTD_FAST && cparams.targetLength > 0);
let mut sequences = vec![SeqDef::default(); max_nb_seq];
let mut literals = vec![0u8; ZSTD_BLOCKSIZE_MAX];
let mut ll_codes = vec![0u8; max_nb_seq];
@@ -239,13 +275,13 @@ unsafe fn compress_frame(
let last_block = u32::from(input_offset + block_size == src_size);
let remaining_capacity = dst_capacity.saturating_sub(output_offset);
let written = unsafe {
let mut written = unsafe {
ZSTD_rust_compressSuperBlock(
(seq_store as *mut SeqStore_t).cast(),
(&prev_block as *const ZSTD_compressedBlockState_t).cast(),
(&mut next_block as *mut ZSTD_compressedBlockState_t).cast(),
cparams.strategy,
0, /* literals compression enabled */
disable_literal_compression,
workspace.as_mut_ptr().cast(),
TMP_WORKSPACE_SIZE,
0, /* BMI2 is optional; portable Rust leaf path */
@@ -261,6 +297,20 @@ unsafe fn compress_frame(
if ERR_isError(written) {
return written;
}
if written == 0 {
written = unsafe {
write_raw_block(
output.add(output_offset),
remaining_capacity,
block_src,
block_size,
last_block,
)
};
if ERR_isError(written) {
return written;
}
}
if written > remaining_capacity {
return ERROR(ZstdErrorCode::DstSizeTooSmall);
}
@@ -286,6 +336,34 @@ pub unsafe extern "C" fn ZSTD_compress(
unsafe { compress_frame(dst, dst_capacity, src, src_size, compression_level) }
}
/// Simple explicit-context compression entry point.
///
/// The public contract deliberately ignores all advanced context parameters.
/// C performs the context reset because the private `ZSTD_CCtx_s` layout is
/// still configuration-dependent; the frame compressor itself is entirely
/// Rust-owned and does not inspect the context.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_compressCCtx(
cctx: *mut c_void,
dst: *mut c_void,
dst_capacity: usize,
src: *const c_void,
src_size: usize,
compression_level: c_int,
) -> usize {
if cctx.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
#[cfg(not(test))]
{
let reset = unsafe { ZSTD_rust_resetCCtxForSimpleCompression(cctx) };
if ERR_isError(reset) {
return reset;
}
}
unsafe { compress_frame(dst, dst_capacity, src, src_size, compression_level) }
}
#[cfg(test)]
mod tests {
use super::*;
@@ -333,8 +411,41 @@ mod tests {
fn public_one_shot_abi_is_c_compatible() {
let entry: unsafe extern "C" fn(*mut c_void, usize, *const c_void, usize, c_int) -> usize =
ZSTD_compress;
let context_entry: unsafe extern "C" fn(
*mut c_void,
*mut c_void,
usize,
*const c_void,
usize,
c_int,
) -> usize = ZSTD_compressCCtx;
assert_eq!(size_of::<usize>(), size_of::<*const c_void>());
let _ = entry;
let _ = context_entry;
}
#[test]
fn explicit_context_simple_api_matches_one_shot_path() {
let input = b"explicit context compression remains a simple API";
let capacity = crate::zstd_compress_api::ZSTD_compressBound(input.len());
let mut output = vec![0u8; capacity];
let written = unsafe {
ZSTD_compressCCtx(
std::ptr::dangling_mut::<c_void>(),
output.as_mut_ptr().cast(),
output.len(),
input.as_ptr().cast(),
input.len(),
3,
)
};
assert!(!ERR_isError(written));
output.truncate(written);
let one_shot = compress_input(input, 3);
assert_eq!(output, one_shot);
if let Some(restored) = system_round_trip(&output) {
assert_eq!(restored, input);
}
}
#[test]