feat(decompress): move legacy stream dispatch to Rust

Move the legacy buffered-stream dispatcher out of the C decompression adapter
while keeping ZSTD_DCtx_s private and C-owned. Rust now consumes the existing
ZSTD_rustDctxView projection, detects the legacy version at input.pos, preserves
static-context rejection and stream-stage reset behavior, and handles legacy
context replacement through the version-gated v0.4-v0.7 Rust stream APIs.

The Rust boundary retains the C helper's NULL normalization, dictionary
lifetime sentinel, previous-version cleanup semantics, input/output position
updates, and version-unsupported and no-legacy-support fallbacks. The C legacy
free helper remains unchanged; only the dispatcher declaration and body were
removed from the adapter. Focused tests cover unsupported versions, stage
reset, context reuse/switching, and partial v0.4 input/output progress.

Test Plan:
- `cargo clippy --manifest-path rust/Cargo.toml --no-default-features --features compression` -- passed
- Same clippy command with `--benches` and `--tests` -- passed
- `cargo +nightly fmt --manifest-path rust/Cargo.toml`, followed by all three clippy commands -- passed
- Decompression/legacy clippy with `-D warnings` -- passed
- All 107 Rust tests with v0.4-v0.7 features -- passed
- Focused legacy dispatcher tests: 4 passed
- `make -C lib -j2 lib-mt` and `make -C lib -j2 lib-nomt` -- passed
- `make -C tests -j2 test-legacy` -- built, then failed in the existing simple one-shot path with one v0.8 output byte mismatch (`e0` vs expected `e2`) before the streaming check; the new dispatcher symbol was not called on that failing path
This commit is contained in:
2026-07-18 15:05:10 +02:00
parent d00bcf7d06
commit 5157bab8a9
2 changed files with 428 additions and 51 deletions
-44
View File
@@ -143,10 +143,6 @@ size_t ZSTD_rust_legacy_frame_size_info(const void* src, size_t srcSize,
size_t ZSTD_rust_legacy_decompress(void* dst, size_t dstCapacity,
const void* src, size_t srcSize,
const void* dict, size_t dictSize);
size_t ZSTD_rust_legacy_decompress_stream(ZSTD_DCtx* dctx,
ZSTD_outBuffer* output,
ZSTD_inBuffer* input,
const void* dict, size_t dictSize);
void ZSTD_rust_legacy_free_stream(ZSTD_DCtx* dctx);
void ZSTD_rust_dctx_view(ZSTD_DCtx* dctx, ZSTD_rustDctxView* out)
@@ -428,46 +424,6 @@ size_t ZSTD_rust_legacy_decompress(void* dst, size_t dstCapacity,
#endif
}
size_t ZSTD_rust_legacy_decompress_stream(ZSTD_DCtx* dctx,
ZSTD_outBuffer* output,
ZSTD_inBuffer* input,
const void* dict, size_t dictSize)
{
#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1)
size_t hint;
if (dctx->legacyVersion) {
hint = ZSTD_decompressLegacyStream(dctx->legacyContext,
dctx->legacyVersion, output, input);
if (hint == 0) dctx->streamStage = zdss_init;
return hint;
}
{
const char* const istart = input->pos != 0
? (const char*)input->src + input->pos
: (const char*)input->src;
size_t const inputSize = input->size - input->pos;
U32 const legacyVersion = ZSTD_isLegacy(istart, inputSize);
if (!legacyVersion) return ERROR(prefix_unknown);
if (dctx->staticSize) return ERROR(memory_allocation);
FORWARD_IF_ERROR(ZSTD_initLegacyStream(&dctx->legacyContext,
dctx->previousLegacyVersion,
legacyVersion, dict, dictSize), "");
dctx->legacyVersion = dctx->previousLegacyVersion = legacyVersion;
hint = ZSTD_decompressLegacyStream(dctx->legacyContext, legacyVersion,
output, input);
if (hint == 0) dctx->streamStage = zdss_init;
return hint;
}
#else
(void)dctx;
(void)output;
(void)input;
(void)dict;
(void)dictSize;
return ERROR(prefix_unknown);
#endif
}
void ZSTD_rust_legacy_free_stream(ZSTD_DCtx* dctx)
{
#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1)
+428 -7
View File
@@ -76,6 +76,12 @@ const ZSTD_USE_INDEFINITELY: c_int = -1;
const ZSTD_DONT_USE: c_int = 0;
const ZSTD_USE_ONCE: c_int = 1;
/* The C legacy dispatcher uses a static byte when a zero-sized input,
* output, or dictionary buffer is represented by NULL. Keep the same
* lifetime and mutability without making the private ZSTD_DCtx layout part
* of the Rust ABI. */
static mut LEGACY_EMPTY_BYTE: u8 = 0;
const ZSTDDS_GET_FRAME_HEADER_SIZE: c_int = 0;
const ZSTDDS_DECODE_FRAME_HEADER: c_int = 1;
const ZSTDDS_DECODE_BLOCK_HEADER: c_int = 2;
@@ -343,13 +349,6 @@ unsafe extern "C" {
dict: *const c_void,
dict_size: usize,
) -> usize;
fn ZSTD_rust_legacy_decompress_stream(
dctx: *mut ZSTD_DCtx,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
dict: *const c_void,
dict_size: usize,
) -> usize;
fn ZSTD_rust_legacy_free_stream(dctx: *mut ZSTD_DCtx);
fn ZSTD_decompressBlock_internal(
dctx: *mut ZSTD_DCtx,
@@ -378,6 +377,428 @@ unsafe fn dctx_view(dctx: *mut ZSTD_DCtx) -> ZSTD_rustDctxView {
unsafe { view.assume_init() }
}
#[inline]
fn legacy_empty_const_ptr() -> *const c_void {
ptr::addr_of!(LEGACY_EMPTY_BYTE).cast()
}
#[inline]
fn legacy_empty_mut_ptr() -> *mut c_void {
ptr::addr_of_mut!(LEGACY_EMPTY_BYTE).cast()
}
/* This mirrors ZSTD_freeLegacyStreamContext(), including its deliberately
* ignored error result. The caller owns the projected pointer slot and the
* C helper's historical behavior is to leave that slot untouched while an
* initialization attempt replaces it only after successful creation. */
unsafe fn free_legacy_stream_context(legacy_context: *mut c_void, version: u32) {
match version {
#[cfg(feature = "legacy-v04")]
4 => {
let _ = crate::legacy::zstd_v04::ZBUFFv04_freeDCtx(
legacy_context.cast::<crate::legacy::zstd_v04::ZBUFFv04_DCtx>(),
);
}
#[cfg(feature = "legacy-v05")]
5 => {
let _ = crate::legacy::zstd_v05::ZBUFFv05_freeDCtx(
legacy_context.cast::<crate::legacy::zstd_v05::ZBUFFv05_DCtx>(),
);
}
#[cfg(feature = "legacy-v06")]
6 => {
let _ = crate::legacy::zstd_v06::ZBUFFv06_freeDCtx(
legacy_context.cast::<crate::legacy::zstd_v06::ZBUFFv06_DCtx>(),
);
}
#[cfg(feature = "legacy-v07")]
7 => {
let _ = crate::legacy::zstd_v07::ZBUFFv07_freeDCtx(
legacy_context.cast::<crate::legacy::zstd_v07::ZBUFFv07_DCtx>(),
);
}
_ => {}
}
}
/* This is the narrow Rust equivalent of ZSTD_initLegacyStream(). It only
* touches the three legacy projections in ZSTD_rustDctxView; the enclosing
* decoder context and every version-specific stream context remain opaque to
* Rust callers outside this function. */
unsafe fn init_legacy_stream(
legacy_context_slot: *mut c_void,
previous_version: u32,
new_version: u32,
dict: *const c_void,
dict_size: usize,
) -> usize {
let dict = if dict.is_null() {
debug_assert_eq!(dict_size, 0);
legacy_empty_const_ptr()
} else {
dict
};
if previous_version != new_version {
let legacy_context = field::<*mut c_void>(legacy_context_slot);
free_legacy_stream_context(legacy_context, previous_version);
}
match new_version {
/* The old formats can be recognized by ZSTD_isLegacy(), but their
* buffered streaming API was never supported by this dispatcher. */
1..=3 => 0,
#[cfg(feature = "legacy-v04")]
4 => {
let context = if previous_version == new_version {
field::<*mut c_void>(legacy_context_slot)
.cast::<crate::legacy::zstd_v04::ZBUFFv04_DCtx>()
} else {
crate::legacy::zstd_v04::ZBUFFv04_createDCtx()
};
if context.is_null() {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
let _ = crate::legacy::zstd_v04::ZBUFFv04_decompressInit(context);
let _ = crate::legacy::zstd_v04::ZBUFFv04_decompressWithDictionary(
context, dict, dict_size,
);
set_field(legacy_context_slot, context.cast::<c_void>());
0
}
#[cfg(feature = "legacy-v05")]
5 => {
let context = if previous_version == new_version {
field::<*mut c_void>(legacy_context_slot)
.cast::<crate::legacy::zstd_v05::ZBUFFv05_DCtx>()
} else {
crate::legacy::zstd_v05::ZBUFFv05_createDCtx()
};
if context.is_null() {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
let _ = crate::legacy::zstd_v05::ZBUFFv05_decompressInitDictionary(
context, dict, dict_size,
);
set_field(legacy_context_slot, context.cast::<c_void>());
0
}
#[cfg(feature = "legacy-v06")]
6 => {
let context = if previous_version == new_version {
field::<*mut c_void>(legacy_context_slot)
.cast::<crate::legacy::zstd_v06::ZBUFFv06_DCtx>()
} else {
crate::legacy::zstd_v06::ZBUFFv06_createDCtx()
};
if context.is_null() {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
let _ = crate::legacy::zstd_v06::ZBUFFv06_decompressInitDictionary(
context, dict, dict_size,
);
set_field(legacy_context_slot, context.cast::<c_void>());
0
}
#[cfg(feature = "legacy-v07")]
7 => {
let context = if previous_version == new_version {
field::<*mut c_void>(legacy_context_slot)
.cast::<crate::legacy::zstd_v07::ZBUFFv07_DCtx>()
} else {
crate::legacy::zstd_v07::ZBUFFv07_createDCtx()
};
if context.is_null() {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
let _ = crate::legacy::zstd_v07::ZBUFFv07_decompressInitDictionary(
context, dict, dict_size,
);
set_field(legacy_context_slot, context.cast::<c_void>());
0
}
/* Keep the C helper's default behavior: initialization itself is
* successful, and the subsequent stream dispatch reports the
* unsupported version. */
_ => 0,
}
}
/* This mirrors ZSTD_decompressLegacyStream(). In particular, legacy
* buffered decoders receive only the remaining input/output slices and then
* return the amount they consumed/produced through the public buffer
* positions. */
unsafe fn decompress_legacy_stream(
legacy_context: *mut c_void,
version: u32,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
) -> usize {
if (*output).dst.is_null() {
debug_assert_eq!((*output).size, 0);
(*output).dst = legacy_empty_mut_ptr();
}
if (*input).src.is_null() {
debug_assert_eq!((*input).size, 0);
(*input).src = legacy_empty_const_ptr();
}
match version {
#[cfg(feature = "legacy-v04")]
4 => {
let src = (*input).src.cast::<u8>().wrapping_add((*input).pos);
let mut read_size = (*input).size.wrapping_sub((*input).pos);
let dst = (*output).dst.cast::<u8>().wrapping_add((*output).pos);
let mut decoded_size = (*output).size.wrapping_sub((*output).pos);
let hint = crate::legacy::zstd_v04::ZBUFFv04_decompressContinue(
legacy_context.cast(),
dst.cast(),
&mut decoded_size,
src.cast(),
&mut read_size,
);
(*output).pos = (*output).pos.wrapping_add(decoded_size);
(*input).pos = (*input).pos.wrapping_add(read_size);
hint
}
#[cfg(feature = "legacy-v05")]
5 => {
let src = (*input).src.cast::<u8>().wrapping_add((*input).pos);
let mut read_size = (*input).size.wrapping_sub((*input).pos);
let dst = (*output).dst.cast::<u8>().wrapping_add((*output).pos);
let mut decoded_size = (*output).size.wrapping_sub((*output).pos);
let hint = crate::legacy::zstd_v05::ZBUFFv05_decompressContinue(
legacy_context.cast(),
dst.cast(),
&mut decoded_size,
src.cast(),
&mut read_size,
);
(*output).pos = (*output).pos.wrapping_add(decoded_size);
(*input).pos = (*input).pos.wrapping_add(read_size);
hint
}
#[cfg(feature = "legacy-v06")]
6 => {
let src = (*input).src.cast::<u8>().wrapping_add((*input).pos);
let mut read_size = (*input).size.wrapping_sub((*input).pos);
let dst = (*output).dst.cast::<u8>().wrapping_add((*output).pos);
let mut decoded_size = (*output).size.wrapping_sub((*output).pos);
let hint = crate::legacy::zstd_v06::ZBUFFv06_decompressContinue(
legacy_context.cast(),
dst.cast(),
&mut decoded_size,
src.cast(),
&mut read_size,
);
(*output).pos = (*output).pos.wrapping_add(decoded_size);
(*input).pos = (*input).pos.wrapping_add(read_size);
hint
}
#[cfg(feature = "legacy-v07")]
7 => {
let src = (*input).src.cast::<u8>().wrapping_add((*input).pos);
let mut read_size = (*input).size.wrapping_sub((*input).pos);
let dst = (*output).dst.cast::<u8>().wrapping_add((*output).pos);
let mut decoded_size = (*output).size.wrapping_sub((*output).pos);
let hint = crate::legacy::zstd_v07::ZBUFFv07_decompressContinue(
legacy_context.cast(),
dst.cast(),
&mut decoded_size,
src.cast(),
&mut read_size,
);
(*output).pos = (*output).pos.wrapping_add(decoded_size);
(*input).pos = (*input).pos.wrapping_add(read_size);
hint
}
_ => ERROR(ZstdErrorCode::VersionUnsupported),
}
}
#[inline]
unsafe fn reset_legacy_stage_if_done(stream_stage: *mut c_void, hint: usize) -> usize {
if hint == 0 {
set_field(stream_stage, ZDSS_INIT);
}
hint
}
#[no_mangle]
pub unsafe extern "C" fn ZSTD_rust_legacy_decompress_stream(
dctx: *mut ZSTD_DCtx,
output: *mut ZSTD_outBuffer,
input: *mut ZSTD_inBuffer,
dict: *const c_void,
dict_size: usize,
) -> usize {
let view = dctx_view(dctx);
/* With ZSTD_LEGACY_SUPPORT disabled, the C projection deliberately leaves
* these slots null. This is the exact no-legacy-support fallback. */
if view.legacy_version.is_null() {
return ERROR(ZstdErrorCode::PrefixUnknown);
}
let current_version = field::<u32>(view.legacy_version);
if current_version != 0 {
let hint = decompress_legacy_stream(
field::<*mut c_void>(view.legacy_context),
current_version,
output,
input,
);
return reset_legacy_stage_if_done(view.stream_stage, hint);
}
let input_ref = &*input;
let istart = if input_ref.pos != 0 {
input_ref
.src
.cast::<u8>()
.wrapping_add(input_ref.pos)
.cast()
} else {
input_ref.src
};
let input_size = input_ref.size.wrapping_sub(input_ref.pos);
let legacy_version = ZSTD_rust_legacy_is(istart, input_size);
if legacy_version == 0 {
return ERROR(ZstdErrorCode::PrefixUnknown);
}
if field::<usize>(view.static_size) != 0 {
return ERROR(ZstdErrorCode::MemoryAllocation);
}
let previous_version = field::<u32>(view.previous_legacy_version);
let init = init_legacy_stream(
view.legacy_context,
previous_version,
legacy_version,
dict,
dict_size,
);
if ERR_isError(init) {
return init;
}
set_field(view.legacy_version, legacy_version);
set_field(view.previous_legacy_version, legacy_version);
let hint = decompress_legacy_stream(
field::<*mut c_void>(view.legacy_context),
legacy_version,
output,
input,
);
reset_legacy_stage_if_done(view.stream_stage, hint)
}
#[cfg(test)]
mod legacy_stream_dispatch_tests {
use super::*;
#[test]
fn rejects_unsupported_versions_after_normalizing_empty_buffers() {
let mut output = ZSTD_outBuffer {
dst: ptr::null_mut(),
size: 0,
pos: 0,
};
let mut input = ZSTD_inBuffer {
src: ptr::null(),
size: 0,
pos: 0,
};
let result =
unsafe { decompress_legacy_stream(ptr::null_mut(), 3, &mut output, &mut input) };
assert_eq!(result, ERROR(ZstdErrorCode::VersionUnsupported));
assert!(!output.dst.is_null());
assert!(!input.src.is_null());
assert_eq!(output.pos, 0);
assert_eq!(input.pos, 0);
}
#[test]
fn resets_stage_only_for_a_completed_legacy_call() {
let mut stage = ZDSS_READ;
let stage_slot = (&mut stage as *mut c_int).cast::<c_void>();
assert_eq!(unsafe { reset_legacy_stage_if_done(stage_slot, 0) }, 0);
assert_eq!(stage, ZDSS_INIT);
stage = ZDSS_READ;
assert_eq!(unsafe { reset_legacy_stage_if_done(stage_slot, 7) }, 7);
assert_eq!(stage, ZDSS_READ);
}
#[cfg(all(feature = "legacy-v04", feature = "legacy-v05"))]
#[test]
fn switches_and_reuses_versioned_stream_contexts() {
let mut context = ptr::null_mut::<c_void>();
let context_slot = (&mut context as *mut *mut c_void).cast::<c_void>();
unsafe {
assert_eq!(init_legacy_stream(context_slot, 0, 4, ptr::null(), 0), 0);
let v04_context = context;
assert!(!v04_context.is_null());
assert_eq!(init_legacy_stream(context_slot, 4, 4, ptr::null(), 0), 0);
assert_eq!(context, v04_context);
assert_eq!(init_legacy_stream(context_slot, 4, 5, ptr::null(), 0), 0);
assert!(!context.is_null());
free_legacy_stream_context(context, 5);
}
}
#[cfg(feature = "legacy-v04")]
#[test]
fn tracks_partial_input_and_output_positions() {
const FRAME: &[u8] = &[
0x24, 0xB5, 0x2F, 0xFD, 0x00, 0x40, 0x00, 0x0B, b'r', b'a', b'w', b' ', b'v', b'0',
b'.', b'4', b'!', b'!', b'!', 0xC0, 0x00, 0x00,
];
let expected = b"raw v0.4!!!";
let mut context = ptr::null_mut::<c_void>();
let context_slot = (&mut context as *mut *mut c_void).cast::<c_void>();
unsafe {
assert_eq!(init_legacy_stream(context_slot, 0, 4, ptr::null(), 0), 0);
}
let mut input_pos = 0;
let mut decoded = Vec::new();
for _ in 0..128 {
let input_end = min(FRAME.len(), input_pos + 1);
let mut input = ZSTD_inBuffer {
src: FRAME.as_ptr().cast(),
size: input_end,
pos: input_pos,
};
let mut chunk = [0u8; 2];
let mut output = ZSTD_outBuffer {
dst: chunk.as_mut_ptr().cast(),
size: chunk.len(),
pos: 0,
};
let old_input_pos = input.pos;
let hint = unsafe { decompress_legacy_stream(context, 4, &mut output, &mut input) };
assert!(!ERR_isError(hint));
assert!(input.pos > old_input_pos || output.pos != 0 || input_end == FRAME.len());
input_pos = input.pos;
decoded.extend_from_slice(&chunk[..output.pos]);
if input_pos == FRAME.len() && decoded == expected {
break;
}
}
assert_eq!(input_pos, FRAME.len());
assert_eq!(decoded, expected);
unsafe { free_legacy_stream_context(context, 4) };
}
}
#[inline]
fn frame_header_prefix(format: c_int) -> usize {
if format == ZSTD_F_ZSTD1 {