From 4304c0af40ab4be6835e6fe1868a01b58ed398d4 Mon Sep 17 00:00:00 2001 From: ddidderr Date: Sat, 18 Jul 2026 22:41:17 +0200 Subject: [PATCH] feat(decompress): move legacy dispatch into Rust Legacy detection, frame-size queries, one-shot decode, and buffered streaming were still routed through the C header dispatcher even though every historical v0.1 through v0.7 decoder is already ported under Rust feature gates. The C implementation also duplicated the support-level boundary independently from the Rust archive selection. Move the dispatcher and stream lifecycle to Rust using the existing DCtx field projection. Rust now selects the version, preserves the null-buffer and position semantics, initializes and frees buffered legacy contexts, and calls the frozen per-version modules. C retains only the configuration leaf that reports ZSTD_LEGACY_SUPPORT plus the private DCtx layout and trace helpers, so library, program, and test builds can still choose different support levels. Test Plan: - `cargo test --manifest-path rust/Cargo.toml --no-default-features --features compression,decompression,dict-builder,legacy-v01,legacy-v02,legacy-v03,legacy-v04,legacy-v05,legacy-v06,legacy-v07 --all-targets -- --test-threads=1` -- 528 passed. - Native `test-legacy`, `test-invalidDictionaries`, `test-decodecorpus`, and `test-zstd` -- passed. - Strict C syntax checks with legacy support 0, 4, 5, and 7 -- passed. - `cargo clippy` default, benches, and tests for library and CLI -- passed. --- lib/decompress/zstd_decompress.c | 104 +----- rust/src/zstd_decompress.rs | 610 ++++++++++++++++++++++++++++++- 2 files changed, 600 insertions(+), 114 deletions(-) diff --git a/lib/decompress/zstd_decompress.c b/lib/decompress/zstd_decompress.c index 9c8823f98..1d5788ef3 100644 --- a/lib/decompress/zstd_decompress.c +++ b/lib/decompress/zstd_decompress.c @@ -16,10 +16,6 @@ #include "zstd_decompress_internal.h" #include "zstd_ddict.h" -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) -# include "../legacy/zstd_legacy.h" -#endif - /* Keep the three public build-time tuning knobs in the C configuration domain. * Rust queries them through the small leaves below rather than baking a second * set of defaults into its source. */ @@ -125,18 +121,7 @@ void ZSTD_rust_dctx_copy_prefix(ZSTD_DCtx* dstDCtx, const ZSTD_DCtx* srcDCtx); void ZSTD_rust_dctx_trace_begin(ZSTD_DCtx* dctx); void ZSTD_rust_dctx_trace_end(ZSTD_DCtx* dctx, U64 uncompressedSize, U64 compressedSize, int streaming); -unsigned ZSTD_rust_legacy_is(const void* src, size_t srcSize); -unsigned long long ZSTD_rust_legacy_get_decompressed_size(const void* src, - size_t srcSize); -size_t ZSTD_rust_legacy_find_compressed_size(const void* src, size_t srcSize); -size_t ZSTD_rust_legacy_frame_size_info(const void* src, size_t srcSize, - size_t* compressedSize, - unsigned long long* decompressedBound, - size_t* nbBlocks); -size_t ZSTD_rust_legacy_decompress(void* dst, size_t dstCapacity, - const void* src, size_t srcSize, - const void* dict, size_t dictSize); -void ZSTD_rust_legacy_free_stream(ZSTD_DCtx* dctx); +unsigned ZSTD_rust_legacy_support(void); void ZSTD_rust_dctx_view(ZSTD_DCtx* dctx, ZSTD_rustDctxView* out) { @@ -319,87 +304,16 @@ void ZSTD_rust_dctx_trace_end(ZSTD_DCtx* dctx, U64 uncompressedSize, #endif } -unsigned ZSTD_rust_legacy_is(const void* src, size_t srcSize) +/* The Rust dispatcher has all version-specific legacy implementations. Keep + * the active C configuration as a leaf so test builds, which compile every + * Rust legacy module but vary this C macro per target, retain the historical + * support boundary. */ +unsigned ZSTD_rust_legacy_support(void) { -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) - return ZSTD_isLegacy(src, srcSize); +#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) \ + && (ZSTD_LEGACY_SUPPORT <= 7) + return ZSTD_LEGACY_SUPPORT; #else - (void)src; - (void)srcSize; return 0; #endif } - -unsigned long long ZSTD_rust_legacy_get_decompressed_size(const void* src, size_t srcSize) -{ -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) - return ZSTD_getDecompressedSize_legacy(src, srcSize); -#else - (void)src; - (void)srcSize; - return 0; -#endif -} - -size_t ZSTD_rust_legacy_find_compressed_size(const void* src, size_t srcSize) -{ -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) - return ZSTD_findFrameCompressedSizeLegacy(src, srcSize); -#else - (void)src; - (void)srcSize; - return ERROR(prefix_unknown); -#endif -} - -size_t ZSTD_rust_legacy_frame_size_info(const void* src, size_t srcSize, - size_t* compressedSize, - unsigned long long* decompressedBound, - size_t* nbBlocks) -{ -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) - ZSTD_frameSizeInfo const info = ZSTD_findFrameSizeInfoLegacy(src, srcSize); - *compressedSize = info.compressedSize; - *decompressedBound = info.decompressedBound; - *nbBlocks = info.decompressedBound == ZSTD_CONTENTSIZE_ERROR - ? 0 : (size_t)(info.decompressedBound / ZSTD_BLOCKSIZE_MAX); - return ZSTD_isError(info.compressedSize) ? info.compressedSize : 0; -#else - (void)src; - (void)srcSize; - *compressedSize = ERROR(prefix_unknown); - *decompressedBound = ZSTD_CONTENTSIZE_ERROR; - *nbBlocks = 0; - return ERROR(prefix_unknown); -#endif -} - -size_t ZSTD_rust_legacy_decompress(void* dst, size_t dstCapacity, - const void* src, size_t srcSize, - const void* dict, size_t dictSize) -{ -#if defined(ZSTD_LEGACY_SUPPORT) && (ZSTD_LEGACY_SUPPORT >= 1) - return ZSTD_decompressLegacy(dst, dstCapacity, src, srcSize, dict, dictSize); -#else - (void)dst; - (void)dstCapacity; - (void)src; - (void)srcSize; - (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) - if (dctx->legacyContext) { - ZSTD_freeLegacyStreamContext(dctx->legacyContext, - dctx->previousLegacyVersion); - dctx->legacyContext = NULL; - } -#else - (void)dctx; -#endif -} diff --git a/rust/src/zstd_decompress.rs b/rust/src/zstd_decompress.rs index b49240cdf..7419e953f 100644 --- a/rust/src/zstd_decompress.rs +++ b/rust/src/zstd_decompress.rs @@ -327,25 +327,19 @@ unsafe extern "C" { ); fn ZSTD_rust_dctx_trace_begin(dctx: *mut ZSTD_DCtx); fn ZSTD_rust_dctx_copy_prefix(dst: *mut ZSTD_DCtx, src: *const ZSTD_DCtx); - fn ZSTD_rust_legacy_is(src: *const c_void, src_size: usize) -> c_uint; - fn ZSTD_rust_legacy_get_decompressed_size(src: *const c_void, src_size: usize) -> u64; - fn ZSTD_rust_legacy_find_compressed_size(src: *const c_void, src_size: usize) -> usize; - fn ZSTD_rust_legacy_frame_size_info( - src: *const c_void, - src_size: usize, - compressed_size: *mut usize, - decompressed_bound: *mut u64, - nb_blocks: *mut usize, - ) -> usize; - fn ZSTD_rust_legacy_decompress( - dst: *mut c_void, - dst_capacity: usize, - src: *const c_void, - src_size: usize, - dict: *const c_void, - dict_size: usize, - ) -> usize; - fn ZSTD_rust_legacy_free_stream(dctx: *mut ZSTD_DCtx); + #[cfg(all( + not(test), + any( + feature = "legacy-v01", + feature = "legacy-v02", + feature = "legacy-v03", + feature = "legacy-v04", + feature = "legacy-v05", + feature = "legacy-v06", + feature = "legacy-v07" + ) + ))] + fn ZSTD_rust_legacy_support() -> c_uint; fn ZSTD_decompressBlock_internal( dctx: *mut ZSTD_DCtx, dst: *mut c_void, @@ -388,6 +382,9 @@ fn legacy_empty_mut_ptr() -> *mut c_void { * 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) { + if _legacy_context.is_null() { + return; + } match version { #[cfg(feature = "legacy-v04")] 4 => { @@ -417,6 +414,395 @@ unsafe fn free_legacy_stream_context(_legacy_context: *mut c_void, version: u32) } } +/* The Rust archive used by the C test harness deliberately contains all + * ported legacy modules, even when a particular test target lowers + * ZSTD_LEGACY_SUPPORT. Query the active C setting instead of deriving the + * dispatch boundary from Cargo features alone. Standalone Rust tests have no + * C adapter to query; enabling all configured modules there gives the helper + * tests a deterministic support boundary without changing production builds. */ +#[inline] +unsafe fn configured_legacy_support() -> u32 { + #[cfg(test)] + { + 1 + } + #[cfg(not(test))] + { + #[cfg(any( + feature = "legacy-v01", + feature = "legacy-v02", + feature = "legacy-v03", + feature = "legacy-v04", + feature = "legacy-v05", + feature = "legacy-v06", + feature = "legacy-v07" + ))] + { + unsafe { ZSTD_rust_legacy_support() } + } + #[cfg(not(any( + feature = "legacy-v01", + feature = "legacy-v02", + feature = "legacy-v03", + feature = "legacy-v04", + feature = "legacy-v05", + feature = "legacy-v06", + feature = "legacy-v07" + )))] + { + 0 + } + } +} + +/* This is the Rust equivalent of ZSTD_isLegacy(). The v0.1 magic is the one + * historical exception: its bytes are checked as the little-endian constant + * 0x1EB52FFD, while v0.2 through v0.7 use the usual 0xFD2FB52N values. */ +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_is(src: *const c_void, src_size: usize) -> c_uint { + if src.is_null() || src_size < ZSTD_FRAMEIDSIZE { + return 0; + } + let support = unsafe { configured_legacy_support() }; + if support == 0 { + return 0; + } + let _magic = unsafe { MEM_readLE32(src) }; + + #[cfg(feature = "legacy-v01")] + if support <= 1 && _magic == 0x1EB5_2FFD { + return 1; + } + #[cfg(feature = "legacy-v02")] + if support <= 2 && _magic == 0xFD2F_B522 { + return 2; + } + #[cfg(feature = "legacy-v03")] + if support <= 3 && _magic == 0xFD2F_B523 { + return 3; + } + #[cfg(feature = "legacy-v04")] + if support <= 4 && _magic == 0xFD2F_B524 { + return 4; + } + #[cfg(feature = "legacy-v05")] + if support <= 5 && _magic == 0xFD2F_B525 { + return 5; + } + #[cfg(feature = "legacy-v06")] + if support <= 6 && _magic == 0xFD2F_B526 { + return 6; + } + #[cfg(feature = "legacy-v07")] + if support <= 7 && _magic == 0xFD2F_B527 { + return 7; + } + 0 +} + +#[inline] +unsafe fn legacy_frame_size_info_values( + src: *const c_void, + src_size: usize, +) -> (usize, u64, usize) { + let version = unsafe { ZSTD_rust_legacy_is(src, src_size) }; + let mut compressed_size = ERROR(ZstdErrorCode::PrefixUnknown); + let mut decompressed_bound = ZSTD_CONTENTSIZE_ERROR; + + match version { + #[cfg(feature = "legacy-v01")] + 1 => unsafe { + crate::legacy::zstd_v01::ZSTDv01_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v02")] + 2 => unsafe { + crate::legacy::zstd_v02::ZSTDv02_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v03")] + 3 => unsafe { + crate::legacy::zstd_v03::ZSTDv03_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v04")] + 4 => unsafe { + crate::legacy::zstd_v04::ZSTDv04_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v05")] + 5 => unsafe { + crate::legacy::zstd_v05::ZSTDv05_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v06")] + 6 => unsafe { + crate::legacy::zstd_v06::ZSTDv06_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + #[cfg(feature = "legacy-v07")] + 7 => unsafe { + crate::legacy::zstd_v07::ZSTDv07_findFrameSizeInfoLegacy( + src, + src_size, + &mut compressed_size, + &mut decompressed_bound, + ); + }, + _ => {} + } + + if !ERR_isError(compressed_size) && compressed_size > src_size { + compressed_size = ERROR(ZstdErrorCode::SrcSizeWrong); + decompressed_bound = ZSTD_CONTENTSIZE_ERROR; + } + + let nb_blocks = if decompressed_bound == ZSTD_CONTENTSIZE_ERROR { + 0 + } else { + debug_assert_eq!(decompressed_bound & (ZSTD_BLOCKSIZE_MAX as u64 - 1), 0); + (decompressed_bound / ZSTD_BLOCKSIZE_MAX as u64) as usize + }; + (compressed_size, decompressed_bound, nb_blocks) +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_get_decompressed_size( + src: *const c_void, + src_size: usize, +) -> u64 { + let version = unsafe { ZSTD_rust_legacy_is(src, src_size) }; + match version { + #[cfg(feature = "legacy-v05")] + 5 => { + /* ZstdParameters is repr(C), and src_size is its first field. The + * field is private inside the frozen module because it is not part + * of that module's public Rust API, so read the documented C ABI + * prefix only after the version-specific parser succeeds. */ + let mut params = MaybeUninit::::zeroed(); + let result = unsafe { + crate::legacy::zstd_v05::ZSTDv05_getFrameParams(params.as_mut_ptr(), src, src_size) + }; + if result == 0 { + unsafe { params.as_ptr().cast::().read() } + } else { + 0 + } + } + #[cfg(feature = "legacy-v06")] + 6 => { + let mut params = crate::legacy::zstd_v06::ZSTDv06_frameParams { + frame_content_size: 0, + window_log: 0, + }; + let result = unsafe { + crate::legacy::zstd_v06::ZSTDv06_getFrameParams(&mut params, src, src_size) + }; + if result == 0 { + params.frame_content_size + } else { + 0 + } + } + #[cfg(feature = "legacy-v07")] + 7 => { + let mut params = crate::legacy::zstd_v07::ZSTDv07_frameParams { + frameContentSize: 0, + windowSize: 0, + dictID: 0, + checksumFlag: 0, + }; + let result = unsafe { + crate::legacy::zstd_v07::ZSTDv07_getFrameParams(&mut params, src, src_size) + }; + if result == 0 { + params.frameContentSize + } else { + 0 + } + } + _ => 0, + } +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_find_compressed_size( + src: *const c_void, + src_size: usize, +) -> usize { + unsafe { legacy_frame_size_info_values(src, src_size).0 } +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_frame_size_info( + src: *const c_void, + src_size: usize, + compressed_size: *mut usize, + decompressed_bound: *mut u64, + nb_blocks: *mut usize, +) -> usize { + let (c_size, d_bound, blocks) = unsafe { legacy_frame_size_info_values(src, src_size) }; + unsafe { + compressed_size.write(c_size); + decompressed_bound.write(d_bound); + nb_blocks.write(blocks); + } + if ERR_isError(c_size) { + c_size + } else { + 0 + } +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_decompress( + dst: *mut c_void, + dst_capacity: usize, + src: *const c_void, + src_size: usize, + dict: *const c_void, + dict_size: usize, +) -> usize { + let version = unsafe { ZSTD_rust_legacy_is(src, src_size) }; + let _dst = if dst.is_null() { + debug_assert_eq!(dst_capacity, 0); + legacy_empty_mut_ptr() + } else { + dst + }; + let _src = if src.is_null() { + debug_assert_eq!(src_size, 0); + legacy_empty_const_ptr() + } else { + src + }; + let _dict = if dict.is_null() { + debug_assert_eq!(dict_size, 0); + legacy_empty_const_ptr() + } else { + dict + }; + + match version { + #[cfg(feature = "legacy-v01")] + 1 => unsafe { + crate::legacy::zstd_v01::ZSTDv01_decompress(_dst, dst_capacity, _src, src_size) + }, + #[cfg(feature = "legacy-v02")] + 2 => unsafe { + crate::legacy::zstd_v02::ZSTDv02_decompress(_dst, dst_capacity, _src, src_size) + }, + #[cfg(feature = "legacy-v03")] + 3 => unsafe { + crate::legacy::zstd_v03::ZSTDv03_decompress(_dst, dst_capacity, _src, src_size) + }, + #[cfg(feature = "legacy-v04")] + 4 => unsafe { + crate::legacy::zstd_v04::ZSTDv04_decompress(_dst, dst_capacity, _src, src_size) + }, + #[cfg(feature = "legacy-v05")] + 5 => { + let dctx = unsafe { crate::legacy::zstd_v05::ZSTDv05_createDCtx() }; + if dctx.is_null() { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + let result = unsafe { + crate::legacy::zstd_v05::ZSTDv05_decompress_usingDict( + dctx, + _dst, + dst_capacity, + _src, + src_size, + _dict, + dict_size, + ) + }; + unsafe { crate::legacy::zstd_v05::ZSTDv05_freeDCtx(dctx) }; + result + } + #[cfg(feature = "legacy-v06")] + 6 => { + let dctx = unsafe { crate::legacy::zstd_v06::ZSTDv06_createDCtx() }; + if dctx.is_null() { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + let result = unsafe { + crate::legacy::zstd_v06::ZSTDv06_decompress_usingDict( + dctx, + _dst, + dst_capacity, + _src, + src_size, + _dict, + dict_size, + ) + }; + unsafe { crate::legacy::zstd_v06::ZSTDv06_freeDCtx(dctx) }; + result + } + #[cfg(feature = "legacy-v07")] + 7 => { + let dctx = unsafe { crate::legacy::zstd_v07::ZSTDv07_createDCtx() }; + if dctx.is_null() { + return ERROR(ZstdErrorCode::MemoryAllocation); + } + let result = unsafe { + crate::legacy::zstd_v07::ZSTDv07_decompress_usingDict( + dctx, + _dst, + dst_capacity, + _src, + src_size, + _dict, + dict_size, + ) + }; + unsafe { crate::legacy::zstd_v07::ZSTDv07_freeDCtx(dctx) }; + result + } + _ => ERROR(ZstdErrorCode::PrefixUnknown), + } +} + +#[no_mangle] +pub unsafe extern "C" fn ZSTD_rust_legacy_free_stream(dctx: *mut ZSTD_DCtx) { + if dctx.is_null() { + return; + } + let view = unsafe { dctx_view(dctx) }; + if view.legacy_context.is_null() { + return; + } + let legacy_context = unsafe { field::<*mut c_void>(view.legacy_context) }; + let previous_version = unsafe { field::(view.previous_legacy_version) }; + unsafe { free_legacy_stream_context(legacy_context, previous_version) }; + unsafe { set_field(view.legacy_context, ptr::null_mut::()) }; +} + /* 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 @@ -795,6 +1181,192 @@ mod legacy_stream_dispatch_tests { } } +#[cfg(test)] +mod legacy_dispatch_helper_tests { + use super::*; + + #[test] + fn legacy_detection_rejects_short_null_and_unknown_input() { + assert_eq!(unsafe { ZSTD_rust_legacy_is(ptr::null(), 0) }, 0); + let short = [0x27, 0xB5, 0x2F]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(short.as_ptr().cast(), short.len()) }, + 0 + ); + let unknown = [0u8; ZSTD_FRAMEIDSIZE]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(unknown.as_ptr().cast(), unknown.len()) }, + 0 + ); + } + + #[cfg(feature = "legacy-v01")] + #[test] + fn detects_v01_magic() { + let magic: [u8; 4] = [0xFD, 0x2F, 0xB5, 0x1E]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 1 + ); + } + + #[cfg(feature = "legacy-v02")] + #[test] + fn detects_v02_magic() { + let magic: [u8; 4] = [0x22, 0xB5, 0x2F, 0xFD]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 2 + ); + } + + #[cfg(feature = "legacy-v03")] + #[test] + fn detects_v03_magic() { + let magic: [u8; 4] = [0x23, 0xB5, 0x2F, 0xFD]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 3 + ); + } + + #[cfg(feature = "legacy-v04")] + #[test] + fn detects_v04_magic() { + let magic: [u8; 4] = [0x24, 0xB5, 0x2F, 0xFD]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 4 + ); + } + + #[cfg(feature = "legacy-v05")] + #[test] + fn detects_v05_magic() { + let magic: [u8; 4] = [0x25, 0xB5, 0x2F, 0xFD]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 5 + ); + } + + #[cfg(feature = "legacy-v06")] + #[test] + fn detects_v06_magic() { + let magic: [u8; 4] = [0x26, 0xB5, 0x2F, 0xFD]; + assert_eq!( + unsafe { ZSTD_rust_legacy_is(magic.as_ptr().cast(), magic.len()) }, + 6 + ); + } + + #[cfg(feature = "legacy-v07")] + #[test] + fn legacy_v07_size_and_one_shot_helpers_match_frame() { + let payload = b"legacy-dispatch"; + let mut frame = Vec::with_capacity(6 + 3 + payload.len() + 3); + frame.extend_from_slice(&[0x27, 0xB5, 0x2F, 0xFD]); + frame.extend_from_slice(&[0x20, payload.len() as u8]); + frame.extend_from_slice(&[ + 0x40 | (((payload.len() >> 16) & 7) as u8), + (payload.len() >> 8) as u8, + payload.len() as u8, + ]); + frame.extend_from_slice(payload); + frame.extend_from_slice(&[0xC0, 0, 0]); + + let mut compressed_size = 0; + let mut decompressed_bound = 0; + let mut nb_blocks = 0; + let status = unsafe { + ZSTD_rust_legacy_frame_size_info( + frame.as_ptr().cast(), + frame.len(), + &mut compressed_size, + &mut decompressed_bound, + &mut nb_blocks, + ) + }; + assert_eq!(status, 0); + assert_eq!(compressed_size, frame.len()); + assert_eq!( + decompressed_bound, ZSTD_BLOCKSIZE_MAX as u64, + "legacy frame bounds are reported in whole legacy blocks" + ); + assert_eq!(nb_blocks, 1); + assert_eq!( + unsafe { ZSTD_rust_legacy_find_compressed_size(frame.as_ptr().cast(), frame.len()) }, + frame.len() + ); + assert_eq!( + unsafe { ZSTD_rust_legacy_get_decompressed_size(frame.as_ptr().cast(), frame.len()) }, + payload.len() as u64 + ); + + let mut output = vec![0u8; payload.len()]; + let decoded = unsafe { + ZSTD_rust_legacy_decompress( + output.as_mut_ptr().cast(), + output.len(), + frame.as_ptr().cast(), + frame.len(), + ptr::null(), + 0, + ) + }; + assert_eq!(decoded, payload.len()); + assert_eq!(output, payload); + } + + #[cfg(feature = "legacy-v07")] + #[test] + fn legacy_size_and_decompress_helpers_report_truncation() { + let mut truncated = vec![0x27, 0xB5, 0x2F, 0xFD, 0x20, 4, 0x40, 0, 4, b't']; + let mut compressed_size = 0; + let mut decompressed_bound = 0; + let mut nb_blocks = 0; + let status = unsafe { + ZSTD_rust_legacy_frame_size_info( + truncated.as_ptr().cast(), + truncated.len(), + &mut compressed_size, + &mut decompressed_bound, + &mut nb_blocks, + ) + }; + assert_eq!(status, ERROR(ZstdErrorCode::SrcSizeWrong)); + assert_eq!(compressed_size, status); + assert_eq!(decompressed_bound, ZSTD_CONTENTSIZE_ERROR); + assert_eq!(nb_blocks, 0); + assert_eq!( + unsafe { + ZSTD_rust_legacy_find_compressed_size(truncated.as_ptr().cast(), truncated.len()) + }, + status + ); + + let mut output = [0u8; 4]; + let decoded = unsafe { + ZSTD_rust_legacy_decompress( + output.as_mut_ptr().cast(), + output.len(), + truncated.as_ptr().cast(), + truncated.len(), + ptr::null(), + 0, + ) + }; + assert!(ERR_isError(decoded)); + truncated[0] ^= 1; + assert_eq!( + unsafe { + ZSTD_rust_legacy_get_decompressed_size(truncated.as_ptr().cast(), truncated.len()) + }, + 0 + ); + } +} + #[inline] fn frame_header_prefix(format: c_int) -> usize { if format == ZSTD_F_ZSTD1 {