From 5157bab8a950804676e584327bd00adaf364bbbf Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 15:05:10 +0200 Subject: [PATCH] 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 --- lib/decompress/zstd_decompress.c | 44 ---- rust/src/zstd_decompress.rs | 435 ++++++++++++++++++++++++++++++- 2 files changed, 428 insertions(+), 51 deletions(-) diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c index 521f2bb84..c741ce1e1 100644 --- a/lib/decompress/zstd_decompress.c +++ b/lib/decompress/zstd_decompress.c @@ -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) diff --git a/rust/src/zstd_decompress.rs b/rust/src/zstd_decompress.rs index 95509ec00..726fd437a 100644 --- a/rust/src/zstd_decompress.rs +++ b/rust/src/zstd_decompress.rs @@ -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::(), + ); + } + #[cfg(feature = "legacy-v05")] + 5 => { + let _ = crate::legacy::zstd_v05::ZBUFFv05_freeDCtx( + legacy_context.cast::(), + ); + } + #[cfg(feature = "legacy-v06")] + 6 => { + let _ = crate::legacy::zstd_v06::ZBUFFv06_freeDCtx( + legacy_context.cast::(), + ); + } + #[cfg(feature = "legacy-v07")] + 7 => { + let _ = crate::legacy::zstd_v07::ZBUFFv07_freeDCtx( + legacy_context.cast::(), + ); + } + _ => {} + } +} + +/* 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::() + } 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::()); + 0 + } + #[cfg(feature = "legacy-v05")] + 5 => { + let context = if previous_version == new_version { + field::<*mut c_void>(legacy_context_slot) + .cast::() + } 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::()); + 0 + } + #[cfg(feature = "legacy-v06")] + 6 => { + let context = if previous_version == new_version { + field::<*mut c_void>(legacy_context_slot) + .cast::() + } 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::()); + 0 + } + #[cfg(feature = "legacy-v07")] + 7 => { + let context = if previous_version == new_version { + field::<*mut c_void>(legacy_context_slot) + .cast::() + } 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::()); + 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::().wrapping_add((*input).pos); + let mut read_size = (*input).size.wrapping_sub((*input).pos); + let dst = (*output).dst.cast::().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::().wrapping_add((*input).pos); + let mut read_size = (*input).size.wrapping_sub((*input).pos); + let dst = (*output).dst.cast::().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::().wrapping_add((*input).pos); + let mut read_size = (*input).size.wrapping_sub((*input).pos); + let dst = (*output).dst.cast::().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::().wrapping_add((*input).pos); + let mut read_size = (*input).size.wrapping_sub((*input).pos); + let dst = (*output).dst.cast::().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::(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::() + .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::(view.static_size) != 0 { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + + let previous_version = field::(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::(); + + 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::(); + let context_slot = (&mut context as *mut *mut c_void).cast::(); + + 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::(); + let context_slot = (&mut context as *mut *mut c_void).cast::(); + + 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 {