feat(compress): move CCtx reset planning into Rust

Move the scalar planning portion of ZSTD_resetCCtx_internal across an
explicit C/Rust boundary. Rust now computes window and block sizes, sequence
capacities, buffered I/O sizes, LDM and external-sequence reservations, the
index-reset policy, and the CCtx workspace estimate. C retains private
workspace resizing, reservations, pointer publication, and layout-dependent
callbacks.

Add ABI layout assertions and focused planner tests for buffered sizing, LDM
and index-reset policy, external sequence capacity, and invalid unadjusted LDM
inputs. Keep the migration boundary documented so the hybrid state remains
explicit while deeper CCtx reset and matchfinder operations stay in C.

Test Plan:
- `cargo fmt --manifest-path rust/Cargo.toml -- --check`
- capped focused planner tests: 3 passed
- capped full Rust library tests: 680 passed, 0 failed
- capped `cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings`: passed
- capped native `make -B -C programs -j1 zstd`: passed; only known fileio const-cast warnings
- capped `make -C tests -j1 test-zstream ZSTREAM_TESTTIME=-T1s`: 84 tests and both short fuzzer rounds passed; only known zstream initializer warning
This commit is contained in:
2026-07-19 17:56:41 +02:00
parent 127dd21195
commit 853792f2c4
3 changed files with 441 additions and 31 deletions
+307
View File
@@ -5849,6 +5849,177 @@ pub unsafe extern "C" fn ZSTD_rust_estimateCCtxWorkspaceSize(
)
}
/// Scalar outputs computed while resetting a private C compression context.
///
/// C still owns workspace resizing and all private reservations. Rust owns
/// this policy projection so the reset formula is testable without exposing
/// `ZSTD_CCtx` or `ZSTD_cwksp` layouts across the ABI.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ZSTD_rustCCtxResetPlan {
pub windowSize: usize,
pub blockSize: usize,
pub maxNbSeq: usize,
pub buffInSize: usize,
pub buffOutSize: usize,
pub maxNbLdmSeq: usize,
pub maxNbExternalSeq: usize,
pub neededSpace: usize,
pub needsIndexReset: c_int,
}
/// Scalar inputs needed to plan a private C compression-context reset.
///
/// The C adapter supplies resolved parameters, private object sizes, and the
/// already-reduced index/dictionary predicates. No private C pointer or
/// layout is passed through this record.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct ZSTD_rustCCtxResetState {
pub cParams: ZSTD_compressionParameters,
pub ldmEnable: c_int,
pub ldmHashLog: c_uint,
pub ldmBucketSizeLog: c_uint,
pub ldmMinMatchLength: c_uint,
pub isStatic: c_int,
pub useRowMatchFinder: c_int,
pub inBufferBuffered: c_int,
pub outBufferBuffered: c_int,
pub useSequenceProducer: c_int,
pub initialized: c_int,
pub indexTooClose: c_int,
pub dictTooBig: c_int,
pub pledgedSrcSize: u64,
pub maxBlockSize: usize,
pub sizing: *const ZSTD_rustCCtxWorkspaceSizing,
pub plan: *mut ZSTD_rustCCtxResetPlan,
}
const _: () = {
assert!(size_of::<ZSTD_rustCCtxResetPlan>() == 9 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, windowSize) == 0);
assert!(offset_of!(ZSTD_rustCCtxResetPlan, blockSize) == size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, maxNbSeq) == 2 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, buffInSize) == 3 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, buffOutSize) == 4 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, maxNbLdmSeq) == 5 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, maxNbExternalSeq) == 6 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, neededSpace) == 7 * size_of::<usize>());
assert!(offset_of!(ZSTD_rustCCtxResetPlan, needsIndexReset) == size_of::<[usize; 8]>());
assert!(offset_of!(ZSTD_rustCCtxResetState, cParams) == 0);
assert!(
offset_of!(ZSTD_rustCCtxResetState, ldmEnable) == size_of::<ZSTD_compressionParameters>()
);
assert!(
offset_of!(ZSTD_rustCCtxResetState, pledgedSrcSize)
> offset_of!(ZSTD_rustCCtxResetState, dictTooBig)
);
assert!(
offset_of!(ZSTD_rustCCtxResetState, maxBlockSize)
== offset_of!(ZSTD_rustCCtxResetState, pledgedSrcSize) + size_of::<u64>()
);
assert!(
offset_of!(ZSTD_rustCCtxResetState, sizing)
== offset_of!(ZSTD_rustCCtxResetState, maxBlockSize) + size_of::<usize>()
);
assert!(
offset_of!(ZSTD_rustCCtxResetState, plan)
== offset_of!(ZSTD_rustCCtxResetState, sizing) + size_of::<usize>()
);
assert!(
size_of::<ZSTD_rustCCtxResetState>()
== offset_of!(ZSTD_rustCCtxResetState, plan) + size_of::<usize>()
);
};
/// Compute the scalar portion of `ZSTD_resetCCtx_internal()`.
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_planCCtxReset(state: *const ZSTD_rustCCtxResetState) -> usize {
if state.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let state = unsafe { &*state };
if state.sizing.is_null() || state.plan.is_null() {
return ERROR(ZstdErrorCode::Generic);
}
let ldm_enabled = state.ldmEnable == ZSTD_RUST_PS_ENABLE;
if ldm_enabled && state.ldmMinMatchLength == 0 {
return ERROR(ZstdErrorCode::Generic);
}
let window_limit = 1u64
.checked_shl(state.cParams.windowLog)
.unwrap_or(u64::MAX);
let window_size = (window_limit
.min(state.pledgedSrcSize)
.min(usize::MAX as u64) as usize)
.max(1);
let block_size = state.maxBlockSize.min(window_size);
let max_nb_seq = ZSTD_rust_params_maxNbSeq(
block_size,
state.cParams.minMatch,
state.useSequenceProducer,
);
let buff_out_size = if state.outBufferBuffered != 0 {
crate::zstd_compress_api::ZSTD_compressBound(block_size).wrapping_add(1)
} else {
0
};
let buff_in_size = if state.inBufferBuffered != 0 {
window_size.wrapping_add(block_size)
} else {
0
};
let max_nb_ldm_seq = if ldm_enabled {
block_size / state.ldmMinMatchLength as usize
} else {
0
};
let max_nb_external_seq = if state.useSequenceProducer != 0 {
crate::zstd_compress_api::ZSTD_sequenceBound(block_size)
} else {
0
};
let needed_space = unsafe {
ZSTD_rust_estimateCCtxWorkspaceSize(
state.cParams,
state.ldmEnable,
state.ldmHashLog,
state.ldmBucketSizeLog,
state.ldmMinMatchLength,
state.isStatic,
state.useRowMatchFinder,
buff_in_size,
buff_out_size,
state.pledgedSrcSize,
state.useSequenceProducer,
state.maxBlockSize,
state.sizing,
)
};
if ERR_isError(needed_space) {
return needed_space;
}
unsafe {
*state.plan = ZSTD_rustCCtxResetPlan {
windowSize: window_size,
blockSize: block_size,
maxNbSeq: max_nb_seq,
buffInSize: buff_in_size,
buffOutSize: buff_out_size,
maxNbLdmSeq: max_nb_ldm_seq,
maxNbExternalSeq: max_nb_external_seq,
neededSpace: needed_space,
needsIndexReset: c_int::from(
state.indexTooClose != 0 || state.dictTooBig != 0 || state.initialized == 0,
),
};
}
0
}
#[inline]
fn max_estimate_cctx_size(
estimate0: usize,
@@ -13668,6 +13839,142 @@ mod tests {
}
}
fn cctx_reset_test_state(
sizing: &ZSTD_rustCCtxWorkspaceSizing,
plan: &mut ZSTD_rustCCtxResetPlan,
pledged_src_size: u64,
max_block_size: usize,
ldm_enable: c_int,
ldm_min_match_length: u32,
in_buffer_buffered: c_int,
out_buffer_buffered: c_int,
use_sequence_producer: c_int,
initialized: c_int,
index_too_close: c_int,
dict_too_big: c_int,
) -> ZSTD_rustCCtxResetState {
ZSTD_rustCCtxResetState {
cParams: cctx_workspace_test_cparams(),
ldmEnable: ldm_enable,
ldmHashLog: 4,
ldmBucketSizeLog: 2,
ldmMinMatchLength: ldm_min_match_length,
isStatic: 0,
useRowMatchFinder: ZSTD_RUST_PS_DISABLE,
inBufferBuffered: in_buffer_buffered,
outBufferBuffered: out_buffer_buffered,
useSequenceProducer: use_sequence_producer,
initialized,
indexTooClose: index_too_close,
dictTooBig: dict_too_big,
pledgedSrcSize: pledged_src_size,
maxBlockSize: max_block_size,
sizing,
plan,
}
}
#[test]
fn cctx_reset_plan_computes_window_buffers_and_sequence_capacities() {
let sizing = cctx_workspace_test_sizing();
let mut plan = ZSTD_rustCCtxResetPlan::default();
let state = cctx_reset_test_state(
&sizing,
&mut plan,
1000,
512,
ZSTD_RUST_PS_DISABLE,
0,
1,
1,
1,
1,
0,
0,
);
assert_eq!(unsafe { ZSTD_rust_planCCtxReset(&state) }, 0);
assert_eq!(plan.windowSize, 1000);
assert_eq!(plan.blockSize, 512);
assert_eq!(plan.maxNbSeq, 512 / 3);
assert_eq!(plan.buffInSize, 1000 + 512);
assert_eq!(
plan.buffOutSize,
crate::zstd_compress_api::ZSTD_compressBound(512) + 1
);
assert_eq!(
plan.maxNbExternalSeq,
crate::zstd_compress_api::ZSTD_sequenceBound(512)
);
assert_eq!(plan.maxNbLdmSeq, 0);
assert_eq!(plan.needsIndexReset, 0);
assert!(!ERR_isError(plan.neededSpace));
assert_ne!(plan.neededSpace, 0);
}
#[test]
fn cctx_reset_plan_tracks_ldm_and_index_reset_policy() {
let sizing = cctx_workspace_test_sizing();
let mut plan = ZSTD_rustCCtxResetPlan::default();
let mut state = cctx_reset_test_state(
&sizing,
&mut plan,
u64::MAX,
2048,
ZSTD_RUST_PS_ENABLE,
64,
0,
0,
0,
0,
0,
0,
);
assert_eq!(unsafe { ZSTD_rust_planCCtxReset(&state) }, 0);
assert_eq!(plan.windowSize, 1 << 10);
assert_eq!(plan.blockSize, 1 << 10);
assert_eq!(plan.maxNbLdmSeq, (1 << 10) / 64);
assert_eq!(plan.buffInSize, 0);
assert_eq!(plan.buffOutSize, 0);
assert_eq!(plan.needsIndexReset, 1);
state.initialized = 1;
state.indexTooClose = 1;
assert_eq!(unsafe { ZSTD_rust_planCCtxReset(&state) }, 0);
assert_eq!(plan.needsIndexReset, 1);
state.indexTooClose = 0;
state.dictTooBig = 1;
assert_eq!(unsafe { ZSTD_rust_planCCtxReset(&state) }, 0);
assert_eq!(plan.needsIndexReset, 1);
}
#[test]
fn cctx_reset_plan_rejects_unadjusted_enabled_ldm() {
let sizing = cctx_workspace_test_sizing();
let mut plan = ZSTD_rustCCtxResetPlan::default();
let state = cctx_reset_test_state(
&sizing,
&mut plan,
1000,
512,
ZSTD_RUST_PS_ENABLE,
0,
0,
0,
0,
1,
0,
0,
);
assert_eq!(
unsafe { ZSTD_rust_planCCtxReset(&state) },
ERROR(ZstdErrorCode::Generic)
);
}
#[test]
fn estimate_cctx_workspace_size_keeps_static_and_buffer_components_separate() {
let sizing = cctx_workspace_test_sizing();